+ Permanently deletes your account, every workout you've logged,
+ every set, your custom exercises, and every session. Other users
+ on this instance are not affected.
+
+
+
+ {!open ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/proof-of-work/lib/auth.ts b/proof-of-work/lib/auth.ts
index 069ded0..f65ea1b 100644
--- a/proof-of-work/lib/auth.ts
+++ b/proof-of-work/lib/auth.ts
@@ -37,13 +37,22 @@ export async function createSession(
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
- await prisma.session.create({
- data: {
- token,
- userId,
- expiresAt,
- },
- });
+ // Create the session and stamp the user's lastLoginAt in the same
+ // transaction. Surfaced in the admin Users table so admins can spot
+ // dormant accounts.
+ await prisma.$transaction([
+ prisma.session.create({
+ data: {
+ token,
+ userId,
+ expiresAt,
+ },
+ }),
+ prisma.user.update({
+ where: { id: userId },
+ data: { lastLoginAt: new Date() },
+ }),
+ ]);
return { token, expiresAt };
}
diff --git a/proof-of-work/next.config.js b/proof-of-work/next.config.js
index 7dab846..b65a613 100644
--- a/proof-of-work/next.config.js
+++ b/proof-of-work/next.config.js
@@ -1,31 +1,66 @@
/** @type {import('next').NextConfig} */
+
+// Content-Security-Policy.
+//
+// `script-src` and `style-src` keep `'unsafe-inline'` because Next.js
+// emits inline bootstrap scripts and Tailwind's runtime CSS-in-JS path
+// requires inline styles. Tightening to nonce-based CSP is a follow-up
+// (requires switching to Next's `headers()` middleware-style nonce
+// injection, not the static config). The directives we DO get for free
+// here still cut off the most common XSS-followup patterns:
+// - frame-ancestors 'none' -> can't be embedded anywhere (clickjacking)
+// - base-uri 'self' -> attacker can't pivot relative URLs
+// - form-action 'self' -> stolen forms can't POST credentials away
+// - object-src 'none' -> no Flash/Java applets, full stop
+// - default-src 'self' -> images/fetches/etc default to same-origin
+const csp = [
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
+ "style-src 'self' 'unsafe-inline'",
+ "img-src 'self' data: blob:",
+ "font-src 'self' data:",
+ "connect-src 'self'",
+ "frame-ancestors 'none'",
+ "base-uri 'self'",
+ "form-action 'self'",
+ "object-src 'none'",
+].join('; ');
+
+const securityHeaders = [
+ { key: 'Content-Security-Policy', value: csp },
+ // HSTS: tell browsers to use HTTPS only for this origin for a year.
+ // StartOS terminates TLS in front of the container, so this applies
+ // to the public hostname users actually visit.
+ {
+ key: 'Strict-Transport-Security',
+ value: 'max-age=31536000; includeSubDomains',
+ },
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
+ { key: 'X-Frame-Options', value: 'DENY' },
+ // Don't leak the full URL (which can include exercise IDs, workout
+ // IDs, etc.) when the user clicks a third-party link.
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
+ // Block every browser API we don't actually use. If we ever add
+ // camera/mic/geo features, allow-list them here explicitly.
+ {
+ key: 'Permissions-Policy',
+ value:
+ 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
+ },
+];
+
const nextConfig = {
reactStrictMode: true,
output: 'standalone',
images: {
unoptimized: false,
},
- headers: async () => {
- return [
- {
- source: '/(.*)',
- headers: [
- {
- key: 'X-Content-Type-Options',
- value: 'nosniff',
- },
- {
- key: 'X-Frame-Options',
- value: 'DENY',
- },
- {
- key: 'X-XSS-Protection',
- value: '1; mode=block',
- },
- ],
- },
- ];
- },
+ headers: async () => [
+ {
+ source: '/(.*)',
+ headers: securityHeaders,
+ },
+ ],
};
module.exports = nextConfig;
diff --git a/proof-of-work/prisma/schema.prisma b/proof-of-work/prisma/schema.prisma
index 6f4af32..62954ff 100644
--- a/proof-of-work/prisma/schema.prisma
+++ b/proof-of-work/prisma/schema.prisma
@@ -16,6 +16,7 @@ model User {
passwordHash String
name String?
isAdmin Boolean @default(false)
+ lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
diff --git a/start9/0.4/docker_entrypoint.sh b/start9/0.4/docker_entrypoint.sh
index de55e4f..1ca2ec1 100755
--- a/start9/0.4/docker_entrypoint.sh
+++ b/start9/0.4/docker_entrypoint.sh
@@ -91,6 +91,11 @@ if command -v sqlite3 >/dev/null 2>&1 && [ -f "$DB_PATH" ]; then
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
@@ -104,6 +109,20 @@ if command -v sqlite3 >/dev/null 2>&1 && [ -f "$DB_PATH" ]; then
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
fi
# -----------------------------------------------------------------------------
diff --git a/start9/0.4/package-lock.json b/start9/0.4/package-lock.json
index d6ef741..7148752 100644
--- a/start9/0.4/package-lock.json
+++ b/start9/0.4/package-lock.json
@@ -1,10 +1,10 @@
{
- "name": "workout-log-startos",
+ "name": "proof-of-work-startos",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "workout-log-startos",
+ "name": "proof-of-work-startos",
"dependencies": {
"@start9labs/start-sdk": "1.0.0",
"bcryptjs": "^2.4.3"