Files
DevRunbook-Public/packages/application/src/auth/auth-service.ts
T
DevRunbook release export cfd2804e27
Managed validation / full (push) Successful in 3m18s
Publish DevRunbook source
2026-09-03 04:09:17 +02:00

149 lines
4.2 KiB
TypeScript

import { isSessionActive, resolveSessionDeadlines } from './session-policy'
import type { TokenDigester } from './token-digest'
export interface AuthUserRecord {
id: string
email: string
displayName: string
passwordHash: string
emailVerified: boolean
image: string | null
status: 'active' | 'disabled' | 'pending_deletion'
createdAt: Date
updatedAt: Date
}
export interface AuthSessionRecord {
id: string
userId: string
tokenHash: string
createdAt: Date
lastSeenAt: Date
idleExpiresAt: Date
absoluteExpiresAt: Date
revokedAt: Date | null
sourceIpHash: string | null
userAgentSummary: string | null
}
export interface CreateAuthSessionRecord {
userId: string
tokenHash: string
createdAt: Date
lastSeenAt: Date
idleExpiresAt: Date
absoluteExpiresAt: Date
sourceIpHash: string | null
userAgentSummary: string | null
}
export interface AuthPersistence {
findUserById(id: string): Promise<AuthUserRecord | null>
findUserByEmail(email: string): Promise<AuthUserRecord | null>
updateUser(
id: string,
update: Partial<
Pick<
AuthUserRecord,
'displayName' | 'email' | 'emailVerified' | 'image' | 'passwordHash'
>
>,
): Promise<AuthUserRecord | null>
createSession(input: CreateAuthSessionRecord): Promise<AuthSessionRecord>
findSessionByTokenHash(tokenHash: string): Promise<AuthSessionRecord | null>
touchSession(
id: string,
input: { lastSeenAt: Date; idleExpiresAt: Date },
): Promise<AuthSessionRecord | null>
revokeSessionByTokenHash(tokenHash: string, revokedAt: Date): Promise<boolean>
revokeSessionsForUser(userId: string, revokedAt: Date): Promise<number>
}
export interface ActiveAuthSession {
session: AuthSessionRecord
user: AuthUserRecord
rawToken: string
}
export class AuthService {
constructor(
private readonly persistence: AuthPersistence,
private readonly digester: TokenDigester,
private readonly now: () => Date = () => new Date(),
) {}
findUserById(id: string) {
return this.persistence.findUserById(id)
}
findUserByEmail(email: string) {
return this.persistence.findUserByEmail(email.trim().toLowerCase())
}
updateUser(
id: string,
update: Partial<
Pick<
AuthUserRecord,
'displayName' | 'email' | 'emailVerified' | 'image' | 'passwordHash'
>
>,
) {
return this.persistence.updateUser(id, update)
}
async createSession(input: {
userId: string
rawToken: string
sourceIpHash?: string | null
userAgentSummary?: string | null
}): Promise<ActiveAuthSession | null> {
const user = await this.persistence.findUserById(input.userId)
if (!user || user.status !== 'active') return null
const now = this.now()
const deadlines = resolveSessionDeadlines({ createdAt: now, now })
const session = await this.persistence.createSession({
userId: user.id,
tokenHash: this.digester.digest(input.rawToken),
createdAt: now,
lastSeenAt: now,
...deadlines,
sourceIpHash: input.sourceIpHash ?? null,
userAgentSummary: input.userAgentSummary ?? null,
})
return { session, user, rawToken: input.rawToken }
}
async findActiveSession(rawToken: string): Promise<ActiveAuthSession | null> {
const now = this.now()
const session = await this.persistence.findSessionByTokenHash(
this.digester.digest(rawToken),
)
if (!session || !isSessionActive(now, session)) return null
const user = await this.persistence.findUserById(session.userId)
if (!user || user.status !== 'active') return null
const deadlines = resolveSessionDeadlines({
createdAt: session.createdAt,
now,
absoluteExpiresAt: session.absoluteExpiresAt,
})
const touched = await this.persistence.touchSession(session.id, {
lastSeenAt: now,
idleExpiresAt: deadlines.idleExpiresAt,
})
if (!touched) return null
return { session: touched, user, rawToken }
}
revokeSession(rawToken: string): Promise<boolean> {
return this.persistence.revokeSessionByTokenHash(
this.digester.digest(rawToken),
this.now(),
)
}
revokeSessionsForUser(userId: string): Promise<number> {
return this.persistence.revokeSessionsForUser(userId, this.now())
}
}