const http = require("http"); const crypto = require("crypto"); const fs = require("fs"); const PORT = 3000; const env = { APP_URL: process.env.APP_URL || "http://localhost:3000", ZITADEL_URL: process.env.ZITADEL_URL || "http://host.docker.internal:8080", ZITADEL_PUBLIC_URL: process.env.ZITADEL_PUBLIC_URL || "http://localhost:8080", ZITADEL_PROJECT_NAME: process.env.ZITADEL_PROJECT_NAME || "Million Miles", OIDC_APP_NAME: process.env.OIDC_APP_NAME || "Million Miles Custom Frontend", OIDC_REDIRECT_URI: process.env.OIDC_REDIRECT_URI || "http://localhost:3000/auth/callback", OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID || "", OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET || "", OIDC_CLIENT_STATE_FILE: process.env.OIDC_CLIENT_STATE_FILE || "/zitadel-secrets/oidc-client.json", ZITADEL_SERVICE_TOKEN_FILE: process.env.ZITADEL_SERVICE_TOKEN_FILE || "/zitadel-secrets/admin.pat", ZITADEL_LOGIN_CLIENT_TOKEN_FILE: process.env.ZITADEL_LOGIN_CLIENT_TOKEN_FILE || "/zitadel-secrets/login-client.pat", SESSION_SECRET: process.env.SESSION_SECRET || "dev-secret", AUTH_DEBUG: process.env.AUTH_DEBUG === "true" }; const COOKIE_SECURE = env.APP_URL.startsWith("https://") || process.env.NODE_ENV === "production"; const PASSWORD_POLICY = { minLength: "8", hasUppercase: true, hasLowercase: true, hasNumber: true, hasSymbol: false }; const persistedOidcClient = readOidcClientConfig(); const state = { projectId: null, clientId: env.OIDC_CLIENT_ID || persistedOidcClient.clientId, clientSecret: env.OIDC_CLIENT_SECRET || persistedOidcClient.clientSecret, setupTried: false, configSyncTried: false, jwks: null, jwksFetchedAt: 0 }; function debug(...args) { if (env.AUTH_DEBUG) console.log("[zitadel-debug]", ...args); } function readTokenFile(path) { try { const value = fs.readFileSync(path, "utf8").trim(); return value || null; } catch { return null; } } function readOidcClientConfig() { try { const raw = fs.readFileSync(env.OIDC_CLIENT_STATE_FILE, "utf8"); const parsed = JSON.parse(raw); return { clientId: parsed.clientId || "", clientSecret: parsed.clientSecret || "" }; } catch { return { clientId: "", clientSecret: "" }; } } function persistOidcClientConfig(clientId, clientSecret) { try { fs.writeFileSync(env.OIDC_CLIENT_STATE_FILE, JSON.stringify({ clientId, clientSecret }), "utf8"); } catch (error) { debug("failed to persist OIDC client", error.message); } } function adminToken() { return readTokenFile(env.ZITADEL_SERVICE_TOKEN_FILE); } function loginClientToken() { return readTokenFile(env.ZITADEL_LOGIN_CLIENT_TOKEN_FILE) || adminToken(); } function stripTrailingSlash(value) { return String(value || "").replace(/\/+$/, ""); } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[char])); } function validatePassword(password) { if (password.length < 8) return "Пароль должен быть не короче 8 символов."; if (!/[A-Z]/.test(password)) return "Пароль должен содержать хотя бы одну заглавную букву."; if (!/[a-z]/.test(password)) return "Пароль должен содержать хотя бы одну строчную букву."; if (!/\d/.test(password)) return "Пароль должен содержать хотя бы одну цифру."; return null; } function b64url(input) { return Buffer.from(input).toString("base64url"); } function sign(value) { return crypto.createHmac("sha256", env.SESSION_SECRET).update(value).digest("base64url"); } function signaturesMatch(left, right) { const a = Buffer.from(String(left || "")); const b = Buffer.from(String(right || "")); return a.length === b.length && crypto.timingSafeEqual(a, b); } function makeSigned(value) { const payload = b64url(JSON.stringify(value)); return `${payload}.${sign(payload)}`; } function readSigned(value) { if (!value || !value.includes(".")) return null; const [payload, mac] = value.split("."); if (!signaturesMatch(sign(payload), mac)) return null; try { return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")); } catch { return null; } } function makeCookie(name, value, maxAge = 3600) { const secure = COOKIE_SECURE ? "; Secure" : ""; return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${secure}`; } function clearCookie(name) { const secure = COOKIE_SECURE ? "; Secure" : ""; return `${name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`; } function parseCookies(req) { const header = req.headers.cookie || ""; return Object.fromEntries( header .split(";") .map(value => value.trim()) .filter(Boolean) .map(value => { const index = value.indexOf("="); if (index < 0) return [value, ""]; return [value.slice(0, index), value.slice(index + 1)]; }) ); } function getSignedCookie(req, name) { return readSigned(parseCookies(req)[name]); } function getSession(req) { return getSignedCookie(req, "mm_session"); } function getPendingAuth(req) { return getSignedCookie(req, "mm_auth"); } function getFlash(req) { return parseCookies(req).mm_flash || ""; } async function parseBody(req) { let data = ""; for await (const chunk of req) data += chunk; return Object.fromEntries(new URLSearchParams(data)); } function decodeJwtPart(token, index) { const parts = String(token || "").split("."); if (parts.length !== 3) throw new Error("Некорректный JWT."); return JSON.parse(Buffer.from(parts[index], "base64url").toString("utf8")); } async function zFetch(path, options = {}, token = adminToken()) { if (!token) throw new Error("ZITADEL PAT еще не создан. Подождите первый запуск ZITADEL."); const res = await fetch(`${env.ZITADEL_URL}${path}`, { ...options, headers: { Accept: "application/json", Authorization: `Bearer ${token}`, ...(options.headers || {}) } }); const text = await res.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; } if (!res.ok) { debug("zFetch failed", path, res.status, data); const message = data.message || data.raw || `ZITADEL API error ${res.status}`; throw new Error(message); } return data; } async function waitForZitadel() { try { const res = await fetch(`${env.ZITADEL_URL}/debug/ready`); return res.ok; } catch { return false; } } async function ensurePasswordPolicy(token) { const currentResponse = await zFetch("/management/v1/policies/password/complexity", {}, token); const current = currentResponse.policy || {}; const alreadyMatches = String(current.minLength || "") === PASSWORD_POLICY.minLength && current.hasUppercase === PASSWORD_POLICY.hasUppercase && current.hasLowercase === PASSWORD_POLICY.hasLowercase && current.hasNumber === PASSWORD_POLICY.hasNumber && Boolean(current.hasSymbol) === PASSWORD_POLICY.hasSymbol; if (alreadyMatches) return; const method = currentResponse.isDefault ? "POST" : "PUT"; const body = JSON.stringify(PASSWORD_POLICY); try { await zFetch("/management/v1/policies/password/complexity", { method, headers: { "Content-Type": "application/json" }, body }, token); } catch (error) { if (String(error.message || "").includes("NotChanged")) return; if (method === "PUT") { try { await zFetch("/management/v1/policies/password/complexity", { method: "POST", headers: { "Content-Type": "application/json" }, body }, token); } catch (fallbackError) { if (String(fallbackError.message || "").includes("already exists")) return; throw fallbackError; } return; } throw error; } } async function ensureLoginV2(token) { const current = await zFetch("/v2/features/instance", {}, token); const loginV2 = current.loginV2 || {}; const desiredBaseUri = stripTrailingSlash(env.APP_URL); const currentBaseUri = stripTrailingSlash(loginV2.baseUri); if (loginV2.required === true && currentBaseUri === desiredBaseUri) return; await zFetch("/v2/features/instance", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ loginV2: { required: true, baseUri: desiredBaseUri } }) }, token); } async function setupOidcApp(token = adminToken()) { if (state.clientId && state.clientSecret) return; if (state.setupTried) return; if (!(await waitForZitadel())) return; if (!token) return; state.setupTried = true; let project = null; try { const search = await zFetch("/management/v1/projects/_search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ queries: [{ nameQuery: { name: env.ZITADEL_PROJECT_NAME, method: "TEXT_QUERY_METHOD_EQUALS" } }] }) }, token); project = (search.result || search.projects || []).find(item => item.name === env.ZITADEL_PROJECT_NAME); } catch (error) { debug("project search failed", error.message); } if (!project) { const created = await zFetch("/management/v1/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: env.ZITADEL_PROJECT_NAME, projectRoleAssertion: false, projectRoleCheck: false, hasProjectCheck: false }) }, token); project = { id: created.id || created.projectId }; } const projectId = project.id || project.projectId; state.projectId = projectId; if (!projectId) throw new Error("Не удалось получить projectId ZITADEL."); const app = await zFetch(`/management/v1/projects/${projectId}/apps/oidc`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: env.OIDC_APP_NAME, redirectUris: [env.OIDC_REDIRECT_URI], responseTypes: ["OIDC_RESPONSE_TYPE_CODE"], grantTypes: ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"], appType: "OIDC_APP_TYPE_WEB", authMethodType: "OIDC_AUTH_METHOD_TYPE_BASIC", version: "OIDC_VERSION_1_0", devMode: true, accessTokenType: "OIDC_TOKEN_TYPE_JWT", idTokenUserinfoAssertion: true }) }, token); state.clientId = app.clientId; state.clientSecret = app.clientSecret; persistOidcClientConfig(state.clientId, state.clientSecret); debug("OIDC app created", { projectId, clientId: state.clientId }); } async function syncZitadelConfig() { if (state.configSyncTried) return; if (!(await waitForZitadel())) return; const token = adminToken(); if (!token) return; state.configSyncTried = true; await ensurePasswordPolicy(token); await ensureLoginV2(token); await setupOidcApp(token); } async function createUser({ fullName, email, password }) { await syncZitadelConfig(); const parts = fullName.trim().split(/\s+/); const givenName = parts[0] || fullName; const familyName = parts.slice(1).join(" ") || "User"; return zFetch("/v2/users/human", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: email, profile: { givenName, familyName, displayName: fullName, preferredLanguage: "ru" }, email: { email, isVerified: true }, password: { password, changeRequired: false } }) }, adminToken()); } async function createPasswordSession(loginName, password) { const token = loginClientToken(); const created = await zFetch("/v2/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ checks: { user: { loginName } } }) }, token); const sessionId = created.sessionId; let sessionToken = created.sessionToken; if (!sessionId || !sessionToken) { throw new Error("Не удалось создать session ZITADEL."); } const updated = await zFetch(`/v2/sessions/${encodeURIComponent(sessionId)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ checks: { password: { password } } }) }, token); sessionToken = updated.sessionToken || sessionToken; return { sessionId, sessionToken }; } async function createAuthRequest() { await syncZitadelConfig(); const clientId = state.clientId || env.OIDC_CLIENT_ID; if (!clientId) throw new Error("OIDC app не настроен. Укажите OIDC_CLIENT_ID/OIDC_CLIENT_SECRET в .env."); const stateValue = crypto.randomBytes(24).toString("base64url"); const verifier = crypto.randomBytes(48).toString("base64url"); const challenge = crypto.createHash("sha256").update(verifier).digest("base64url"); const url = `${env.ZITADEL_URL}/oauth/v2/authorize?${new URLSearchParams({ client_id: clientId, redirect_uri: env.OIDC_REDIRECT_URI, response_type: "code", scope: "openid email profile", code_challenge: challenge, code_challenge_method: "S256", state: stateValue })}`; const res = await fetch(url, { redirect: "manual" }); const location = res.headers.get("location") || ""; const redirectUrl = new URL(location, env.ZITADEL_PUBLIC_URL); const authRequest = redirectUrl.searchParams.get("authRequest"); const hostedAuthRequest = redirectUrl.searchParams.get("authRequestID") || redirectUrl.searchParams.get("auth_request") || ""; debug("authorize start", res.status, location); if (!authRequest && hostedAuthRequest) { throw new Error("ZITADEL вернул hosted login flow вместо custom login. Проверьте loginV2 и baseUri."); } if (!authRequest) throw new Error("ZITADEL не вернул authRequest."); return { authRequest, stateValue, verifier }; } async function finalizeAuthRequest(authRequest, session) { const token = loginClientToken(); const response = await zFetch(`/v2/oidc/auth_requests/${encodeURIComponent(authRequest)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session: { sessionId: session.sessionId, sessionToken: session.sessionToken } }) }, token); if (!response.callbackUrl) throw new Error("ZITADEL не вернул callbackUrl."); return response.callbackUrl; } async function exchangeCode(callbackUrl, verifier, expectedState) { const callback = new URL(callbackUrl, env.APP_URL); const code = callback.searchParams.get("code"); const returnedState = callback.searchParams.get("state"); if (!code || returnedState !== expectedState) { throw new Error("Некорректный callback от ZITADEL."); } const clientId = state.clientId || env.OIDC_CLIENT_ID; const clientSecret = state.clientSecret || env.OIDC_CLIENT_SECRET; const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); const res = await fetch(`${env.ZITADEL_URL}/oauth/v2/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${basic}` }, body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: env.OIDC_REDIRECT_URI, code_verifier: verifier }) }); const data = await res.json(); debug("token", res.status, data); if (!res.ok || !data.id_token) throw new Error("ZITADEL не выдал id_token."); return { idToken: data.id_token, accessToken: data.access_token || "" }; } async function getJwks() { const now = Date.now(); if (state.jwks && now - state.jwksFetchedAt < 5 * 60 * 1000) { return state.jwks; } const res = await fetch(`${env.ZITADEL_URL}/oauth/v2/keys`); const data = await res.json(); if (!res.ok || !Array.isArray(data.keys)) { throw new Error("Не удалось получить JWKS ZITADEL."); } state.jwks = data.keys; state.jwksFetchedAt = now; return state.jwks; } function extractPublicClaims(claims) { const username = claims.preferred_username || claims.username || claims.email || ""; return { sub: claims.sub || "", email: claims.email || "", name: claims.name || username, preferred_username: claims.preferred_username || "", username, iss: claims.iss || "", aud: Array.isArray(claims.aud) ? claims.aud : (claims.aud ? [claims.aud] : []), iat: claims.iat || 0, exp: claims.exp || 0 }; } function formatTimestamp(timestamp) { if (!timestamp) return "-"; return new Date(timestamp * 1000).toLocaleString("ru-RU"); } function initials(name, email) { const source = name || email || "MM"; return source .split(/[ ._-]+/) .filter(Boolean) .slice(0, 2) .map(part => part[0]) .join("") .toUpperCase(); } function jsonScript(value) { return JSON.stringify(value).replace(/ item.kid === header.kid); if (!jwk) throw new Error("Не найден публичный ключ для JWT."); const algorithms = { RS256: "RSA-SHA256", RS384: "RSA-SHA384", RS512: "RSA-SHA512" }; const algorithm = algorithms[header.alg]; if (!algorithm) throw new Error(`Неподдерживаемый алгоритм JWT: ${header.alg}`); const publicKey = crypto.createPublicKey({ key: jwk, format: "jwk" }); const payload = Buffer.from(`${parts[0]}.${parts[1]}`); const verified = crypto.verify(algorithm, payload, publicKey, signature); if (!verified) throw new Error("Подпись JWT не прошла проверку."); return extractPublicClaims(claims); } async function closeZitadelSession(session) { if (!session?.sessionId || !session?.sessionToken) return; try { await fetch(`${env.ZITADEL_URL}/v2/sessions/${encodeURIComponent(session.sessionId)}`, { method: "DELETE", headers: { Accept: "application/json", Authorization: `Bearer ${loginClientToken() || adminToken() || session.sessionToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ sessionToken: session.sessionToken }) }); } catch (error) { debug("session close failed", error.message); } } function page(title, body) { return ` ${escapeHtml(title)} ${body} `; } function authLayout({ title, subtitle, heading, text, form }) { return `

${heading}

${text}

${title}

${subtitle}

${form}
`; } function loginPath(authRequest = "") { const suffix = authRequest ? `?${new URLSearchParams({ authRequest })}` : ""; return `/login${suffix}`; } function registerPath(authRequest = "") { const suffix = authRequest ? `?${new URLSearchParams({ authRequest })}` : ""; return `/register${suffix}`; } function loginPage(req, res, msg = "", headers = {}, explicitAuthRequest = "") { const url = new URL(req.url, env.APP_URL); const authRequest = explicitAuthRequest || url.searchParams.get("authRequest") || ""; const hiddenAuthRequest = authRequest ? `` : ""; const registerHref = registerPath(authRequest); const form = `
${msg}${hiddenAuthRequest}
`; send(res, 200, page("Вход | Million Miles", authLayout({ title: "Вход", subtitle: "Войдите в личный кабинет Million Miles.", heading: "Добро пожаловать", text: "Кастомная форма входа. ZITADEL используется только как backend IdP.", form })), headers); } function registerPage(req, res, msg = "", explicitAuthRequest = "") { const url = new URL(req.url, env.APP_URL); const authRequest = explicitAuthRequest || url.searchParams.get("authRequest") || ""; const hiddenAuthRequest = authRequest ? `` : ""; const loginHref = loginPath(authRequest); const form = `
${msg}${hiddenAuthRequest}
`; send(res, 200, page("Регистрация | Million Miles", authLayout({ title: "Регистрация", subtitle: "Создайте личный кабинет Million Miles.", heading: `Персональный доступ Million Miles`, text: "Пароли и учетные записи хранятся в ZITADEL, не в нашем приложении.", form }))); } async function registerPost(req, res) { const data = await parseBody(req); const email = String(data.email || "").trim().toLowerCase(); const fullName = String(data.fullName || "").trim(); const password = String(data.password || ""); const confirmPassword = String(data.confirmPassword || ""); const authRequest = String(data.authRequest || "").trim(); try { if (!fullName || !email || !password || !confirmPassword) { throw new Error("Проверьте заполненные данные."); } if (password !== confirmPassword) { throw new Error("Пароли не совпадают."); } const passwordError = validatePassword(password); if (passwordError) throw new Error(passwordError); await createUser({ fullName, email, password }); return redirect(res, loginPath(authRequest), { "Set-Cookie": makeCookie("mm_flash", "registered", 120) }); } catch (error) { return registerPage(req, res, `
${escapeHtml(error.message || "Не удалось создать аккаунт.")}
`, authRequest); } } async function loginPost(req, res) { const data = await parseBody(req); const email = String(data.email || "").trim().toLowerCase(); const password = String(data.password || ""); const authRequest = String(data.authRequest || "").trim(); try { if (!email || !password) throw new Error("Введите email и пароль."); const session = await createPasswordSession(email, password); if (authRequest) { const callbackUrl = await finalizeAuthRequest(authRequest, session); return redirect(res, callbackUrl); } const auth = await createAuthRequest(); const callbackUrl = await finalizeAuthRequest(auth.authRequest, session); return redirect(res, callbackUrl, { "Set-Cookie": makeCookie("mm_auth", makeSigned({ verifier: auth.verifier, stateValue: auth.stateValue, sessionId: session.sessionId, sessionToken: session.sessionToken }), 300) }); } catch (error) { return loginPage(req, res, `
${escapeHtml(error.message || "Неверный email или пароль.")}
`, {}, authRequest); } } async function authCallback(req, res) { const pendingAuth = getPendingAuth(req); if (!pendingAuth?.verifier || !pendingAuth?.stateValue) { return redirect(res, "/login", { "Set-Cookie": clearCookie("mm_auth") }); } try { const callbackUrl = new URL(req.url, env.APP_URL).toString(); const tokens = await exchangeCode(callbackUrl, pendingAuth.verifier, pendingAuth.stateValue); await validateIdToken(tokens.idToken); const signedSession = makeSigned({ idToken: tokens.idToken, accessToken: tokens.accessToken, sessionId: pendingAuth.sessionId || "", sessionToken: pendingAuth.sessionToken || "" }); return redirect(res, "/dashboard", { "Set-Cookie": [ makeCookie("mm_session", signedSession, 3600 * 8), clearCookie("mm_auth") ] }); } catch { return redirect(res, "/login", { "Set-Cookie": clearCookie("mm_auth") }); } } async function dashboard(req, res) { const session = getSession(req); if (!session?.idToken) return redirect(res, "/login"); let profile; try { profile = await validateIdToken(session.idToken); } catch { return redirect(res, "/login", { "Set-Cookie": clearCookie("mm_session") }); } const body = `
${escapeHtml(initials(profile.name, profile.email))}

${escapeHtml(profile.name || "Клиент")}

Данные ниже получены из /me. Backend валидирует JWT ZITADEL из HttpOnly cookie.

Claims

sub${escapeHtml(profile.sub || "-")}
name${escapeHtml(profile.name || "-")}
email${escapeHtml(profile.email || "-")}
username${escapeHtml(profile.username || profile.preferred_username || "-")}
iat${escapeHtml(formatTimestamp(profile.iat))}
exp${escapeHtml(formatTimestamp(profile.exp))}
iss${escapeHtml(profile.iss || "-")}
aud${escapeHtml(Array.isArray(profile.aud) ? profile.aud.join(", ") : profile.aud || "-")}
/me JSON
${escapeHtml(JSON.stringify(profile, null, 2))}
`; const script = ``; send(res, 200, page("Dashboard | Million Miles", body + script)); } async function me(req, res) { const session = getSession(req); if (!session?.idToken) { return sendJson(res, 401, { error: "unauthorized" }); } try { const claims = await validateIdToken(session.idToken); return sendJson(res, 200, claims); } catch (error) { return sendJson(res, 401, { error: error.message || "invalid_token" }, { "Set-Cookie": clearCookie("mm_session") }); } } async function logout(req, res) { const session = getSession(req); await closeZitadelSession(session); redirect(res, "/login", { "Set-Cookie": [clearCookie("mm_session"), clearCookie("mm_flash"), clearCookie("mm_auth")] }); } function redirect(res, location, headers = {}) { res.writeHead(302, { Location: location, ...headers }); res.end(); } function send(res, status, content, headers = {}) { res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", ...headers }); res.end(content); } function sendJson(res, status, value, headers = {}) { res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...headers }); res.end(JSON.stringify(value)); } http.createServer(async (req, res) => { const url = new URL(req.url, env.APP_URL); if (req.method === "GET" && url.pathname === "/") return redirect(res, "/login"); if (req.method === "GET" && url.pathname === "/login") { if (getSession(req)) return redirect(res, "/dashboard"); const authRequest = url.searchParams.get("authRequest") || ""; if (!authRequest) { try { const auth = await createAuthRequest(); const authCookie = makeCookie("mm_auth", makeSigned({ verifier: auth.verifier, stateValue: auth.stateValue }), 300); if (getFlash(req) === "registered") { return loginPage(req, res, `
Аккаунт создан. Теперь можно войти.
`, { "Set-Cookie": [authCookie, clearCookie("mm_flash")] }, auth.authRequest); } return loginPage(req, res, "", { "Set-Cookie": authCookie }, auth.authRequest); } catch (error) { return loginPage(req, res, `
${escapeHtml(error.message || "Не удалось подготовить вход.")}
`); } } if (getFlash(req) === "registered") { return loginPage(req, res, `
Аккаунт создан. Теперь можно войти.
`, { "Set-Cookie": clearCookie("mm_flash") }); } return loginPage(req, res); } if (req.method === "POST" && url.pathname === "/login") return loginPost(req, res); if (req.method === "GET" && url.pathname === "/auth/callback") return authCallback(req, res); if (req.method === "GET" && url.pathname === "/register") { if (getSession(req)) return redirect(res, "/dashboard"); return registerPage(req, res); } if (req.method === "POST" && url.pathname === "/register") return registerPost(req, res); if (req.method === "GET" && url.pathname === "/dashboard") return dashboard(req, res); if (req.method === "GET" && url.pathname === "/me") return me(req, res); if (req.method === "POST" && url.pathname === "/logout") return logout(req, res); if (req.method === "GET" && url.pathname === "/health") { return sendJson(res, 200, { ok: true }); } redirect(res, "/login"); }).listen(PORT, () => { syncZitadelConfig().catch(error => debug("startup setup failed", error.message)); console.log(`Million Miles ZITADEL POC on http://localhost:${PORT}`); });