Files
technitium-console/hub/session.ts
T
DenozordecandClaude Opus 5.5 3d8df08fa0 Hub stage 1: apps across servers, read-only
hub/apps/matrix reads apps/list and each app's config on every server (the read
token first, the admin token once if a server refuses) and returns per app and server
the version, available update and a config hash, plus whether versions and configs
match and how many JSON leaf paths differ. Config texts never leave the hub. In hub
mode the Apps section has the server selector: All servers shows the matrix with
filters for differing versions, differing configs and updates; one server shows its
list without write actions. The dev mock takes --variant a|b with differing apps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:46:51 +07:00

119 lines
3.9 KiB
TypeScript

/*
The hub's own sign-in. Sessions are kept in memory with an idle expiry; each remembers who
signed in and what that user looked like (role, hash, disabled flag) at that moment, so a
change to the users file ends the sessions of the users it touched. `info` is the object
the console reads from `user/login` and `user/session/get` (flat, as the real server sends it).
*/
import { randomBytes } from 'node:crypto'
import { HUB_VERSION, type HubConfig } from './config.ts'
import { signature, type Role, type UserRecord, type Users } from './users.ts'
export const IDLE_MS = 30 * 60_000
export interface Identity {
user: string
role: Role
}
interface Session extends Identity {
last: number
sig: string
}
/*
Every key `src/app/sections.ts` reads, spelled out: the console hides a section only
when `canView === false`, so a missing key would leave it visible. The hub shows the
Dashboard, the Logs and the Apps; About has no permission and is always there. The hub is
read-only, so nothing can be modified or deleted.
*/
const PERMISSION_KEYS = [
'Dashboard', 'Zones', 'Cache', 'Allowed', 'Blocked', 'Apps', 'DnsClient', 'Settings', 'DhcpServer', 'Administration', 'Logs',
]
const VIEWABLE = new Set(['Dashboard', 'Logs', 'Apps'])
export class Sessions {
#config: HubConfig
#users: Users
#now: () => number
#started: string
#seen = new Map<string, Session>()
constructor(config: HubConfig, users: Users, now: () => number = Date.now) {
this.#config = config
this.#users = users
this.#now = now
this.#started = new Date(now()).toISOString()
}
/** Opens a session for a user already authenticated by `Users.authenticate`. */
open(user: UserRecord): string {
this.#sweep()
const token = randomBytes(32).toString('hex')
this.#seen.set(token, { user: user.name, role: user.role, last: this.#now(), sig: signature(user) })
return token
}
/* The session's identity while it is valid (extending it), else null. A user who was removed,
disabled or edited since sign-in ends the session. */
touch(token: string | null | undefined): Identity | null {
if (!token) return null
const session = this.#seen.get(token)
if (session === undefined) return null
const record = this.#users.find(session.user)
if (this.#now() - session.last > IDLE_MS || record === undefined || signature(record) !== session.sig) {
this.#seen.delete(token)
return null
}
session.last = this.#now()
return { user: session.user, role: session.role }
}
logout(token: string): void {
this.#seen.delete(token)
}
#sweep(): void {
const now = this.#now()
for (const [token, session] of this.#seen) if (now - session.last > IDLE_MS) this.#seen.delete(token)
}
info(role: Role) {
const { servers, domain } = this.#config
return {
version: HUB_VERSION,
uptimestamp: this.#started,
dnsServerDomain: domain,
permissions: Object.fromEntries(
PERMISSION_KEYS.map((key) => [key, { canView: VIEWABLE.has(key), canModify: false, canDelete: false }]),
),
/* The console offers its node selector when the cluster is initialised. The
first server plays the Primary (`primaryNodeName` looks for one). */
clusterInitialized: true,
clusterDomain: domain,
clusterNodes: servers.map((s, i) => ({ name: s.id, type: i === 0 ? 'Primary' : 'Secondary' })),
defaultRecordTtl: 3600,
defaultNsRecordTtl: 3600,
defaultSoaRecordTtl: 900,
useSoaSerialDateScheme: true,
dnssecValidation: true,
hub: true,
hubRole: role,
}
}
/** The flat body of `user/login` and `user/session/get`. */
body(token: string, identity: Identity) {
return {
status: 'ok',
token,
username: identity.user,
displayName: identity.user,
type: 'Local',
isSsoUser: false,
totpEnabled: false,
hubRole: identity.role,
info: this.info(identity.role),
}
}
}