This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user