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
@@ -0,0 +1,80 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createTestContext, tokenFrom } from '../test/helpers.ts'
|
||||
|
||||
let ctx: Awaited<ReturnType<typeof createTestContext>>
|
||||
beforeAll(async () => {
|
||||
ctx = await createTestContext()
|
||||
await ctx.createUser('supervisor', 'linked@example.com')
|
||||
})
|
||||
afterAll(() => ctx.close())
|
||||
|
||||
const lastToken = async (email: string) => {
|
||||
await ctx.notifier.flush()
|
||||
return tokenFrom(ctx.mailer.last(email)?.text)
|
||||
}
|
||||
|
||||
describe('magic links', () => {
|
||||
it('signs in once with an emailed link', async () => {
|
||||
const client = ctx.anonymous()
|
||||
expect((await client.post('/api/auth/magic-link', { email: 'Linked@Example.com' })).status).toBe(204)
|
||||
const token = await lastToken('linked@example.com')
|
||||
expect(ctx.mailer.last('linked@example.com')?.text).toContain(`http://safety.test/auth/magic?token=${token}`)
|
||||
|
||||
const res = await client.post('/api/auth/magic-link/verify', { token })
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.user.email).toBe('linked@example.com')
|
||||
expect((await client.get('/api/auth/me')).status).toBe(200)
|
||||
|
||||
expect((await ctx.anonymous().post('/api/auth/magic-link/verify', { token })).status).toBe(400)
|
||||
})
|
||||
|
||||
it('answers the same for unknown emails and sends nothing', async () => {
|
||||
const before = ctx.mailer.sent.length
|
||||
expect((await ctx.anonymous().post('/api/auth/magic-link', { email: 'nobody@example.com' })).status).toBe(204)
|
||||
await ctx.notifier.flush()
|
||||
expect(ctx.mailer.sent.length).toBe(before)
|
||||
})
|
||||
|
||||
it("doesn't accept a magic link token as a password reset", async () => {
|
||||
await ctx.anonymous().post('/api/auth/magic-link', { email: 'linked@example.com' })
|
||||
const token = await lastToken('linked@example.com')
|
||||
expect((await ctx.anonymous().post('/api/auth/password-reset/confirm', { token, newPassword: 'a whole new password' })).status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('password reset', () => {
|
||||
it('sets a new password and signs out other sessions', async () => {
|
||||
const user = await ctx.createUser('reporter', 'forgetful@example.com')
|
||||
await ctx.anonymous().post('/api/auth/password-reset', { email: 'forgetful@example.com' })
|
||||
const token = await lastToken('forgetful@example.com')
|
||||
|
||||
const client = ctx.anonymous()
|
||||
const res = await client.post('/api/auth/password-reset/confirm', { token, newPassword: 'remembered at last' })
|
||||
expect(res.status).toBe(200)
|
||||
expect((await client.get('/api/auth/me')).status).toBe(200)
|
||||
expect((await user.get('/api/auth/me')).status).toBe(401)
|
||||
expect((await ctx.anonymous().post('/api/auth/login', { email: 'forgetful@example.com', password: 'remembered at last' })).status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('invitations', () => {
|
||||
it('emails new users without a password a link to set one', async () => {
|
||||
const res = await ctx.admin.post('/api/users', { name: 'New Starter', email: 'starter@example.com', role: 'reporter' })
|
||||
expect(res.body).toMatchObject({ invited: true })
|
||||
await ctx.notifier.flush()
|
||||
const mail = ctx.mailer.last('starter@example.com')
|
||||
expect(mail?.subject).toBe("You've been invited to Northstar Co. on Open SafetyLog EHS")
|
||||
|
||||
// Can't sign in until the password is set
|
||||
expect((await ctx.anonymous().post('/api/auth/login', { email: 'starter@example.com', password: 'anything at all' })).status).toBe(401)
|
||||
const client = ctx.anonymous()
|
||||
expect((await client.post('/api/auth/password-reset/confirm', { token: tokenFrom(mail?.text), newPassword: 'my first password' })).status).toBe(200)
|
||||
expect((await client.get('/api/auth/me')).body.user.email).toBe('starter@example.com')
|
||||
})
|
||||
|
||||
it('lets admins set a password directly when email is not available', async () => {
|
||||
const created = await ctx.admin.post('/api/users', { name: 'No Email', email: 'offline@example.com', role: 'reporter' })
|
||||
await ctx.admin.patch(`/api/users/${created.body.user.id}`, { password: 'set by the admin' })
|
||||
expect((await ctx.anonymous().post('/api/auth/login', { email: 'offline@example.com', password: 'set by the admin' })).status).toBe(200)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user