feat: implement permissions and actions management
- Add permissions logic for status options in corrective actions. - Create tests for permissions functionality. - Implement queries for user authentication and actions management. - Develop Actions page to display and manage corrective actions. - Create authentication pages for login, setup, and password reset. - Add IncidentDetail page to view incident details and associated actions. - Implement Settings page for user management and password changes.
This commit is contained in:
1 parent
708da6b9f7
commit
3f6dff5f24
94 files changed
+7213
-643
No files matched your search
+55
-1
@@ -1,17 +1,29 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { Hono } from 'hono'
|
||||
import { changePasswordSchema, loginSchema } from '../../shared/schemas.ts'
|
||||
import { changePasswordSchema, emailOnlySchema, loginSchema, resetPasswordSchema, tokenSchema } 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 { consumeAuthToken, createAuthToken } from '../auth/tokens.ts'
|
||||
import type { Db } from '../db/client.ts'
|
||||
import { organizations, users } from '../db/schema.ts'
|
||||
import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts'
|
||||
import { publicUser } from './users.ts'
|
||||
|
||||
const MAGIC_LINK_TTL_MS = 15 * 60 * 1000
|
||||
const RESET_TTL_MS = 60 * 60 * 1000
|
||||
|
||||
async function activeUserByEmail(db: Db, email: string) {
|
||||
const [user] = await db.select({ id: users.id, email: users.email, active: users.active }).from(users).where(eq(users.email, email))
|
||||
return user?.active ? user : null
|
||||
}
|
||||
|
||||
export function authRoutes() {
|
||||
// 10 failed-or-successful attempts per email per 15 minutes
|
||||
const loginLimiter = new RateLimiter(10, 15 * 60 * 1000)
|
||||
// Emailed links: 5 per email per 15 minutes
|
||||
const emailLimiter = new RateLimiter(5, 15 * 60 * 1000)
|
||||
|
||||
return new Hono<AppEnv>()
|
||||
.post('/login', validate('json', loginSchema), async (c) => {
|
||||
@@ -30,6 +42,48 @@ export function authRoutes() {
|
||||
return c.json({ user: result })
|
||||
})
|
||||
|
||||
// Always 204, so the response never reveals whether an account exists.
|
||||
.post('/magic-link', validate('json', emailOnlySchema), async (c) => {
|
||||
const { email } = c.req.valid('json')
|
||||
if (!emailLimiter.attempt(`magic:${email}`)) throw fail(429, 'Too many requests. Try again in 15 minutes.')
|
||||
const db = c.get('db')
|
||||
const user = await activeUserByEmail(db, email)
|
||||
if (user) c.get('notifier').magicLink(user.email, await createAuthToken(db, user.id, 'magic_link', MAGIC_LINK_TTL_MS))
|
||||
return c.body(null, 204)
|
||||
})
|
||||
|
||||
// POST rather than GET so email link scanners can't use up the token.
|
||||
.post('/magic-link/verify', validate('json', tokenSchema), async (c) => {
|
||||
const db = c.get('db')
|
||||
const userId = await consumeAuthToken(db, c.req.valid('json').token, 'magic_link')
|
||||
if (!userId) throw fail(400, 'This sign-in link is invalid or has expired. Request a new one.')
|
||||
await db.update(users).set({ lastLoginAt: new Date() }).where(eq(users.id, userId))
|
||||
await startSession(c, userId)
|
||||
const [user] = await db.select(publicUser).from(users).where(eq(users.id, userId))
|
||||
return c.json({ user })
|
||||
})
|
||||
|
||||
.post('/password-reset', validate('json', emailOnlySchema), async (c) => {
|
||||
const { email } = c.req.valid('json')
|
||||
if (!emailLimiter.attempt(`reset:${email}`)) throw fail(429, 'Too many requests. Try again in 15 minutes.')
|
||||
const db = c.get('db')
|
||||
const user = await activeUserByEmail(db, email)
|
||||
if (user) c.get('notifier').passwordReset(user.email, await createAuthToken(db, user.id, 'password_reset', RESET_TTL_MS), null)
|
||||
return c.body(null, 204)
|
||||
})
|
||||
|
||||
.post('/password-reset/confirm', validate('json', resetPasswordSchema), async (c) => {
|
||||
const { token, newPassword } = c.req.valid('json')
|
||||
const db = c.get('db')
|
||||
const userId = await consumeAuthToken(db, token, 'password_reset')
|
||||
if (!userId) throw fail(400, 'This link is invalid or has expired. Request a new one.')
|
||||
await db.update(users).set({ passwordHash: await hashPassword(newPassword), lastLoginAt: new Date() }).where(eq(users.id, userId))
|
||||
await deleteUserSessions(db, userId)
|
||||
await startSession(c, userId)
|
||||
const [user] = await db.select(publicUser).from(users).where(eq(users.id, userId))
|
||||
return c.json({ user })
|
||||
})
|
||||
|
||||
.post('/logout', async (c) => {
|
||||
await endSession(c)
|
||||
return c.body(null, 204)
|
||||
|
||||
Reference in new issue
Block a user