diff --git a/.env.example b/.env.example index 9dddc97..e3b7c89 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,17 @@ MONOCHROME_PORT=3000 MONOCHROME_DEV_PORT=5173 +# --- Auth Gate (server-side authentication) --- +# Set AUTH_ENABLED=false to disable the auth gate entirely (no login required) +AUTH_ENABLED=true +AUTH_SECRET=change-me-to-a-random-string +FIREBASE_PROJECT_ID=monochrome-database +# Optional: override the Firebase config for the login page (JSON string) +# FIREBASE_CONFIG={"apiKey":"...","authDomain":"...","projectId":"...","storageBucket":"...","messagingSenderId":"...","appId":"..."} +# Optional: set PocketBase URL (hides the field in settings when set) +# POCKETBASE_URL=https://monodb.samidy.com +# SESSION_MAX_AGE=604800000 # 7 days in ms (default) + # --- PocketBase (only used with --profile pocketbase) --- POCKETBASE_PORT=8090 PB_ADMIN_EMAIL=admin@example.com diff --git a/Dockerfile b/Dockerfile index 2f9995d..16b7d76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app RUN apk add --no-cache wget # Copy package files first for caching -COPY package.json ./ +COPY package.json package-lock.json ./ # Install dependencies RUN npm install diff --git a/docker-compose.yml b/docker-compose.yml index 3c39b58..5da3e6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,11 +7,18 @@ services: container_name: monochrome ports: - '${MONOCHROME_PORT:-3000}:4173' + environment: + AUTH_ENABLED: ${AUTH_ENABLED:-true} + AUTH_SECRET: ${AUTH_SECRET:-} + FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID:-monochrome-database} + FIREBASE_CONFIG: ${FIREBASE_CONFIG:-} + POCKETBASE_URL: ${POCKETBASE_URL:-} + SESSION_MAX_AGE: ${SESSION_MAX_AGE:-604800000} restart: unless-stopped networks: - monochrome-network healthcheck: - test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:4173/'] + test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:4173/health'] interval: 30s timeout: 3s retries: 3 diff --git a/js/accounts/auth.js b/js/accounts/auth.js index 865e31d..fceffde 100644 --- a/js/accounts/auth.js +++ b/js/accounts/auth.js @@ -103,7 +103,14 @@ export class AuthManager { try { await firebaseSignOut(auth); - // The onAuthStateChanged listener will handle the rest + if (window.__AUTH_GATE__) { + try { + await fetch('/api/auth/logout', { method: 'POST' }); + } catch (_) { + // Server endpoint may not exist in dev mode + } + window.location.href = '/login'; + } } catch (error) { console.error('Logout failed:', error); throw error; @@ -119,6 +126,39 @@ export class AuthManager { if (!connectBtn) return; // UI might not be rendered yet + // Auth gate active: strip down to status + sign out only + if (window.__AUTH_GATE__) { + connectBtn.textContent = 'Sign Out'; + connectBtn.classList.add('danger'); + connectBtn.onclick = () => this.signOut(); + if (clearDataBtn) clearDataBtn.style.display = 'none'; + if (emailContainer) emailContainer.style.display = 'none'; + if (emailToggleBtn) emailToggleBtn.style.display = 'none'; + if (statusText) statusText.textContent = user ? `Signed in as ${user.email}` : 'Signed in'; + + // Account page: clean up unnecessary text + const accountPage = document.getElementById('page-account'); + if (accountPage) { + const title = accountPage.querySelector('.section-title'); + if (title) title.textContent = 'Account'; + // Hide description + privacy paragraphs, keep only status + accountPage.querySelectorAll('.account-content > p, .account-content > div').forEach((el) => { + if (el.id !== 'firebase-status' && el.id !== 'auth-buttons-container') { + el.style.display = 'none'; + } + }); + } + + // Settings page: hide custom DB/Auth config + const customDbBtn = document.getElementById('custom-db-btn'); + if (customDbBtn) { + const settingItem = customDbBtn.closest('.setting-item'); + if (settingItem) settingItem.style.display = 'none'; + } + + return; + } + if (user) { connectBtn.textContent = 'Sign Out'; connectBtn.classList.add('danger'); diff --git a/js/accounts/config.js b/js/accounts/config.js index a0b6aaa..a7956da 100644 --- a/js/accounts/config.js +++ b/js/accounts/config.js @@ -30,8 +30,9 @@ function getStoredConfig() { } // Attempt to initialize on load +// Priority: server-injected env (auth gate) > localStorage > default const storedConfig = getStoredConfig(); -const config = storedConfig || DEFAULT_CONFIG; +const config = window.__FIREBASE_CONFIG__ || storedConfig || DEFAULT_CONFIG; if (config) { try { diff --git a/js/settings.js b/js/settings.js index 210673f..4507075 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1606,19 +1606,34 @@ export function initializeSettings(scrobbler, player, api, ui) { const customDbCancelBtn = document.getElementById('custom-db-cancel'); if (customDbBtn && customDbModal) { + const fbFromEnv = !!window.__FIREBASE_CONFIG__; + const pbFromEnv = !!window.__POCKETBASE_URL__; + + // Hide entire setting if both are server-configured + if (fbFromEnv && pbFromEnv) { + const settingItem = customDbBtn.closest('.setting-item'); + if (settingItem) settingItem.style.display = 'none'; + } + + // Hide individual fields in the modal + if (pbFromEnv && customPbUrlInput) customPbUrlInput.closest('div[style]').style.display = 'none'; + if (fbFromEnv && customFirebaseConfigInput) customFirebaseConfigInput.closest('div[style]').style.display = 'none'; + customDbBtn.addEventListener('click', () => { const pbUrl = localStorage.getItem('monochrome-pocketbase-url') || ''; const fbConfig = localStorage.getItem('monochrome-firebase-config'); - customPbUrlInput.value = pbUrl; - if (fbConfig) { - try { - customFirebaseConfigInput.value = JSON.stringify(JSON.parse(fbConfig), null, 2); - } catch { - customFirebaseConfigInput.value = fbConfig; + if (!pbFromEnv) customPbUrlInput.value = pbUrl; + if (!fbFromEnv) { + if (fbConfig) { + try { + customFirebaseConfigInput.value = JSON.stringify(JSON.parse(fbConfig), null, 2); + } catch { + customFirebaseConfigInput.value = fbConfig; + } + } else { + customFirebaseConfigInput.value = ''; } - } else { - customFirebaseConfigInput.value = ''; } customDbModal.classList.add('active'); diff --git a/package-lock.json b/package-lock.json index 6e376e8..5534c1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "dependencies": { "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", + "cookie-session": "^2.1.0", "dashjs": "^5.1.1", + "jose": "^6.0.11", "pocketbase": "^0.26.5" }, "devDependencies": { @@ -2467,9 +2469,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3751,6 +3753,43 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie-session": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.1.tgz", + "integrity": "sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==", + "license": "MIT", + "dependencies": { + "cookies": "0.9.1", + "debug": "3.2.7", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cookie-session/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", @@ -4011,6 +4050,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -5970,6 +6018,15 @@ "node": ">=10" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6074,6 +6131,18 @@ "node": ">=0.10.0" } }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6329,7 +6398,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -6448,6 +6516,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7139,7 +7216,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -8176,6 +8252,15 @@ "node": ">=8.0" } }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index d358f9d..705726f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", + "start": "vite preview", "lint:js": "eslint .", "lint:css": "stylelint \"**/*.css\"", "lint:html": "htmlhint \"**/*.html\" --ignore=\"dist/**,legacy/**,node_modules/**\"", @@ -43,6 +44,8 @@ "source-map": "^0.7.4" }, "dependencies": { + "cookie-session": "^2.1.0", + "jose": "^6.0.11", "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", "dashjs": "^5.1.1", diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..f318465 --- /dev/null +++ b/public/login.html @@ -0,0 +1,290 @@ + + +
+ + +