5f7b3b6b7a
Security: shipping admin@local / workout123 as a default that the
operator was supposed-to-rotate-but-might-not is the kind of footgun
that turns into "default-credential exposure" headlines. Eliminated.
prisma/seed.ts now ONLY seeds the InstanceSettings singleton — no
admin user, no UserPreferences, no exercises in the build-time
fallback DB. The image still ships with prisma/exercises.seed.json
(curated 164-exercise library) but those rows aren't inserted until
an admin is created via the StartOS Action.
The change-admin-credentials Action now does INSERT-or-UPDATE in one
shot. CREATE mode (no admin exists) inserts the User row, inserts
UserPreferences with sensible defaults, and runs
ensureExerciseLibrary.cjs for the new admin so they don't have to
wait for the next service start to see the curated library. UPDATE
mode (admin exists) keeps the v1.0.0:1-3 rotation behavior. The
mode is auto-detected by counting `WHERE isAdmin = 1`.
The login page is now a server component that reads the admin count
upfront. Zero admins -> renders a "needs setup" panel pointing at
the StartOS Action ("Services -> Proof of Work -> Actions -> Set
admin credentials"). Otherwise renders the existing LoginForm
(extracted to LoginForm.tsx). Eliminates the
"I tried admin@local/workout123 and it failed, what's wrong"
fresh-installer confusion.
Backward compatible for upgrades from v1.0.0:1-3:
- /data already has an admin user; the no-admin detection never
triggers; login behaves identically to before.
- The Action's UPDATE mode still works for rotation.
Version graph: v1.0.0:4 promoted to current; v1.0.0:1, :2, :3 all
listed as `other` for in-place upgrade paths.
README updated to call out the explicit no-default-account design
and how to bootstrap an admin in local dev (Prisma Studio, since
the StartOS action isn't available off-StartOS).
171 lines
8.2 KiB
Bash
Executable File
171 lines
8.2 KiB
Bash
Executable File
#!/bin/sh
|
|
# Proof of Work (proof-of-work) — StartOS 0.4 container entrypoint.
|
|
#
|
|
# Responsibilities (in order):
|
|
# 1. Ensure the persistent /data directory exists.
|
|
# 2. First-boot fallback ONLY: if /data is empty (no app.db, no
|
|
# .seeded marker) AND a fallback DB exists at
|
|
# /app/prisma/data/app.db, copy it into /data. This handles
|
|
# brand-new sideloads on a host that has never had proof-of-work
|
|
# installed before. Existing installs ALWAYS skip this branch
|
|
# because /data/app.db is already present.
|
|
# (v1.0.0:1 had a second branch that copied a baked cutover seed
|
|
# from /app/seed/data/app.db. v1.0.0:3 stripped both the COPY of
|
|
# that seed in the Dockerfile and the branch that copied it here,
|
|
# now that the cutover from `workout-log` is verified done.)
|
|
# (v1.0.0:4 made another change to this fallback DB: it now
|
|
# contains ONLY the InstanceSettings singleton, NOT a default
|
|
# admin user or seeded exercises. The operator is required to
|
|
# run the StartOS Action 'Set admin credentials' to bootstrap
|
|
# the first admin — eliminates the default-credentials footgun.)
|
|
# 3. Run idempotent compat ALTERs for columns added after older snapshots.
|
|
# No-ops on hosts whose schema is already current.
|
|
# 4. Ensure the curated exercise library is present for every user. New
|
|
# maintainer-shipped exercises appear on every boot (INSERT OR IGNORE
|
|
# keyed on (userId, name); never overwrites a user's own exercises).
|
|
# 5. Exec the Next.js standalone server as PID 1 (under dumb-init).
|
|
#
|
|
# Every branch logs to stderr so it is visible in StartOS -> Logs.
|
|
|
|
set -eu
|
|
|
|
DATA_DIR="${WORKOUT_DATA_DIR:-/data}"
|
|
DB_PATH="${WORKOUT_DB_PATH:-$DATA_DIR/app.db}"
|
|
FALLBACK_SEED_DB_PATH="${WORKOUT_FALLBACK_SEED_DB_PATH:-/app/prisma/data/app.db}"
|
|
LIBRARY_JSON_PATH="${WORKOUT_LIBRARY_JSON_PATH:-/app/prisma/exercises.seed.json}"
|
|
|
|
log() {
|
|
# write to stderr so StartOS log viewer surfaces it immediately
|
|
printf '[entrypoint] %s\n' "$*" 1>&2
|
|
}
|
|
|
|
mkdir -p "$DATA_DIR"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 1 — first-boot fallback. NEVER overwrites an existing app.db.
|
|
# Skipped on every restart of an installed host because /data/app.db
|
|
# already exists.
|
|
# -----------------------------------------------------------------------------
|
|
if [ ! -f "$DB_PATH" ] && [ ! -f "$DATA_DIR/.seeded" ]; then
|
|
if [ -f "$FALLBACK_SEED_DB_PATH" ]; then
|
|
log "no $DB_PATH and no .seeded marker; copying empty-schema fallback from $FALLBACK_SEED_DB_PATH"
|
|
cp "$FALLBACK_SEED_DB_PATH" "$DB_PATH"
|
|
date -u +"seeded from empty-schema fallback at %Y-%m-%dT%H:%M:%SZ" > "$DATA_DIR/.seeded"
|
|
else
|
|
log "no $DB_PATH and no fallback DB; creating empty $DB_PATH"
|
|
touch "$DB_PATH"
|
|
fi
|
|
else
|
|
log "$DB_PATH already present; live data is the source of truth"
|
|
if [ -f "$DATA_DIR/.seeded" ]; then
|
|
log "found .seeded: $(cat "$DATA_DIR/.seeded")"
|
|
fi
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 2 — idempotent compat ALTERs (safety net for older snapshots).
|
|
# No-ops on hosts whose schema is already current.
|
|
# -----------------------------------------------------------------------------
|
|
if command -v sqlite3 >/dev/null 2>&1 && [ -f "$DB_PATH" ]; then
|
|
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('SetLog');" 2>/dev/null | grep -q "|customMetrics|"; then
|
|
log "adding missing column SetLog.customMetrics"
|
|
sqlite3 "$DB_PATH" "ALTER TABLE SetLog ADD COLUMN customMetrics TEXT;"
|
|
fi
|
|
|
|
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('Workout');" 2>/dev/null | grep -q "|deletedAt|"; then
|
|
log "adding missing column Workout.deletedAt"
|
|
sqlite3 "$DB_PATH" "ALTER TABLE Workout ADD COLUMN deletedAt DATETIME;"
|
|
fi
|
|
|
|
# Multi-user support shipped in v1.0.0:1: User.isAdmin column +
|
|
# InstanceSettings singleton. New install: seed.ts creates both. Upgrade
|
|
# from a snapshot pulled off the legacy `workout-log` package: this block
|
|
# adds them in place, then promotes the oldest user to admin so the
|
|
# in-app admin Settings panel + change-credentials action keep working.
|
|
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('User');" 2>/dev/null | grep -q "|isAdmin|"; then
|
|
log "adding missing column User.isAdmin (default 0)"
|
|
sqlite3 "$DB_PATH" "ALTER TABLE User ADD COLUMN isAdmin INTEGER NOT NULL DEFAULT 0;"
|
|
log "promoting oldest user to admin (one-shot, only if no admin exists)"
|
|
sqlite3 "$DB_PATH" \
|
|
"UPDATE User SET isAdmin = 1 \
|
|
WHERE id = (SELECT id FROM User ORDER BY createdAt ASC LIMIT 1) \
|
|
AND NOT EXISTS (SELECT 1 FROM User WHERE isAdmin = 1);"
|
|
fi
|
|
|
|
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('User');" 2>/dev/null | grep -q "|lastLoginAt|"; then
|
|
log "adding missing column User.lastLoginAt (nullable)"
|
|
sqlite3 "$DB_PATH" "ALTER TABLE User ADD COLUMN lastLoginAt DATETIME;"
|
|
fi
|
|
|
|
if ! sqlite3 "$DB_PATH" \
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='InstanceSettings';" \
|
|
2>/dev/null | grep -q InstanceSettings; then
|
|
log "creating InstanceSettings table + singleton row (signupsOpen=0)"
|
|
sqlite3 "$DB_PATH" \
|
|
"CREATE TABLE InstanceSettings ( \
|
|
id INTEGER PRIMARY KEY DEFAULT 1, \
|
|
signupsOpen INTEGER NOT NULL DEFAULT 0, \
|
|
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP \
|
|
);"
|
|
sqlite3 "$DB_PATH" \
|
|
"INSERT OR IGNORE INTO InstanceSettings (id, signupsOpen) VALUES (1, 0);"
|
|
fi
|
|
|
|
# SQLite tuning. Enabling WAL means readers don't block on a concurrent
|
|
# writer (and vice versa) — crucial for the "background StartOS Backup
|
|
# while users are using the app" case, which under the default rollback
|
|
# journal can produce a torn snapshot. journal_mode persists in the DB
|
|
# header once set, so this is effectively a one-shot. synchronous=NORMAL
|
|
# is the safe-with-WAL balance: no fsync after every commit but still
|
|
# crash-consistent at every checkpoint, ~10x faster than FULL.
|
|
current_mode=$(sqlite3 "$DB_PATH" "PRAGMA journal_mode;" 2>/dev/null || echo "")
|
|
if [ "$current_mode" != "wal" ]; then
|
|
log "switching SQLite journal_mode from '${current_mode:-unknown}' to WAL"
|
|
sqlite3 "$DB_PATH" "PRAGMA journal_mode=WAL;" >/dev/null
|
|
fi
|
|
sqlite3 "$DB_PATH" "PRAGMA synchronous=NORMAL;" >/dev/null
|
|
|
|
# Composite indexes for hot query paths. CREATE INDEX IF NOT EXISTS is
|
|
# idempotent — no-op if Prisma already created them via db push on a
|
|
# fresh install. These are also declared in schema.prisma so any future
|
|
# `prisma db push` keeps them in sync.
|
|
log "ensuring composite indexes are present"
|
|
sqlite3 "$DB_PATH" "
|
|
CREATE INDEX IF NOT EXISTS Session_userId_expiresAt_idx
|
|
ON Session(userId, expiresAt);
|
|
CREATE INDEX IF NOT EXISTS Workout_userId_deletedAt_date_idx
|
|
ON Workout(userId, deletedAt, date);
|
|
CREATE INDEX IF NOT EXISTS SetLog_workoutId_setNumber_idx
|
|
ON SetLog(workoutId, setNumber);
|
|
" >/dev/null
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 3 — ensure curated exercise library for every user (multi-user-aware).
|
|
# New entries shipped in /app/prisma/exercises.seed.json appear on every boot.
|
|
# `INSERT OR IGNORE` keyed on (userId, name) so we never overwrite a user's
|
|
# own custom exercises. Designed to be additive only — exercises removed from
|
|
# the curated JSON are not deleted from existing installs (users may have
|
|
# logged sets against them).
|
|
# -----------------------------------------------------------------------------
|
|
if [ -f "$LIBRARY_JSON_PATH" ] && [ -f "$DB_PATH" ]; then
|
|
log "ensuring curated exercise library is present for every user"
|
|
node /app/prisma/ensureExerciseLibrary.cjs \
|
|
--db "$DB_PATH" \
|
|
--json "$LIBRARY_JSON_PATH" \
|
|
|| log "WARNING: ensureExerciseLibrary failed; continuing boot"
|
|
else
|
|
log "skipping library ensure (json or db not found)"
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 4 — launch the app.
|
|
# -----------------------------------------------------------------------------
|
|
export DATABASE_URL="file:$DB_PATH"
|
|
export NODE_ENV="${NODE_ENV:-production}"
|
|
export HOSTNAME="${HOSTNAME:-0.0.0.0}"
|
|
export PORT="${PORT:-3000}"
|
|
|
|
log "launching Next.js on :${PORT} with DATABASE_URL=file:${DB_PATH}"
|
|
exec node /app/server.js
|