67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
|
|
import crypto from 'node:crypto'
|
||
|
|
import argon2 from 'argon2'
|
||
|
|
import type { DB } from './db.js'
|
||
|
|
|
||
|
|
export interface UserRow {
|
||
|
|
id: number
|
||
|
|
username: string
|
||
|
|
password_hash: string
|
||
|
|
is_admin: number
|
||
|
|
created_at: string
|
||
|
|
}
|
||
|
|
|
||
|
|
const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000 // 30 days
|
||
|
|
|
||
|
|
export function hashPassword(password: string): Promise<string> {
|
||
|
|
return argon2.hash(password)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function verifyPassword(hash: string, password: string): Promise<boolean> {
|
||
|
|
return argon2.verify(hash, password)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createUser(db: DB, username: string, passwordHash: string, isAdmin: boolean): UserRow {
|
||
|
|
const info = db
|
||
|
|
.prepare('INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, ?)')
|
||
|
|
.run(username, passwordHash, isAdmin ? 1 : 0)
|
||
|
|
return db.prepare('SELECT * FROM users WHERE id = ?').get(info.lastInsertRowid) as UserRow
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getUserByUsername(db: DB, username: string): UserRow | undefined {
|
||
|
|
return db.prepare('SELECT * FROM users WHERE username = ?').get(username) as UserRow | undefined
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getUserById(db: DB, id: number): UserRow | undefined {
|
||
|
|
return db.prepare('SELECT * FROM users WHERE id = ?').get(id) as UserRow | undefined
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createSession(db: DB, userId: number): string {
|
||
|
|
const token = crypto.randomBytes(32).toString('hex')
|
||
|
|
const expiresAt = new Date(Date.now() + SESSION_TTL_MS).toISOString()
|
||
|
|
db.prepare('INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)').run(
|
||
|
|
token,
|
||
|
|
userId,
|
||
|
|
expiresAt
|
||
|
|
)
|
||
|
|
return token
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getUserBySession(db: DB, token: string, now: () => number = Date.now): UserRow | null {
|
||
|
|
const row = db
|
||
|
|
.prepare(
|
||
|
|
`SELECT u.*, s.expires_at FROM sessions s
|
||
|
|
JOIN users u ON u.id = s.user_id WHERE s.token = ?`
|
||
|
|
)
|
||
|
|
.get(token) as (UserRow & { expires_at: string }) | undefined
|
||
|
|
if (!row) return null
|
||
|
|
if (new Date(row.expires_at).getTime() < now()) {
|
||
|
|
deleteSession(db, token)
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
return row
|
||
|
|
}
|
||
|
|
|
||
|
|
export function deleteSession(db: DB, token: string): void {
|
||
|
|
db.prepare('DELETE FROM sessions WHERE token = ?').run(token)
|
||
|
|
}
|