7aac07a013
Introduces src/models/admin/, a dedicated identity and permissions module on its own nachklang_admin database, and the shared authenticator that feedback and tickets will move onto in the cutover step. Nothing swaps over yet: feedback.auth.ts and tickets.auth.ts still authenticate against the legacy calendar sessions, so production behaviour is unchanged. - better-auth 1.7 mounted at /admin/auth/*, sessions as httpOnly cookies scoped to .nachklang.art so one sign-in covers every *.nachklang.art app. - Accounts are invite-only: public sign-up is disabled, and the invitations plugin is the only code that creates users. Tokens are stored as SHA-256 hashes and travel in the request body, never in a URL. - Per-app permissions in user_app_permissions; requireAppAccess(app) queries the database on every request (no cookie cache) so disabling a user or revoking a session takes effect immediately. - ADMIN_BOOTSTRAP_EMAIL guarantees a way in on an empty database, idempotently and without crashing the API if the database is unreachable at boot. - Guards prevent an admin from removing their own admin permission, disabling themselves, or stripping the last active admin. The admin pool uses the callback-style mysql2, not mysql2/promise: Kysely's MysqlDialect drives the pool with callbacks, and the promise wrapper ignores them, so every query hangs silently. Only the integration tests caught this. Schema in sql/admin/001_init.sql, derived from getAuthTables() on the installed better-auth rather than the published CLI, which lags the library and omits account.issuer. app.ts is split into src/app.factory.ts so the integration tests drive the real middleware order rather than a copy of it. Tests: 131 unit, plus 41 integration tests against a throwaway MariaDB started by test/integration/setup.ts (docker or podman). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
1.5 KiB
JSON
67 lines
1.5 KiB
JSON
{
|
|
"name": "nachklang-api",
|
|
"version": "0.1.0",
|
|
"description": "",
|
|
"main": "index.js",
|
|
"type": "module",
|
|
"engines": {
|
|
"node": ">=26"
|
|
},
|
|
"scripts": {
|
|
"start": "tsc && node ./dist/app.js",
|
|
"build": "tsc",
|
|
"debug": "export DEBUG=* && npm run start",
|
|
"test": "vitest run --coverage",
|
|
"test:watch": "vitest",
|
|
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
},
|
|
"keywords": [],
|
|
"author": "",
|
|
"license": "ISC",
|
|
"dependencies": {
|
|
"@better-auth/passkey": "^1.7.2",
|
|
"app-root-path": "^3.0.0",
|
|
"axios": "^1.20.0",
|
|
"bcrypt": "^5.0.1",
|
|
"better-auth": "^1.7.2",
|
|
"cors": "^2.8.5",
|
|
"debug": "^4.3.1",
|
|
"dotenv": "^16.6.1",
|
|
"express": "^4.18.2",
|
|
"guid-typescript": "^1.0.9",
|
|
"kysely": "^0.29.5",
|
|
"mariadb": "^3.0.2",
|
|
"mysql2": "^3.24.3",
|
|
"random-words": "^1.1.1",
|
|
"swagger-jsdoc": "^6.1.0",
|
|
"swagger-ui-express": "^4.3.0",
|
|
"winston": "^3.3.3",
|
|
"zod": "^4.5.4"
|
|
},
|
|
"devDependencies": {
|
|
"@types/app-root-path": "^1.2.4",
|
|
"@types/bcrypt": "^3.0.1",
|
|
"@types/cors": "^2.8.19",
|
|
"@types/debug": "^4.1.5",
|
|
"@types/express": "^4.17.15",
|
|
"@types/node": "^26.4.1",
|
|
"@types/random-words": "^1.1.2",
|
|
"@types/supertest": "^7.2.1",
|
|
"@types/swagger-jsdoc": "^6.0.1",
|
|
"@types/swagger-ui-express": "^4.1.3",
|
|
"@types/winston": "^2.4.4",
|
|
"@vitest/coverage-v8": "^5.0.0",
|
|
"is-number": "^7.0.0",
|
|
"source-map-support": "^0.5.19",
|
|
"supertest": "^7.2.2",
|
|
"typescript": "^5.9.3",
|
|
"vitest": "^5.0.0",
|
|
"vitest-sonar-reporter": "^3.0.0"
|
|
},
|
|
"overrides": {
|
|
"better-auth": {
|
|
"vitest": "$vitest"
|
|
}
|
|
}
|
|
}
|