feat: argon2 passwords and sqlite-backed sessions
This commit is contained in:
66
server/src/auth.ts
Normal file
66
server/src/auth.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user