- Add incident reporting routes and logic to handle creation, updating, and retrieval of incidents. - Implement visibility rules for users based on roles (reporters, supervisors). - Create tests for incident reporting functionality to ensure correctness and reliability. - Introduce site and area management routes, including validation for area hierarchy. - Establish user management routes with role-based access control for creating and updating users. - Add setup routes for initializing the application with an organization and admin user. - Implement shared enums and schemas for consistent validation and data handling across the application. - Create a test helper for setting up an in-memory database and API client for testing. - Add TypeScript configuration for server-side code with strict type checking and module resolution.
60 lines
2.8 KiB
TypeScript
60 lines
2.8 KiB
TypeScript
import { eq } from 'drizzle-orm'
|
|
import { Hono } from 'hono'
|
|
import { changePasswordSchema, loginSchema } from '../../shared/schemas.ts'
|
|
import { endSession, requireAuth, startSession } from '../auth/middleware.ts'
|
|
import { hashPassword, verifyAgainstDummy, verifyPassword } from '../auth/password.ts'
|
|
import { RateLimiter } from '../auth/rate-limit.ts'
|
|
import { deleteUserSessions } from '../auth/sessions.ts'
|
|
import { organizations, users } from '../db/schema.ts'
|
|
import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts'
|
|
import { publicUser } from './users.ts'
|
|
|
|
export function authRoutes() {
|
|
// 10 failed-or-successful attempts per email per 15 minutes
|
|
const loginLimiter = new RateLimiter(10, 15 * 60 * 1000)
|
|
|
|
return new Hono<AppEnv>()
|
|
.post('/login', validate('json', loginSchema), async (c) => {
|
|
const { email, password } = c.req.valid('json')
|
|
if (!loginLimiter.attempt(email)) throw fail(429, 'Too many sign-in attempts. Try again in 15 minutes.')
|
|
|
|
const db = c.get('db')
|
|
const [user] = await db.select().from(users).where(eq(users.email, email))
|
|
const valid = user?.active && user.passwordHash ? await verifyPassword(password, user.passwordHash) : await verifyAgainstDummy(password)
|
|
if (!user || !valid) throw fail(401, 'Incorrect email or password')
|
|
|
|
loginLimiter.reset(email)
|
|
await db.update(users).set({ lastLoginAt: new Date() }).where(eq(users.id, user.id))
|
|
await startSession(c, user.id)
|
|
const [result] = await db.select(publicUser).from(users).where(eq(users.id, user.id))
|
|
return c.json({ user: result })
|
|
})
|
|
|
|
.post('/logout', async (c) => {
|
|
await endSession(c)
|
|
return c.body(null, 204)
|
|
})
|
|
|
|
.get('/me', requireAuth, async (c) => {
|
|
const me = currentUser(c)
|
|
const db = c.get('db')
|
|
const [user] = await db.select(publicUser).from(users).where(eq(users.id, me.id))
|
|
const [organization] = await db.select({ id: organizations.id, name: organizations.name }).from(organizations).where(eq(organizations.id, me.organizationId))
|
|
return c.json({ user, organization })
|
|
})
|
|
|
|
.post('/password', requireAuth, validate('json', changePasswordSchema), async (c) => {
|
|
const me = currentUser(c)
|
|
const { currentPassword, newPassword } = c.req.valid('json')
|
|
const db = c.get('db')
|
|
const [user] = await db.select({ passwordHash: users.passwordHash }).from(users).where(eq(users.id, me.id))
|
|
if (!user.passwordHash || !(await verifyPassword(currentPassword, user.passwordHash))) throw fail(400, 'Current password is incorrect')
|
|
|
|
await db.update(users).set({ passwordHash: await hashPassword(newPassword) }).where(eq(users.id, me.id))
|
|
// Sign out everywhere else
|
|
await deleteUserSessions(db, me.id)
|
|
await startSession(c, me.id)
|
|
return c.body(null, 204)
|
|
})
|
|
}
|