mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-15 19:06:16 +08:00
Remove the transitional website auth, admin hooks, SEO and hosted analytics together, then align the app routes, runtime configuration, tests and dependencies. These changes form one atomic cleanup because the filtered app must switch its identity and runtime surfaces as a unit.
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import { deleteCookie, getCookie, setCookie, type H3Event } from 'h3'
|
||
import { createJwtToken, verifyJwtToken } from './jwt'
|
||
import type { AppUserDocument } from '../models/AppUser'
|
||
|
||
/**
|
||
* The app's *own* session — deliberately independent of the issuer.
|
||
*
|
||
* Once an identity has arrived (via the SSO exchange, or trivially in
|
||
* AUTH_MODE=open), the app mints a session signed with its own secret and
|
||
* stored in a host-only cookie on its own origin. No cross-domain cookie or
|
||
* CORS is needed. After this point the app can serve every request without the
|
||
* optional issuer being reachable.
|
||
*/
|
||
|
||
const APP_SESSION_COOKIE = 'os_app_session'
|
||
const APP_SESSION_TTL_SECONDS = 60 * 60 * 24 * 30
|
||
const APP_ACCESS_TOKEN_TTL_SECONDS = 60 * 60 * 24
|
||
|
||
// Marks a bearer token as minted by this app.
|
||
const APP_TOKEN_TYPE = 'app'
|
||
|
||
export interface AppSessionPayload {
|
||
sub: string
|
||
sso: string
|
||
email: string
|
||
role: string
|
||
}
|
||
|
||
function getAppSessionSecret() {
|
||
// JWT_SECRET remains a compatibility fallback for existing installations.
|
||
const secret = (process.env.APP_JWT_SECRET || process.env.JWT_SECRET || '').trim()
|
||
if (!secret) {
|
||
throw new Error('App session secret missing – bitte APP_JWT_SECRET (oder JWT_SECRET) in .env setzen')
|
||
}
|
||
return secret
|
||
}
|
||
|
||
function sessionClaims(user: AppUserDocument) {
|
||
return {
|
||
sub: String(user._id),
|
||
sso: user.ssoSubject,
|
||
email: user.email,
|
||
role: user.role,
|
||
typ: APP_TOKEN_TYPE,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Short-lived bearer token for the browser. The durable session lives in the
|
||
* httpOnly cookie; this is what the client puts in the Authorization header.
|
||
*/
|
||
export function createAppAccessToken(user: AppUserDocument) {
|
||
return createJwtToken(sessionClaims(user), getAppSessionSecret(), APP_ACCESS_TOKEN_TTL_SECONDS)
|
||
}
|
||
|
||
/**
|
||
* Returns the payload if `token` is an app-minted bearer token, else null.
|
||
*/
|
||
export function verifyAppAccessToken(token: string): AppSessionPayload | null {
|
||
try {
|
||
const payload = verifyJwtToken(token, getAppSessionSecret())
|
||
if (payload?.typ !== APP_TOKEN_TYPE || !payload?.sub) return null
|
||
return {
|
||
sub: String(payload.sub),
|
||
sso: String(payload.sso || payload.sub),
|
||
email: String(payload.email || ''),
|
||
role: String(payload.role || 'user'),
|
||
}
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
export function issueAppSession(event: H3Event, user: AppUserDocument) {
|
||
const token = createJwtToken(sessionClaims(user), getAppSessionSecret(), APP_SESSION_TTL_SECONDS)
|
||
|
||
setCookie(event, APP_SESSION_COOKIE, token, {
|
||
httpOnly: true,
|
||
sameSite: 'lax',
|
||
path: '/',
|
||
maxAge: APP_SESSION_TTL_SECONDS,
|
||
// Host-only on purpose: no `domain`, so the cookie never leaks to the
|
||
// issuer's origin and self-hosted instances behave identically.
|
||
secure: process.env.NODE_ENV === 'production',
|
||
})
|
||
|
||
return token
|
||
}
|
||
|
||
export function readAppSession(event: H3Event): AppSessionPayload | null {
|
||
const token = getCookie(event, APP_SESSION_COOKIE)
|
||
if (!token) return null
|
||
return verifyAppAccessToken(token)
|
||
}
|
||
|
||
export function clearAppSession(event: H3Event) {
|
||
deleteCookie(event, APP_SESSION_COOKIE, { path: '/' })
|
||
}
|