Add admin identity module: better-auth, per-app permissions, invitations (#12)
Jenkins Production Deployment
Jenkins Production Deployment
Reviewed-on: #12 Co-authored-by: Patrick Müller <mail@pmueller.me> Co-committed-by: Patrick Müller <mail@pmueller.me>
This commit was merged in pull request #12.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import {expect} from 'vitest';
|
||||
import type {Application} from 'express';
|
||||
import request from 'supertest';
|
||||
import {NachklangAdminDB} from '../../src/models/admin/Admin.db.js';
|
||||
import * as InvitationsService from '../../src/models/admin/invitations/invitations.service.js';
|
||||
import {ACCESS_ROLE, AppName, AppPermission, toPermissions} from '../../src/models/admin/admin.schema.js';
|
||||
|
||||
const db = NachklangAdminDB.db;
|
||||
|
||||
// Dev/test cookie name: advanced.cookiePrefix is 'nachklang', and the __Secure-
|
||||
// prefix is only added over https.
|
||||
export const SESSION_COOKIE = 'nachklang.session_token';
|
||||
|
||||
/**
|
||||
* Wipes every table between test files. Child tables first - the FKs to `user`
|
||||
* are ON DELETE CASCADE, but the rest are not.
|
||||
*
|
||||
* `rateLimit` matters more than it looks: the limiter is enabled during the
|
||||
* suite, and better-auth caps /sign-in/* at 3 requests per 10 seconds. All
|
||||
* tests resolve to the same client IP, so they share one bucket - without this
|
||||
* reset the suite would start failing with 429s that look like auth bugs as
|
||||
* soon as a third sign-in assertion is added.
|
||||
*/
|
||||
export const resetDatabase = async (): Promise<void> => {
|
||||
await db.deleteFrom('session').execute();
|
||||
await db.deleteFrom('user_app_permissions').execute();
|
||||
await db.deleteFrom('passkey').execute();
|
||||
await db.deleteFrom('invitations').execute();
|
||||
await db.deleteFrom('verification').execute();
|
||||
await db.deleteFrom('rateLimit').execute();
|
||||
await db.deleteFrom('user').execute();
|
||||
};
|
||||
|
||||
export const closeDatabase = async (): Promise<void> => {
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an invitation straight through the service (so the test gets the raw
|
||||
* token, which the API deliberately never returns) and redeems it through the
|
||||
* public endpoint. Returns an agent that carries the resulting session cookie.
|
||||
*/
|
||||
export const createAndAcceptInvitation = async (
|
||||
app: Application,
|
||||
email: string,
|
||||
name: string,
|
||||
// Takes the shorthand as well as the full form: most tests only care that
|
||||
// someone can open an app, and `['tickets']` says that with less noise.
|
||||
grants: (AppName | AppPermission)[],
|
||||
password = 'devpassword123'
|
||||
) => {
|
||||
const permissions = toPermissions(grants) ?? [];
|
||||
const invitation = await InvitationsService.createInvitation(email, name, permissions, null);
|
||||
|
||||
const agent = request.agent(app);
|
||||
const res = await agent
|
||||
.post('/admin/auth/invitations/accept')
|
||||
.send({token: invitation.token, password});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
return {agent, userId: res.body.user.id, token: invitation.token};
|
||||
};
|
||||
|
||||
export const cookieHeader = (res: request.Response): string[] => {
|
||||
const raw = res.headers['set-cookie'];
|
||||
return Array.isArray(raw) ? raw : raw ? [raw] : [];
|
||||
};
|
||||
|
||||
export const sessionCookieFrom = (res: request.Response): string | undefined => {
|
||||
return cookieHeader(res).find(cookie => cookie.startsWith(SESSION_COOKIE));
|
||||
};
|
||||
|
||||
/** `accessTo('feedback')` reads better than the (app, role) literal in tests
|
||||
* that only care that someone can open an app. */
|
||||
export const accessTo = (...apps: AppName[]): AppPermission[] => {
|
||||
return apps.map(app => ({app, role: ACCESS_ROLE}));
|
||||
};
|
||||
Reference in New Issue
Block a user