Implement RBAC with permissions, roles, and route guards
- permission, role, role_permission, user_role_assignment tables - 42 system permissions across 13 domains - 6 default roles: Admin, Manager, Sales Associate, Technician, Instructor, Viewer - Permission inheritance: admin implies edit implies view - requirePermission() Fastify decorator on ALL routes - System permissions and roles seeded per company - Test helpers and API test runner seed RBAC data - All 42 API tests pass with permissions enforced
This commit is contained in:
@@ -72,6 +72,25 @@ async function setupDatabase() {
|
||||
await testSql`INSERT INTO item_condition (company_id, name, slug, description, is_system, sort_order) VALUES (${COMPANY_ID}, ${c.name}, ${c.slug}, ${c.description}, true, ${c.sortOrder})`
|
||||
}
|
||||
|
||||
// Seed RBAC permissions and default roles
|
||||
const { SYSTEM_PERMISSIONS, DEFAULT_ROLES } = await import('../src/db/seeds/rbac.js')
|
||||
for (const p of SYSTEM_PERMISSIONS) {
|
||||
await testSql`INSERT INTO permission (slug, domain, action, description) VALUES (${p.slug}, ${p.domain}, ${p.action}, ${p.description}) ON CONFLICT (slug) DO NOTHING`
|
||||
}
|
||||
|
||||
const permRows = await testSql`SELECT id, slug FROM permission`
|
||||
const permMap = new Map(permRows.map((r: any) => [r.slug, r.id]))
|
||||
|
||||
for (const roleDef of DEFAULT_ROLES) {
|
||||
const [role] = await testSql`INSERT INTO role (company_id, name, slug, description, is_system) VALUES (${COMPANY_ID}, ${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
|
||||
for (const permSlug of roleDef.permissions) {
|
||||
const permId = permMap.get(permSlug)
|
||||
if (permId) {
|
||||
await testSql`INSERT INTO role_permission (role_id, permission_id) VALUES (${role.id}, ${permId})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await testSql.end()
|
||||
console.log(' Database ready')
|
||||
}
|
||||
@@ -128,6 +147,8 @@ async function startBackend(): Promise<Subprocess> {
|
||||
|
||||
// --- Register test user ---
|
||||
async function registerTestUser(): Promise<string> {
|
||||
const testPassword = 'testpassword1234'
|
||||
|
||||
// Register needs x-company-id header
|
||||
const headers = { 'Content-Type': 'application/json', 'x-company-id': COMPANY_ID }
|
||||
const registerRes = await fetch(`${BASE_URL}/v1/auth/register`, {
|
||||
@@ -135,20 +156,29 @@ async function registerTestUser(): Promise<string> {
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
email: 'test@forte.dev',
|
||||
password: 'testpassword123',
|
||||
password: testPassword,
|
||||
firstName: 'Test',
|
||||
lastName: 'Runner',
|
||||
role: 'admin',
|
||||
}),
|
||||
})
|
||||
const registerData = await registerRes.json() as { token?: string }
|
||||
if (registerRes.status === 201 && registerData.token) return registerData.token
|
||||
const registerData = await registerRes.json() as { token?: string; user?: { id: string } }
|
||||
|
||||
// Already exists — login
|
||||
// Assign admin role to the user via direct SQL
|
||||
if (registerRes.status === 201 && registerData.user) {
|
||||
const assignSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`)
|
||||
const [adminRole] = await assignSql`SELECT id FROM role WHERE company_id = ${COMPANY_ID} AND slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
await assignSql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${registerData.user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
|
||||
}
|
||||
await assignSql.end()
|
||||
}
|
||||
|
||||
// Login to get token with permissions loaded
|
||||
const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test@forte.dev', password: 'testpassword123' }),
|
||||
body: JSON.stringify({ email: 'test@forte.dev', password: testPassword }),
|
||||
})
|
||||
const loginData = await loginRes.json() as { token?: string }
|
||||
if (loginRes.status !== 200 || !loginData.token) throw new Error(`Auth failed: ${JSON.stringify(loginData)}`)
|
||||
|
||||
Reference in New Issue
Block a user