update outdated self-hosting shit

This commit is contained in:
Samidy 2026-03-12 02:03:36 +03:00
parent ffdcc1d396
commit 4b7833dc8e
9 changed files with 131 additions and 101 deletions

View file

@ -9,12 +9,11 @@ MONOCHROME_DEV_PORT=5173
# Set AUTH_ENABLED=true to enable the auth gate entirely (login required) # Set AUTH_ENABLED=true to enable the auth gate entirely (login required)
AUTH_ENABLED=false AUTH_ENABLED=false
AUTH_SECRET=change-me-to-a-random-string AUTH_SECRET=change-me-to-a-random-string
FIREBASE_PROJECT_ID=monochrome-database APPWRITE_ENDPOINT=https://auth.yourdomain.com/v1
APPWRITE_PROJECT_ID=auth-for-monochrome
# Optional: toggle login providers (defaults to true when unset) # Optional: toggle login providers (defaults to true when unset)
# AUTH_GOOGLE_ENABLED=true # AUTH_GOOGLE_ENABLED=true
# AUTH_EMAIL_ENABLED=true # AUTH_EMAIL_ENABLED=true
# 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) # Optional: set PocketBase URL (hides the field in settings when set)
# POCKETBASE_URL=https://monodb.samidy.com # POCKETBASE_URL=https://monodb.samidy.com
# SESSION_MAX_AGE=604800000 # 7 days in ms (default) # SESSION_MAX_AGE=604800000 # 7 days in ms (default)

View file

@ -92,6 +92,33 @@ Override files can extend existing services (add labels, env vars, networks) and
--- ---
## Configuration
The application is configured via environment variables. Copy `.env.example` to `.env` and edit it to match your setup.
### Authentication (Appwrite)
Monochrome uses Appwrite for user authentication. While it defaults to official instances, you can use your own self-hosted Appwrite instance:
1. Create a project in Appwrite.
2. Enable the **Google** or **Email/Password** providers in the Appwrite Console.
3. Set these variables in your `.env`:
- `APPWRITE_ENDPOINT`: Your Appwrite API endpoint (e.g., `https://auth.yourdomain.com/v1`).
- `APPWRITE_PROJECT_ID`: Your Appwrite project ID (e.g., `auth-for-monochrome`).
### Database (PocketBase)
Monochrome uses PocketBase to store user data (playlists, favorites, profiles, etc.). You can run it alongside Monochrome using the `pocketbase` profile:
```bash
docker compose --profile pocketbase up -d
```
#### PocketBase Schema Note
If you are setting up a new PocketBase collection for user data, ensure it has a field named `firebase_id` (this is a legacy name we use when we first started the accounts system, we used firebase. and im too lazy to change it so yea fuck you).
---
## Portainer Deployment ## Portainer Deployment
Portainer can deploy directly from your GitHub fork with auto-updates on push. Portainer can deploy directly from your GitHub fork with auto-updates on push.

View file

@ -10,8 +10,8 @@ services:
environment: environment:
AUTH_ENABLED: ${AUTH_ENABLED:-false} AUTH_ENABLED: ${AUTH_ENABLED:-false}
AUTH_SECRET: ${AUTH_SECRET:-} AUTH_SECRET: ${AUTH_SECRET:-}
FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID:-monochrome-database} APPWRITE_ENDPOINT: ${APPWRITE_ENDPOINT:-https://auth.yourdomain.com/v1}
FIREBASE_CONFIG: ${FIREBASE_CONFIG:-} APPWRITE_PROJECT_ID: ${APPWRITE_PROJECT_ID:-auth-for-monochrome}
POCKETBASE_URL: ${POCKETBASE_URL:-} POCKETBASE_URL: ${POCKETBASE_URL:-}
SESSION_MAX_AGE: ${SESSION_MAX_AGE:-604800000} SESSION_MAX_AGE: ${SESSION_MAX_AGE:-604800000}
restart: unless-stopped restart: unless-stopped

View file

@ -1276,11 +1276,11 @@
</button> </button>
</div> </div>
<p style="font-size: 0.9rem; color: var(--muted-foreground); margin-bottom: 1rem"> <p style="font-size: 0.9rem; color: var(--muted-foreground); margin-bottom: 1rem">
Configure custom PocketBase and Firebase instances. Leave empty to use defaults. Configure custom PocketBase and Appwrite instances. Leave empty to use defaults.
<br /> <br />
A Guide To Set This Up Can Be Found A Guide To Set This Up Can Be Found
<a <a
href="https://github.com/monochrome-music/monochrome/blob/main/self-hosted-database.md" href="https://github.com/monochrome-music/monochrome/blob/main/DOCKER.md"
style="text-decoration: underline" style="text-decoration: underline"
>Here</a >Here</a
>. >.
@ -1296,14 +1296,25 @@
</div> </div>
<div style="margin-bottom: 1rem"> <div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem" <label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem"
>Firebase Configuration (JSON)</label >Appwrite Endpoint</label
> >
<textarea <input
id="custom-firebase-config" type="url"
id="custom-appwrite-endpoint"
class="template-input" class="template-input"
style="height: 150px; font-family: monospace; font-size: 0.8rem; resize: vertical" placeholder="https://auth.samidy.com/v1"
placeholder="{'apiKey': '...', ...}" />
></textarea> </div>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem"
>Appwrite Project ID</label
>
<input
type="text"
id="custom-appwrite-project"
class="template-input"
placeholder="auth-for-monochrome"
/>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button id="custom-db-cancel" class="btn-secondary">Cancel</button> <button id="custom-db-cancel" class="btn-secondary">Cancel</button>
@ -5262,7 +5273,7 @@
<div class="info"> <div class="info">
<span class="label">ADVANCED: Custom Database/Auth</span> <span class="label">ADVANCED: Custom Database/Auth</span>
<span class="description" <span class="description"
>Configure custom PocketBase and Firebase instances</span >Configure custom PocketBase and Appwrite instances</span
> >
</div> </div>
<button id="custom-db-btn" class="btn-secondary">Configure</button> <button id="custom-db-btn" class="btn-secondary">Configure</button>
@ -5357,7 +5368,7 @@
>Delete all your data from the cloud (cannot be undone)</span >Delete all your data from the cloud (cannot be undone)</span
> >
</div> </div>
<button id="firebase-clear-cloud-btn" class="btn-secondary danger"> <button id="auth-clear-cloud-btn" class="btn-secondary danger">
Clear Cloud Data Clear Cloud Data
</button> </button>
</div> </div>
@ -5530,19 +5541,19 @@
flex-wrap: wrap; flex-wrap: wrap;
" "
> >
<button id="firebase-connect-btn" class="btn-secondary">Connect with Google</button> <button id="auth-connect-btn" class="btn-secondary">Connect with Google</button>
<button id="toggle-email-auth-btn" class="btn-secondary">Connect with Email</button> <button id="toggle-email-auth-btn" class="btn-secondary">Connect with Email</button>
<button id="view-my-profile-btn" class="btn-secondary" style="display: none"> <button id="view-my-profile-btn" class="btn-secondary" style="display: none">
View My Profile View My Profile
</button> </button>
</div> </div>
<p id="firebase-status" style="text-align: center; padding-top: 15px; color: #8b8b93"> <p id="auth-status" style="text-align: center; padding-top: 15px; color: #8b8b93">
Sync your library across devices Sync your library across devices
</p> </p>
<script> <script>
if (window.authManager && window.authManager.user) { if (window.authManager && window.authManager.user) {
const statusText = document.getElementById('firebase-status'); const statusText = document.getElementById('auth-status');
if (statusText) if (statusText)
statusText.textContent = `Signed in as ${window.authManager.user.email}`; statusText.textContent = `Signed in as ${window.authManager.user.email}`;
} }

View file

@ -117,9 +117,9 @@ export class AuthManager {
} }
updateUI(user) { updateUI(user) {
const connectBtn = document.getElementById('firebase-connect-btn'); const connectBtn = document.getElementById('auth-connect-btn');
const clearDataBtn = document.getElementById('firebase-clear-cloud-btn'); const clearDataBtn = document.getElementById('auth-clear-cloud-btn');
const statusText = document.getElementById('firebase-status'); const statusText = document.getElementById('auth-status');
const emailContainer = document.getElementById('email-auth-container'); const emailContainer = document.getElementById('email-auth-container');
const emailToggleBtn = document.getElementById('toggle-email-auth-btn'); const emailToggleBtn = document.getElementById('toggle-email-auth-btn');
@ -139,7 +139,7 @@ export class AuthManager {
const title = accountPage.querySelector('.section-title'); const title = accountPage.querySelector('.section-title');
if (title) title.textContent = 'Account'; if (title) title.textContent = 'Account';
accountPage.querySelectorAll('.account-content > p, .account-content > div').forEach((el) => { accountPage.querySelectorAll('.account-content > p, .account-content > div').forEach((el) => {
if (el.id !== 'firebase-status' && el.id !== 'auth-buttons-container') { if (el.id !== 'auth-status' && el.id !== 'auth-buttons-container') {
el.style.display = 'none'; el.style.display = 'none';
} }
}); });

View file

@ -1,6 +1,11 @@
import { Client, Account } from 'appwrite'; import { Client, Account } from 'appwrite';
const getEndpoint = () => { const getEndpoint = () => {
const local = localStorage.getItem('monochrome-appwrite-endpoint');
if (local) return local;
if (window.__APPWRITE_ENDPOINT__) return window.__APPWRITE_ENDPOINT__;
const hostname = window.location.hostname; const hostname = window.location.hostname;
if (hostname.endsWith('monochrome.tf') || hostname === 'monochrome.tf') { if (hostname.endsWith('monochrome.tf') || hostname === 'monochrome.tf') {
return 'https://auth.monochrome.tf/v1'; return 'https://auth.monochrome.tf/v1';
@ -8,13 +13,16 @@ const getEndpoint = () => {
return 'https://auth.samidy.com/v1'; return 'https://auth.samidy.com/v1';
}; };
const client = new Client().setEndpoint(getEndpoint()).setProject('auth-for-monochrome'); const getProject = () => {
const local = localStorage.getItem('monochrome-appwrite-project');
if (local) return local;
if (window.__APPWRITE_PROJECT_ID__) return window.__APPWRITE_PROJECT_ID__;
return 'auth-for-monochrome';
};
const client = new Client().setEndpoint(getEndpoint()).setProject(getProject());
const account = new Account(client); const account = new Account(client);
export { client, account as auth }; export { client, account as auth };
export const saveFirebaseConfig = () => {
console.log('ill fix this tomorrow');
};
export const clearFirebaseConfig = () => {
console.log('ill fix this tomorrow');
};

View file

@ -41,7 +41,6 @@ import { getButterchurnPresets } from './visualizers/butterchurn.js';
import { db } from './db.js'; import { db } from './db.js';
import { authManager } from './accounts/auth.js'; import { authManager } from './accounts/auth.js';
import { syncManager } from './accounts/pocketbase.js'; import { syncManager } from './accounts/pocketbase.js';
import { saveFirebaseConfig, clearFirebaseConfig } from './accounts/config.js';
export function initializeSettings(scrobbler, player, api, ui) { export function initializeSettings(scrobbler, player, api, ui) {
// Restore last active settings tab // Restore last active settings tab
@ -2746,7 +2745,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
} }
}); });
document.getElementById('firebase-clear-cloud-btn')?.addEventListener('click', async () => { document.getElementById('auth-clear-cloud-btn')?.addEventListener('click', async () => {
if (confirm('Are you sure you want to delete ALL your data from the cloud? This cannot be undone.')) { if (confirm('Are you sure you want to delete ALL your data from the cloud? This cannot be undone.')) {
try { try {
await syncManager.clearCloudData(); await syncManager.clearCloudData();
@ -2853,41 +2852,40 @@ export function initializeSettings(scrobbler, player, api, ui) {
const customDbBtn = document.getElementById('custom-db-btn'); const customDbBtn = document.getElementById('custom-db-btn');
const customDbModal = document.getElementById('custom-db-modal'); const customDbModal = document.getElementById('custom-db-modal');
const customPbUrlInput = document.getElementById('custom-pb-url'); const customPbUrlInput = document.getElementById('custom-pb-url');
const customFirebaseConfigInput = document.getElementById('custom-firebase-config'); const customAppwriteEndpointInput = document.getElementById('custom-appwrite-endpoint');
const customAppwriteProjectInput = document.getElementById('custom-appwrite-project');
const customDbSaveBtn = document.getElementById('custom-db-save'); const customDbSaveBtn = document.getElementById('custom-db-save');
const customDbResetBtn = document.getElementById('custom-db-reset'); const customDbResetBtn = document.getElementById('custom-db-reset');
const customDbCancelBtn = document.getElementById('custom-db-cancel'); const customDbCancelBtn = document.getElementById('custom-db-cancel');
if (customDbBtn && customDbModal) { if (customDbBtn && customDbModal) {
const fbFromEnv = !!window.__FIREBASE_CONFIG__; const appwriteFromEnv = !!(window.__APPWRITE_ENDPOINT__ || window.__APPWRITE_PROJECT_ID__);
const pbFromEnv = !!window.__POCKETBASE_URL__; const pbFromEnv = !!window.__POCKETBASE_URL__;
// Hide entire setting if both are server-configured // Hide entire setting if both are server-configured
if (fbFromEnv && pbFromEnv) { if (appwriteFromEnv && pbFromEnv) {
const settingItem = customDbBtn.closest('.setting-item'); const settingItem = customDbBtn.closest('.setting-item');
if (settingItem) settingItem.style.display = 'none'; if (settingItem) settingItem.style.display = 'none';
} }
// Hide individual fields in the modal // Hide individual fields in the modal
if (pbFromEnv && customPbUrlInput) customPbUrlInput.closest('div[style]').style.display = 'none'; if (pbFromEnv && customPbUrlInput) customPbUrlInput.closest('div[style]').style.display = 'none';
if (fbFromEnv && customFirebaseConfigInput) if (appwriteFromEnv) {
customFirebaseConfigInput.closest('div[style]').style.display = 'none'; if (customAppwriteEndpointInput)
customAppwriteEndpointInput.closest('div[style]').style.display = 'none';
if (customAppwriteProjectInput)
customAppwriteProjectInput.closest('div[style]').style.display = 'none';
}
customDbBtn.addEventListener('click', () => { customDbBtn.addEventListener('click', () => {
const pbUrl = localStorage.getItem('monochrome-pocketbase-url') || ''; const pbUrl = localStorage.getItem('monochrome-pocketbase-url') || '';
const fbConfig = localStorage.getItem('monochrome-firebase-config'); const appwriteEndpoint = localStorage.getItem('monochrome-appwrite-endpoint') || '';
const appwriteProject = localStorage.getItem('monochrome-appwrite-project') || '';
if (!pbFromEnv) customPbUrlInput.value = pbUrl; if (!pbFromEnv && customPbUrlInput) customPbUrlInput.value = pbUrl;
if (!fbFromEnv) { if (!appwriteFromEnv) {
if (fbConfig) { if (customAppwriteEndpointInput) customAppwriteEndpointInput.value = appwriteEndpoint;
try { if (customAppwriteProjectInput) customAppwriteProjectInput.value = appwriteProject;
customFirebaseConfigInput.value = JSON.stringify(JSON.parse(fbConfig), null, 2);
} catch {
customFirebaseConfigInput.value = fbConfig;
}
} else {
customFirebaseConfigInput.value = '';
}
} }
customDbModal.classList.add('active'); customDbModal.classList.add('active');
@ -2901,25 +2899,30 @@ export function initializeSettings(scrobbler, player, api, ui) {
customDbModal.querySelector('.modal-overlay').addEventListener('click', closeCustomDbModal); customDbModal.querySelector('.modal-overlay').addEventListener('click', closeCustomDbModal);
customDbSaveBtn.addEventListener('click', () => { customDbSaveBtn.addEventListener('click', () => {
const pbUrl = customPbUrlInput.value.trim(); if (!pbFromEnv && customPbUrlInput) {
const fbConfigStr = customFirebaseConfigInput.value.trim(); const pbUrl = customPbUrlInput.value.trim();
if (pbUrl) {
if (pbUrl) { localStorage.setItem('monochrome-pocketbase-url', pbUrl);
localStorage.setItem('monochrome-pocketbase-url', pbUrl); } else {
} else { localStorage.removeItem('monochrome-pocketbase-url');
localStorage.removeItem('monochrome-pocketbase-url'); }
} }
if (fbConfigStr) { if (!appwriteFromEnv) {
try { const endpoint = customAppwriteEndpointInput?.value.trim();
const fbConfig = JSON.parse(fbConfigStr); const project = customAppwriteProjectInput?.value.trim();
saveFirebaseConfig(fbConfig);
} catch { if (endpoint) {
alert('Invalid JSON for Firebase Config'); localStorage.setItem('monochrome-appwrite-endpoint', endpoint);
return; } else {
localStorage.removeItem('monochrome-appwrite-endpoint');
}
if (project) {
localStorage.setItem('monochrome-appwrite-project', project);
} else {
localStorage.removeItem('monochrome-appwrite-project');
} }
} else {
clearFirebaseConfig();
} }
alert('Settings saved. Reloading...'); alert('Settings saved. Reloading...');
@ -2929,7 +2932,8 @@ export function initializeSettings(scrobbler, player, api, ui) {
customDbResetBtn.addEventListener('click', () => { customDbResetBtn.addEventListener('click', () => {
if (confirm('Reset custom database settings to default?')) { if (confirm('Reset custom database settings to default?')) {
localStorage.removeItem('monochrome-pocketbase-url'); localStorage.removeItem('monochrome-pocketbase-url');
clearFirebaseConfig(); localStorage.removeItem('monochrome-appwrite-endpoint');
localStorage.removeItem('monochrome-appwrite-project');
alert('Settings reset. Reloading...'); alert('Settings reset. Reloading...');
window.location.reload(); window.location.reload();
} }

View file

@ -5349,8 +5349,7 @@ img[src=''] {
filter: brightness(1.1); filter: brightness(1.1);
} }
/* Firebase Settings Styling */ .appwrite-settings-wrapper {
.firebase-settings-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
@ -5358,7 +5357,7 @@ img[src=''] {
max-width: 400px; max-width: 400px;
} }
.custom-firebase-config { .custom-appwrite-endpoint, .custom-appwrite-project {
display: none; display: none;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
@ -5366,7 +5365,7 @@ img[src=''] {
width: 100%; width: 100%;
} }
.custom-firebase-config.visible { .custom-appwrite-endpoint.visible, .custom-appwrite-project.visible {
display: flex; display: flex;
} }
@ -5376,13 +5375,13 @@ img[src=''] {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.firebase-controls-container { .appwrite-controls-container {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
width: 100%; width: 100%;
} }
#toggle-firebase-config-btn { #toggle-auth-config-btn {
width: fit-content; width: fit-content;
} }

View file

@ -31,24 +31,12 @@ export default function authGatePlugin() {
configurePreviewServer(server) { configurePreviewServer(server) {
const AUTH_ENABLED = (env.AUTH_ENABLED ?? 'false') !== 'false'; const AUTH_ENABLED = (env.AUTH_ENABLED ?? 'false') !== 'false';
const FIREBASE_CONFIG = env.FIREBASE_CONFIG; const APPWRITE_ENDPOINT = env.APPWRITE_ENDPOINT;
const APPWRITE_PROJECT_ID = env.APPWRITE_PROJECT_ID;
const POCKETBASE_URL = env.POCKETBASE_URL; const POCKETBASE_URL = env.POCKETBASE_URL;
const AUTH_GOOGLE_ENABLED = env.AUTH_GOOGLE_ENABLED; const AUTH_GOOGLE_ENABLED = env.AUTH_GOOGLE_ENABLED;
const AUTH_EMAIL_ENABLED = env.AUTH_EMAIL_ENABLED; const AUTH_EMAIL_ENABLED = env.AUTH_EMAIL_ENABLED;
// Parse Firebase config once (used for injection + auth verification)
let parsedFirebaseConfig = null;
let PROJECT_ID = env.FIREBASE_PROJECT_ID || 'monochrome-database';
if (FIREBASE_CONFIG) {
try {
parsedFirebaseConfig = JSON.parse(FIREBASE_CONFIG);
if (parsedFirebaseConfig.projectId) PROJECT_ID = parsedFirebaseConfig.projectId;
} catch (e) {
console.error('Invalid FIREBASE_CONFIG JSON:', e.message);
process.exit(1);
}
}
// --- Build injection script (always, for both auth gate and env config) --- // --- Build injection script (always, for both auth gate and env config) ---
const flags = []; const flags = [];
@ -58,13 +46,14 @@ export default function authGatePlugin() {
authProviderOverrides.google = AUTH_GOOGLE_ENABLED !== 'false'; authProviderOverrides.google = AUTH_GOOGLE_ENABLED !== 'false';
} }
if (AUTH_EMAIL_ENABLED !== undefined) { if (AUTH_EMAIL_ENABLED !== undefined) {
// Firebase calls it "password" provider; env uses "EMAIL" for clarity
authProviderOverrides.password = AUTH_EMAIL_ENABLED !== 'false'; authProviderOverrides.password = AUTH_EMAIL_ENABLED !== 'false';
} }
if (Object.keys(authProviderOverrides).length > 0) { if (Object.keys(authProviderOverrides).length > 0) {
flags.push(`window.__AUTH_PROVIDERS__=${JSON.stringify(authProviderOverrides)}`); flags.push(`window.__AUTH_PROVIDERS__=${JSON.stringify(authProviderOverrides)}`);
} }
if (parsedFirebaseConfig) flags.push(`window.__FIREBASE_CONFIG__=${JSON.stringify(parsedFirebaseConfig)}`); if (APPWRITE_ENDPOINT) flags.push(`window.__APPWRITE_ENDPOINT__=${JSON.stringify(APPWRITE_ENDPOINT)}`);
if (APPWRITE_PROJECT_ID)
flags.push(`window.__APPWRITE_PROJECT_ID__=${JSON.stringify(APPWRITE_PROJECT_ID)}`);
if (POCKETBASE_URL) flags.push(`window.__POCKETBASE_URL__=${JSON.stringify(POCKETBASE_URL)}`); if (POCKETBASE_URL) flags.push(`window.__POCKETBASE_URL__=${JSON.stringify(POCKETBASE_URL)}`);
const configScript = flags.length > 0 ? `<script>${flags.join(';')};</script>` : null; const configScript = flags.length > 0 ? `<script>${flags.join(';')};</script>` : null;
@ -109,11 +98,8 @@ export default function authGatePlugin() {
process.exit(1); process.exit(1);
} }
console.log(`Auth gate enabled (Firebase project: ${PROJECT_ID})`); console.log(`Auth gate enabled (Project: ${APPWRITE_PROJECT_ID})`);
const JWKS = createRemoteJWKSet(
new URL('https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com')
);
server.middlewares.use( server.middlewares.use(
cookieSession({ cookieSession({
@ -148,26 +134,22 @@ export default function authGatePlugin() {
if (url === '/api/auth/login' && req.method === 'POST') { if (url === '/api/auth/login' && req.method === 'POST') {
try { try {
const body = await parseBody(req); const body = await parseBody(req);
if (!body.token) { if (!body.userId) {
res.statusCode = 400; res.statusCode = 400;
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Missing token' })); res.end(JSON.stringify({ error: 'Missing userId' }));
return; return;
} }
const { payload } = await jwtVerify(body.token, JWKS, { req.session.uid = body.userId;
issuer: `https://securetoken.google.com/${PROJECT_ID}`, req.session.email = body.email;
audience: PROJECT_ID,
});
req.session.uid = payload.sub;
req.session.email = payload.email;
req.session.iat = Date.now(); req.session.iat = Date.now();
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ok: true })); res.end(JSON.stringify({ ok: true }));
} catch (err) { } catch (err) {
console.error('Token verification failed:', err.message); console.error('Login session creation failed:', err.message);
res.statusCode = 401; res.statusCode = 401;
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Invalid token' })); res.end(JSON.stringify({ error: 'Login failed' }));
} }
return; return;
} }