@enderworld/onlyapi 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +201 -0
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/dist/cli.js +14 -0
- package/package.json +69 -0
- package/src/application/dtos/admin.dto.ts +25 -0
- package/src/application/dtos/auth.dto.ts +97 -0
- package/src/application/dtos/index.ts +40 -0
- package/src/application/index.ts +2 -0
- package/src/application/services/admin.service.ts +150 -0
- package/src/application/services/api-key.service.ts +65 -0
- package/src/application/services/auth.service.ts +606 -0
- package/src/application/services/health.service.ts +97 -0
- package/src/application/services/index.ts +10 -0
- package/src/application/services/user.service.ts +95 -0
- package/src/cli/commands/help.ts +86 -0
- package/src/cli/commands/init.ts +301 -0
- package/src/cli/commands/upgrade.ts +471 -0
- package/src/cli/index.ts +76 -0
- package/src/cli/ui.ts +189 -0
- package/src/cluster.ts +62 -0
- package/src/core/entities/index.ts +1 -0
- package/src/core/entities/user.entity.ts +24 -0
- package/src/core/errors/app-error.ts +81 -0
- package/src/core/errors/index.ts +15 -0
- package/src/core/index.ts +7 -0
- package/src/core/ports/account-lockout.ts +15 -0
- package/src/core/ports/alert-sink.ts +27 -0
- package/src/core/ports/api-key.ts +37 -0
- package/src/core/ports/audit-log.ts +46 -0
- package/src/core/ports/cache.ts +24 -0
- package/src/core/ports/circuit-breaker.ts +42 -0
- package/src/core/ports/event-bus.ts +78 -0
- package/src/core/ports/index.ts +62 -0
- package/src/core/ports/job-queue.ts +73 -0
- package/src/core/ports/logger.ts +21 -0
- package/src/core/ports/metrics.ts +49 -0
- package/src/core/ports/oauth.ts +55 -0
- package/src/core/ports/password-hasher.ts +10 -0
- package/src/core/ports/password-history.ts +23 -0
- package/src/core/ports/password-policy.ts +43 -0
- package/src/core/ports/refresh-token-store.ts +37 -0
- package/src/core/ports/retry.ts +23 -0
- package/src/core/ports/token-blacklist.ts +16 -0
- package/src/core/ports/token-service.ts +23 -0
- package/src/core/ports/totp-service.ts +16 -0
- package/src/core/ports/user.repository.ts +40 -0
- package/src/core/ports/verification-token.ts +41 -0
- package/src/core/ports/webhook.ts +58 -0
- package/src/core/types/brand.ts +19 -0
- package/src/core/types/index.ts +19 -0
- package/src/core/types/pagination.ts +28 -0
- package/src/core/types/result.ts +52 -0
- package/src/infrastructure/alerting/index.ts +1 -0
- package/src/infrastructure/alerting/webhook.ts +100 -0
- package/src/infrastructure/cache/in-memory-cache.ts +111 -0
- package/src/infrastructure/cache/index.ts +6 -0
- package/src/infrastructure/cache/redis-cache.ts +204 -0
- package/src/infrastructure/config/config.ts +185 -0
- package/src/infrastructure/config/index.ts +1 -0
- package/src/infrastructure/database/in-memory-user.repository.ts +134 -0
- package/src/infrastructure/database/index.ts +37 -0
- package/src/infrastructure/database/migrations/001_create_users.ts +26 -0
- package/src/infrastructure/database/migrations/002_create_token_blacklist.ts +21 -0
- package/src/infrastructure/database/migrations/003_create_audit_log.ts +31 -0
- package/src/infrastructure/database/migrations/004_auth_platform.ts +112 -0
- package/src/infrastructure/database/migrations/runner.ts +120 -0
- package/src/infrastructure/database/mssql/index.ts +14 -0
- package/src/infrastructure/database/mssql/migrations.ts +299 -0
- package/src/infrastructure/database/mssql/mssql-account-lockout.ts +95 -0
- package/src/infrastructure/database/mssql/mssql-api-keys.ts +146 -0
- package/src/infrastructure/database/mssql/mssql-audit-log.ts +86 -0
- package/src/infrastructure/database/mssql/mssql-oauth-accounts.ts +118 -0
- package/src/infrastructure/database/mssql/mssql-password-history.ts +71 -0
- package/src/infrastructure/database/mssql/mssql-refresh-token-store.ts +144 -0
- package/src/infrastructure/database/mssql/mssql-token-blacklist.ts +54 -0
- package/src/infrastructure/database/mssql/mssql-user.repository.ts +263 -0
- package/src/infrastructure/database/mssql/mssql-verification-tokens.ts +120 -0
- package/src/infrastructure/database/postgres/index.ts +14 -0
- package/src/infrastructure/database/postgres/migrations.ts +235 -0
- package/src/infrastructure/database/postgres/pg-account-lockout.ts +75 -0
- package/src/infrastructure/database/postgres/pg-api-keys.ts +126 -0
- package/src/infrastructure/database/postgres/pg-audit-log.ts +74 -0
- package/src/infrastructure/database/postgres/pg-oauth-accounts.ts +101 -0
- package/src/infrastructure/database/postgres/pg-password-history.ts +61 -0
- package/src/infrastructure/database/postgres/pg-refresh-token-store.ts +117 -0
- package/src/infrastructure/database/postgres/pg-token-blacklist.ts +48 -0
- package/src/infrastructure/database/postgres/pg-user.repository.ts +237 -0
- package/src/infrastructure/database/postgres/pg-verification-tokens.ts +97 -0
- package/src/infrastructure/database/sqlite-account-lockout.ts +97 -0
- package/src/infrastructure/database/sqlite-api-keys.ts +155 -0
- package/src/infrastructure/database/sqlite-audit-log.ts +90 -0
- package/src/infrastructure/database/sqlite-oauth-accounts.ts +105 -0
- package/src/infrastructure/database/sqlite-password-history.ts +54 -0
- package/src/infrastructure/database/sqlite-refresh-token-store.ts +122 -0
- package/src/infrastructure/database/sqlite-token-blacklist.ts +47 -0
- package/src/infrastructure/database/sqlite-user.repository.ts +260 -0
- package/src/infrastructure/database/sqlite-verification-tokens.ts +112 -0
- package/src/infrastructure/events/event-bus.ts +105 -0
- package/src/infrastructure/events/event-factory.ts +31 -0
- package/src/infrastructure/events/in-memory-webhook-registry.ts +67 -0
- package/src/infrastructure/events/index.ts +4 -0
- package/src/infrastructure/events/webhook-dispatcher.ts +114 -0
- package/src/infrastructure/index.ts +58 -0
- package/src/infrastructure/jobs/index.ts +1 -0
- package/src/infrastructure/jobs/job-queue.ts +185 -0
- package/src/infrastructure/logging/index.ts +1 -0
- package/src/infrastructure/logging/logger.ts +63 -0
- package/src/infrastructure/metrics/index.ts +1 -0
- package/src/infrastructure/metrics/prometheus.ts +231 -0
- package/src/infrastructure/oauth/github.ts +116 -0
- package/src/infrastructure/oauth/google.ts +83 -0
- package/src/infrastructure/oauth/index.ts +2 -0
- package/src/infrastructure/resilience/circuit-breaker.ts +133 -0
- package/src/infrastructure/resilience/index.ts +2 -0
- package/src/infrastructure/resilience/retry.ts +50 -0
- package/src/infrastructure/security/account-lockout.ts +73 -0
- package/src/infrastructure/security/index.ts +6 -0
- package/src/infrastructure/security/password-hasher.ts +31 -0
- package/src/infrastructure/security/password-policy.ts +77 -0
- package/src/infrastructure/security/token-blacklist.ts +45 -0
- package/src/infrastructure/security/token-service.ts +144 -0
- package/src/infrastructure/security/totp-service.ts +142 -0
- package/src/infrastructure/tracing/index.ts +7 -0
- package/src/infrastructure/tracing/trace-context.ts +93 -0
- package/src/main.ts +479 -0
- package/src/presentation/context.ts +26 -0
- package/src/presentation/handlers/admin.handler.ts +114 -0
- package/src/presentation/handlers/api-key.handler.ts +68 -0
- package/src/presentation/handlers/auth.handler.ts +218 -0
- package/src/presentation/handlers/health.handler.ts +27 -0
- package/src/presentation/handlers/index.ts +15 -0
- package/src/presentation/handlers/metrics.handler.ts +21 -0
- package/src/presentation/handlers/oauth.handler.ts +61 -0
- package/src/presentation/handlers/openapi.handler.ts +543 -0
- package/src/presentation/handlers/response.ts +29 -0
- package/src/presentation/handlers/sse.handler.ts +165 -0
- package/src/presentation/handlers/user.handler.ts +81 -0
- package/src/presentation/handlers/webhook.handler.ts +92 -0
- package/src/presentation/handlers/websocket.handler.ts +226 -0
- package/src/presentation/i18n/index.ts +254 -0
- package/src/presentation/index.ts +5 -0
- package/src/presentation/middleware/api-key.ts +18 -0
- package/src/presentation/middleware/auth.ts +39 -0
- package/src/presentation/middleware/cors.ts +41 -0
- package/src/presentation/middleware/index.ts +12 -0
- package/src/presentation/middleware/rate-limit.ts +65 -0
- package/src/presentation/middleware/security-headers.ts +18 -0
- package/src/presentation/middleware/validate.ts +16 -0
- package/src/presentation/middleware/versioning.ts +69 -0
- package/src/presentation/routes/index.ts +1 -0
- package/src/presentation/routes/router.ts +272 -0
- package/src/presentation/server.ts +381 -0
- package/src/shared/cli.ts +294 -0
- package/src/shared/container.ts +65 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/log-format.ts +148 -0
- package/src/shared/utils/id.ts +5 -0
- package/src/shared/utils/index.ts +2 -0
- package/src/shared/utils/timing-safe.ts +20 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import sql from "mssql";
|
|
5
|
+
import { createAdminService } from "./application/services/admin.service.js";
|
|
6
|
+
import { createApiKeyService } from "./application/services/api-key.service.js";
|
|
7
|
+
import { createAuthService } from "./application/services/auth.service.js";
|
|
8
|
+
import { createHealthService } from "./application/services/health.service.js";
|
|
9
|
+
import { createUserService } from "./application/services/user.service.js";
|
|
10
|
+
import { AlertLevel } from "./core/ports/alert-sink.js";
|
|
11
|
+
import type { Cache } from "./core/ports/cache.js";
|
|
12
|
+
import { CircuitState } from "./core/ports/circuit-breaker.js";
|
|
13
|
+
import type { OAuthProvider } from "./core/ports/oauth.js";
|
|
14
|
+
import { createNoopAlertSink, createWebhookAlertSink } from "./infrastructure/alerting/webhook.js";
|
|
15
|
+
import { createInMemoryCache } from "./infrastructure/cache/in-memory-cache.js";
|
|
16
|
+
import { createRedisCache } from "./infrastructure/cache/redis-cache.js";
|
|
17
|
+
import { loadConfig } from "./infrastructure/config/config.js";
|
|
18
|
+
import { migrateUp } from "./infrastructure/database/migrations/runner.js";
|
|
19
|
+
import {
|
|
20
|
+
createMssqlAccountLockout,
|
|
21
|
+
createMssqlApiKeyRepository,
|
|
22
|
+
createMssqlAuditLog,
|
|
23
|
+
createMssqlOAuthAccountRepo,
|
|
24
|
+
createMssqlPasswordHistory,
|
|
25
|
+
createMssqlRefreshTokenStore,
|
|
26
|
+
createMssqlTokenBlacklist,
|
|
27
|
+
createMssqlUserRepository,
|
|
28
|
+
createMssqlVerificationTokenRepo,
|
|
29
|
+
mssqlMigrateUp,
|
|
30
|
+
} from "./infrastructure/database/mssql/index.js";
|
|
31
|
+
import {
|
|
32
|
+
createPgAccountLockout,
|
|
33
|
+
createPgApiKeyRepository,
|
|
34
|
+
createPgAuditLog,
|
|
35
|
+
createPgOAuthAccountRepo,
|
|
36
|
+
createPgPasswordHistory,
|
|
37
|
+
createPgRefreshTokenStore,
|
|
38
|
+
createPgTokenBlacklist,
|
|
39
|
+
createPgUserRepository,
|
|
40
|
+
createPgVerificationTokenRepo,
|
|
41
|
+
pgMigrateUp,
|
|
42
|
+
} from "./infrastructure/database/postgres/index.js";
|
|
43
|
+
import { createSqliteAccountLockout } from "./infrastructure/database/sqlite-account-lockout.js";
|
|
44
|
+
import { createSqliteApiKeyRepository } from "./infrastructure/database/sqlite-api-keys.js";
|
|
45
|
+
import { createSqliteAuditLog } from "./infrastructure/database/sqlite-audit-log.js";
|
|
46
|
+
import { createSqliteOAuthAccountRepo } from "./infrastructure/database/sqlite-oauth-accounts.js";
|
|
47
|
+
import { createSqlitePasswordHistory } from "./infrastructure/database/sqlite-password-history.js";
|
|
48
|
+
import { createSqliteRefreshTokenStore } from "./infrastructure/database/sqlite-refresh-token-store.js";
|
|
49
|
+
import { createSqliteTokenBlacklist } from "./infrastructure/database/sqlite-token-blacklist.js";
|
|
50
|
+
import { createSqliteUserRepository } from "./infrastructure/database/sqlite-user.repository.js";
|
|
51
|
+
import { createSqliteVerificationTokenRepo } from "./infrastructure/database/sqlite-verification-tokens.js";
|
|
52
|
+
import { createEventBus } from "./infrastructure/events/event-bus.js";
|
|
53
|
+
import { createDomainEventFactory } from "./infrastructure/events/event-factory.js";
|
|
54
|
+
import { createInMemoryWebhookRegistry } from "./infrastructure/events/in-memory-webhook-registry.js";
|
|
55
|
+
import { createWebhookDispatcher } from "./infrastructure/events/webhook-dispatcher.js";
|
|
56
|
+
import { createInMemoryJobQueue } from "./infrastructure/jobs/job-queue.js";
|
|
57
|
+
import { createLogger } from "./infrastructure/logging/logger.js";
|
|
58
|
+
import { createMetricsCollector } from "./infrastructure/metrics/prometheus.js";
|
|
59
|
+
import { createGitHubOAuthProvider } from "./infrastructure/oauth/github.js";
|
|
60
|
+
import { createGoogleOAuthProvider } from "./infrastructure/oauth/google.js";
|
|
61
|
+
import { createCircuitBreaker } from "./infrastructure/resilience/circuit-breaker.js";
|
|
62
|
+
import { createPasswordHasher } from "./infrastructure/security/password-hasher.js";
|
|
63
|
+
import { createPasswordPolicy } from "./infrastructure/security/password-policy.js";
|
|
64
|
+
import { createTokenService } from "./infrastructure/security/token-service.js";
|
|
65
|
+
import { createTotpService } from "./infrastructure/security/totp-service.js";
|
|
66
|
+
import { createWebSocketManager } from "./presentation/handlers/websocket.handler.js";
|
|
67
|
+
import { createRouter } from "./presentation/routes/router.js";
|
|
68
|
+
import { createServer } from "./presentation/server.js";
|
|
69
|
+
import { printShutdown, printStartupBanner } from "./shared/cli.js";
|
|
70
|
+
import { Container, Tokens } from "./shared/container.js";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Bootstrap — compose the entire dependency graph, then start the server.
|
|
74
|
+
* Single entry point, fail-fast on misconfiguration.
|
|
75
|
+
*/
|
|
76
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: bootstrap wires all drivers/services — inherently branchy
|
|
77
|
+
const bootstrap = async () => {
|
|
78
|
+
const bootStart = performance.now();
|
|
79
|
+
|
|
80
|
+
// 1. Config (validated, fails fast)
|
|
81
|
+
const config = loadConfig();
|
|
82
|
+
|
|
83
|
+
// 2. Infrastructure
|
|
84
|
+
const logger = createLogger(config.log.level, {}, config.log.format);
|
|
85
|
+
const passwordHasher = createPasswordHasher();
|
|
86
|
+
const tokenService = createTokenService(config.jwt);
|
|
87
|
+
|
|
88
|
+
// 3. Database — driver-based adapter selection
|
|
89
|
+
let db: Database | undefined;
|
|
90
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun.sql tagged template type
|
|
91
|
+
let pgSql: any | undefined;
|
|
92
|
+
let mssqlPool: sql.ConnectionPool | undefined;
|
|
93
|
+
let userRepo: ReturnType<typeof createSqliteUserRepository>;
|
|
94
|
+
let tokenBlacklist: ReturnType<typeof createSqliteTokenBlacklist>;
|
|
95
|
+
let accountLockout: ReturnType<typeof createSqliteAccountLockout>;
|
|
96
|
+
let auditLog: ReturnType<typeof createSqliteAuditLog>;
|
|
97
|
+
let verificationTokens: ReturnType<typeof createSqliteVerificationTokenRepo>;
|
|
98
|
+
let refreshTokenStore: ReturnType<typeof createSqliteRefreshTokenStore>;
|
|
99
|
+
let apiKeyRepo: ReturnType<typeof createSqliteApiKeyRepository>;
|
|
100
|
+
let passwordHistory: ReturnType<typeof createSqlitePasswordHistory>;
|
|
101
|
+
let oauthAccounts: ReturnType<typeof createSqliteOAuthAccountRepo>;
|
|
102
|
+
|
|
103
|
+
if (config.database.driver === "postgres") {
|
|
104
|
+
// Postgres via Bun.sql (zero-dep, native driver)
|
|
105
|
+
const dbUrl = config.database.url;
|
|
106
|
+
if (!dbUrl) {
|
|
107
|
+
logger.fatal("DATABASE_URL is required when DATABASE_DRIVER=postgres");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun.sql dynamic URL config
|
|
111
|
+
pgSql = new (Bun as any).SQL({ url: dbUrl });
|
|
112
|
+
logger.info("Postgres database connecting", { url: dbUrl.replace(/:[^:@]+@/, ":***@") });
|
|
113
|
+
|
|
114
|
+
await pgMigrateUp(pgSql, logger);
|
|
115
|
+
|
|
116
|
+
userRepo = createPgUserRepository(pgSql);
|
|
117
|
+
tokenBlacklist = createPgTokenBlacklist(pgSql);
|
|
118
|
+
accountLockout = createPgAccountLockout(pgSql, {
|
|
119
|
+
maxAttempts: config.lockout.maxAttempts,
|
|
120
|
+
lockoutDurationMs: config.lockout.durationMs,
|
|
121
|
+
});
|
|
122
|
+
auditLog = createPgAuditLog(pgSql);
|
|
123
|
+
verificationTokens = createPgVerificationTokenRepo(pgSql);
|
|
124
|
+
refreshTokenStore = createPgRefreshTokenStore(pgSql);
|
|
125
|
+
apiKeyRepo = createPgApiKeyRepository(pgSql);
|
|
126
|
+
passwordHistory = createPgPasswordHistory(pgSql);
|
|
127
|
+
oauthAccounts = createPgOAuthAccountRepo(pgSql);
|
|
128
|
+
logger.info("Postgres adapters initialized");
|
|
129
|
+
} else if (config.database.driver === "mssql") {
|
|
130
|
+
// SQL Server via mssql npm package (tedious TDS protocol)
|
|
131
|
+
const dbUrl = config.database.url;
|
|
132
|
+
if (!dbUrl) {
|
|
133
|
+
logger.fatal("DATABASE_URL is required when DATABASE_DRIVER=mssql");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
mssqlPool = await sql.connect(dbUrl);
|
|
137
|
+
logger.info("SQL Server database connected", {
|
|
138
|
+
url: dbUrl.replace(/:[^:@]+@/, ":***@"),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await mssqlMigrateUp(mssqlPool, logger);
|
|
142
|
+
|
|
143
|
+
userRepo = createMssqlUserRepository(mssqlPool);
|
|
144
|
+
tokenBlacklist = createMssqlTokenBlacklist(mssqlPool);
|
|
145
|
+
accountLockout = createMssqlAccountLockout(mssqlPool, {
|
|
146
|
+
maxAttempts: config.lockout.maxAttempts,
|
|
147
|
+
lockoutDurationMs: config.lockout.durationMs,
|
|
148
|
+
});
|
|
149
|
+
auditLog = createMssqlAuditLog(mssqlPool);
|
|
150
|
+
verificationTokens = createMssqlVerificationTokenRepo(mssqlPool);
|
|
151
|
+
refreshTokenStore = createMssqlRefreshTokenStore(mssqlPool);
|
|
152
|
+
apiKeyRepo = createMssqlApiKeyRepository(mssqlPool);
|
|
153
|
+
passwordHistory = createMssqlPasswordHistory(mssqlPool);
|
|
154
|
+
oauthAccounts = createMssqlOAuthAccountRepo(mssqlPool);
|
|
155
|
+
logger.info("SQL Server adapters initialized");
|
|
156
|
+
} else {
|
|
157
|
+
// SQLite (default — zero deps, bun:sqlite built-in)
|
|
158
|
+
const dbPath = config.database.path;
|
|
159
|
+
const dbDir = dirname(dbPath);
|
|
160
|
+
if (!existsSync(dbDir)) {
|
|
161
|
+
mkdirSync(dbDir, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
db = new Database(dbPath, { create: true });
|
|
164
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
165
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
166
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
167
|
+
logger.info("SQLite database opened", { path: dbPath });
|
|
168
|
+
|
|
169
|
+
await migrateUp(db, logger);
|
|
170
|
+
|
|
171
|
+
userRepo = createSqliteUserRepository(db);
|
|
172
|
+
tokenBlacklist = createSqliteTokenBlacklist(db);
|
|
173
|
+
accountLockout = createSqliteAccountLockout(db, {
|
|
174
|
+
maxAttempts: config.lockout.maxAttempts,
|
|
175
|
+
lockoutDurationMs: config.lockout.durationMs,
|
|
176
|
+
});
|
|
177
|
+
auditLog = createSqliteAuditLog(db);
|
|
178
|
+
verificationTokens = createSqliteVerificationTokenRepo(db);
|
|
179
|
+
refreshTokenStore = createSqliteRefreshTokenStore(db);
|
|
180
|
+
apiKeyRepo = createSqliteApiKeyRepository(db);
|
|
181
|
+
passwordHistory = createSqlitePasswordHistory(db);
|
|
182
|
+
oauthAccounts = createSqliteOAuthAccountRepo(db);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 4. Cache — driver-based adapter selection
|
|
186
|
+
let cache: Cache;
|
|
187
|
+
|
|
188
|
+
if (config.redis.enabled) {
|
|
189
|
+
cache = await createRedisCache({
|
|
190
|
+
config: {
|
|
191
|
+
host: config.redis.host,
|
|
192
|
+
port: config.redis.port,
|
|
193
|
+
password: config.redis.password,
|
|
194
|
+
db: config.redis.db,
|
|
195
|
+
},
|
|
196
|
+
logger: logger.child({ service: "redis" }),
|
|
197
|
+
});
|
|
198
|
+
logger.info("Redis cache connected", { host: config.redis.host, port: config.redis.port });
|
|
199
|
+
} else {
|
|
200
|
+
cache = createInMemoryCache();
|
|
201
|
+
logger.info("In-memory cache initialized");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 5. Security services
|
|
205
|
+
const totpService = createTotpService();
|
|
206
|
+
const passwordPolicy = createPasswordPolicy(config.passwordPolicy);
|
|
207
|
+
|
|
208
|
+
// 5a. OAuth providers (only registered when configured)
|
|
209
|
+
const oauthProviders = new Map<string, OAuthProvider>();
|
|
210
|
+
if (config.oauth.googleClientId && config.oauth.googleClientSecret) {
|
|
211
|
+
oauthProviders.set(
|
|
212
|
+
"google",
|
|
213
|
+
createGoogleOAuthProvider({
|
|
214
|
+
clientId: config.oauth.googleClientId,
|
|
215
|
+
clientSecret: config.oauth.googleClientSecret,
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
logger.info("OAuth provider registered: google");
|
|
219
|
+
}
|
|
220
|
+
if (config.oauth.githubClientId && config.oauth.githubClientSecret) {
|
|
221
|
+
oauthProviders.set(
|
|
222
|
+
"github",
|
|
223
|
+
createGitHubOAuthProvider({
|
|
224
|
+
clientId: config.oauth.githubClientId,
|
|
225
|
+
clientSecret: config.oauth.githubClientSecret,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
logger.info("OAuth provider registered: github");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 5b. Event system, webhooks, job queue
|
|
232
|
+
const eventBus = createEventBus({ logger: logger.child({ service: "event-bus" }) });
|
|
233
|
+
const eventFactory = createDomainEventFactory();
|
|
234
|
+
const webhookRegistry = createInMemoryWebhookRegistry();
|
|
235
|
+
const webhookDispatcher = createWebhookDispatcher({
|
|
236
|
+
registry: webhookRegistry,
|
|
237
|
+
logger: logger.child({ service: "webhook" }),
|
|
238
|
+
});
|
|
239
|
+
const jobQueue = createInMemoryJobQueue({
|
|
240
|
+
logger: logger.child({ service: "job-queue" }),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Wire webhook dispatcher as a wildcard event subscriber
|
|
244
|
+
eventBus.subscribeAll((event) => webhookDispatcher.dispatch(event));
|
|
245
|
+
|
|
246
|
+
// 6. Register in DI container
|
|
247
|
+
Container.register(Tokens.Logger, logger);
|
|
248
|
+
Container.register(Tokens.Config, config);
|
|
249
|
+
Container.register(Tokens.Database, db);
|
|
250
|
+
Container.register(Tokens.PasswordHasher, passwordHasher);
|
|
251
|
+
Container.register(Tokens.TokenService, tokenService);
|
|
252
|
+
Container.register(Tokens.TokenBlacklist, tokenBlacklist);
|
|
253
|
+
Container.register(Tokens.AccountLockout, accountLockout);
|
|
254
|
+
Container.register(Tokens.UserRepository, userRepo);
|
|
255
|
+
Container.register(Tokens.AuditLog, auditLog);
|
|
256
|
+
Container.register(Tokens.VerificationTokenRepository, verificationTokens);
|
|
257
|
+
Container.register(Tokens.RefreshTokenStore, refreshTokenStore);
|
|
258
|
+
Container.register(Tokens.ApiKeyRepository, apiKeyRepo);
|
|
259
|
+
Container.register(Tokens.PasswordHistory, passwordHistory);
|
|
260
|
+
Container.register(Tokens.PasswordPolicy, passwordPolicy);
|
|
261
|
+
Container.register(Tokens.TotpService, totpService);
|
|
262
|
+
Container.register(Tokens.OAuthProviders, oauthProviders);
|
|
263
|
+
Container.register(Tokens.OAuthAccountRepository, oauthAccounts);
|
|
264
|
+
|
|
265
|
+
// 6b. Cache
|
|
266
|
+
Container.register(Tokens.Cache, cache);
|
|
267
|
+
|
|
268
|
+
// 6c. Event, webhook, and job queue registrations
|
|
269
|
+
Container.register(Tokens.EventBus, eventBus);
|
|
270
|
+
Container.register(Tokens.EventFactory, eventFactory);
|
|
271
|
+
Container.register(Tokens.WebhookRegistry, webhookRegistry);
|
|
272
|
+
Container.register(Tokens.WebhookDispatcher, webhookDispatcher);
|
|
273
|
+
Container.register(Tokens.JobQueue, jobQueue);
|
|
274
|
+
|
|
275
|
+
// 6d. Observability — metrics collector
|
|
276
|
+
const metricsCollector = createMetricsCollector();
|
|
277
|
+
Container.register(Tokens.MetricsCollector, metricsCollector);
|
|
278
|
+
|
|
279
|
+
// 6e. Alerting — webhook alert sink (or noop if no URL configured)
|
|
280
|
+
const alertSink = config.alerting.webhookUrl
|
|
281
|
+
? createWebhookAlertSink({
|
|
282
|
+
url: config.alerting.webhookUrl,
|
|
283
|
+
timeoutMs: config.alerting.timeoutMs,
|
|
284
|
+
logger: logger.child({ service: "alerting" }),
|
|
285
|
+
})
|
|
286
|
+
: createNoopAlertSink();
|
|
287
|
+
Container.register(Tokens.AlertSink, alertSink);
|
|
288
|
+
|
|
289
|
+
// 6f. Resilience — circuit breaker for database operations
|
|
290
|
+
const dbCircuitBreaker = createCircuitBreaker({
|
|
291
|
+
name: "database",
|
|
292
|
+
failureThreshold: config.circuitBreaker.failureThreshold,
|
|
293
|
+
resetTimeoutMs: config.circuitBreaker.resetTimeoutMs,
|
|
294
|
+
halfOpenSuccessThreshold: config.circuitBreaker.halfOpenSuccessThreshold,
|
|
295
|
+
onStateChange: (cbName, from, to) => {
|
|
296
|
+
logger.warn("Circuit breaker state changed", {
|
|
297
|
+
circuitBreaker: cbName,
|
|
298
|
+
from,
|
|
299
|
+
to,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Update metrics gauge: 0=closed, 1=half_open, 2=open
|
|
303
|
+
const stateValue = to === CircuitState.CLOSED ? 0 : to === CircuitState.HALF_OPEN ? 1 : 2;
|
|
304
|
+
metricsCollector.circuitBreakerState.set(stateValue, { name: cbName });
|
|
305
|
+
|
|
306
|
+
// Fire alerting hooks on state changes
|
|
307
|
+
if (to === CircuitState.OPEN) {
|
|
308
|
+
alertSink.send({
|
|
309
|
+
level: AlertLevel.CRITICAL,
|
|
310
|
+
title: `Circuit breaker OPEN: ${cbName}`,
|
|
311
|
+
message: `Circuit breaker "${cbName}" has opened after reaching failure threshold. Requests will be short-circuited.`,
|
|
312
|
+
timestamp: new Date().toISOString(),
|
|
313
|
+
source: "onlyApi",
|
|
314
|
+
metadata: { circuitBreaker: cbName, from, to },
|
|
315
|
+
});
|
|
316
|
+
metricsCollector.alertsSentTotal.inc({ level: "critical" });
|
|
317
|
+
} else if (to === CircuitState.CLOSED && from === CircuitState.HALF_OPEN) {
|
|
318
|
+
alertSink.send({
|
|
319
|
+
level: AlertLevel.RESOLVED,
|
|
320
|
+
title: `Circuit breaker CLOSED: ${cbName}`,
|
|
321
|
+
message: `Circuit breaker "${cbName}" has recovered and returned to normal operation.`,
|
|
322
|
+
timestamp: new Date().toISOString(),
|
|
323
|
+
source: "onlyApi",
|
|
324
|
+
metadata: { circuitBreaker: cbName, from, to },
|
|
325
|
+
});
|
|
326
|
+
metricsCollector.alertsSentTotal.inc({ level: "resolved" });
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// 7. Application services
|
|
332
|
+
const authService = createAuthService({
|
|
333
|
+
userRepo,
|
|
334
|
+
passwordHasher,
|
|
335
|
+
tokenService,
|
|
336
|
+
tokenBlacklist,
|
|
337
|
+
accountLockout,
|
|
338
|
+
verificationTokens,
|
|
339
|
+
refreshTokenStore,
|
|
340
|
+
passwordHistory,
|
|
341
|
+
passwordPolicy,
|
|
342
|
+
totpService,
|
|
343
|
+
oauthProviders,
|
|
344
|
+
oauthAccounts,
|
|
345
|
+
logger: logger.child({ service: "auth" }),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const userService = createUserService({
|
|
349
|
+
userRepo,
|
|
350
|
+
passwordHasher,
|
|
351
|
+
logger: logger.child({ service: "user" }),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const healthService = createHealthService({
|
|
355
|
+
logger: logger.child({ service: "health" }),
|
|
356
|
+
version: "2.0.0",
|
|
357
|
+
circuitBreakers: [dbCircuitBreaker],
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const adminService = createAdminService({
|
|
361
|
+
userRepo,
|
|
362
|
+
auditLog,
|
|
363
|
+
logger: logger.child({ service: "admin" }),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const apiKeyService = createApiKeyService({
|
|
367
|
+
apiKeyRepo,
|
|
368
|
+
logger: logger.child({ service: "api-key" }),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
Container.register(Tokens.AuthService, authService);
|
|
372
|
+
Container.register(Tokens.UserService, userService);
|
|
373
|
+
Container.register(Tokens.HealthService, healthService);
|
|
374
|
+
Container.register(Tokens.AdminService, adminService);
|
|
375
|
+
Container.register(Tokens.ApiKeyService, apiKeyService);
|
|
376
|
+
|
|
377
|
+
// 8. Presentation
|
|
378
|
+
const router = createRouter({
|
|
379
|
+
authService,
|
|
380
|
+
userService,
|
|
381
|
+
healthService,
|
|
382
|
+
adminService,
|
|
383
|
+
apiKeyService,
|
|
384
|
+
tokenService,
|
|
385
|
+
metricsCollector,
|
|
386
|
+
oauthProviders,
|
|
387
|
+
eventBus,
|
|
388
|
+
webhookRegistry,
|
|
389
|
+
logger,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// 8b. WebSocket manager
|
|
393
|
+
const wsManager = createWebSocketManager({
|
|
394
|
+
tokenService,
|
|
395
|
+
eventBus,
|
|
396
|
+
logger: logger.child({ service: "websocket" }),
|
|
397
|
+
});
|
|
398
|
+
Container.register(Tokens.WebSocketManager, wsManager);
|
|
399
|
+
|
|
400
|
+
// Wire WebSocket manager as event subscriber (broadcast to WS clients)
|
|
401
|
+
eventBus.subscribeAll((event) => wsManager.broadcast(event));
|
|
402
|
+
|
|
403
|
+
const srv = createServer({ config, logger, router, metrics: metricsCollector, wsManager });
|
|
404
|
+
|
|
405
|
+
// 9. Start
|
|
406
|
+
const instance = srv.start();
|
|
407
|
+
jobQueue.start();
|
|
408
|
+
|
|
409
|
+
// 10. Print startup banner
|
|
410
|
+
const isCluster = Bun.env["WORKER_ID"] !== undefined;
|
|
411
|
+
if (!isCluster) {
|
|
412
|
+
printStartupBanner({
|
|
413
|
+
config,
|
|
414
|
+
bootTimeMs: performance.now() - bootStart,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 11. Periodic token pruning (every 10 minutes)
|
|
419
|
+
const pruneInterval = setInterval(
|
|
420
|
+
async () => {
|
|
421
|
+
const blacklistResult = await tokenBlacklist.prune();
|
|
422
|
+
if (blacklistResult.ok && blacklistResult.value > 0) {
|
|
423
|
+
logger.debug("Pruned expired blacklisted tokens", { count: blacklistResult.value });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const verifyResult = await verificationTokens.prune();
|
|
427
|
+
if (verifyResult.ok && verifyResult.value > 0) {
|
|
428
|
+
logger.debug("Pruned expired verification tokens", { count: verifyResult.value });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const refreshResult = await refreshTokenStore.prune(30 * 24 * 60 * 60 * 1000);
|
|
432
|
+
if (refreshResult.ok && refreshResult.value > 0) {
|
|
433
|
+
logger.debug("Pruned revoked refresh token families", { count: refreshResult.value });
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
10 * 60 * 1000,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// 12. Graceful shutdown
|
|
440
|
+
const shutdown = (signal: string) => {
|
|
441
|
+
clearInterval(pruneInterval);
|
|
442
|
+
jobQueue.stop();
|
|
443
|
+
srv.flush(); // flush buffered access logs before exit
|
|
444
|
+
if (db) db.close(); // close SQLite connection
|
|
445
|
+
if (mssqlPool) mssqlPool.close(); // close SQL Server connection pool
|
|
446
|
+
cache.close(); // close cache (no-op for in-memory, QUIT for Redis)
|
|
447
|
+
printShutdown(signal);
|
|
448
|
+
instance.stop();
|
|
449
|
+
process.exit(0);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
453
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
454
|
+
|
|
455
|
+
// 13. Unhandled rejection safety net
|
|
456
|
+
process.on("unhandledRejection", (reason) => {
|
|
457
|
+
logger.fatal("Unhandled promise rejection", {
|
|
458
|
+
error: reason instanceof Error ? reason.message : String(reason),
|
|
459
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Fire alert on fatal errors
|
|
463
|
+
alertSink.send({
|
|
464
|
+
level: AlertLevel.CRITICAL,
|
|
465
|
+
title: "Unhandled promise rejection",
|
|
466
|
+
message: reason instanceof Error ? reason.message : String(reason),
|
|
467
|
+
timestamp: new Date().toISOString(),
|
|
468
|
+
source: "onlyApi",
|
|
469
|
+
metadata: {
|
|
470
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
metricsCollector.alertsSentTotal.inc({ level: "critical" });
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return instance;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
bootstrap();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Logger } from "../core/ports/logger.js";
|
|
2
|
+
import type { TokenPayload } from "../core/ports/token-service.js";
|
|
3
|
+
import type { RequestId } from "../core/types/brand.js";
|
|
4
|
+
import type { TraceContext } from "../infrastructure/tracing/trace-context.js";
|
|
5
|
+
import type { I18nContext } from "./i18n/index.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Typed request context threaded through the middleware pipeline.
|
|
9
|
+
* Immutable — each middleware returns a new context with added fields.
|
|
10
|
+
*/
|
|
11
|
+
export interface RequestContext {
|
|
12
|
+
readonly requestId: RequestId;
|
|
13
|
+
readonly startTime: number;
|
|
14
|
+
readonly ip: string;
|
|
15
|
+
readonly method: string;
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly auth?: TokenPayload | undefined;
|
|
18
|
+
/** W3C Trace Context — traceId + spanId for distributed tracing */
|
|
19
|
+
readonly trace: TraceContext;
|
|
20
|
+
/** Request-scoped logger with requestId pre-bound */
|
|
21
|
+
readonly logger: Logger;
|
|
22
|
+
/** API version resolved from URL path or Accept-Version header */
|
|
23
|
+
readonly apiVersion: "v1" | "v2";
|
|
24
|
+
/** i18n context with locale-bound translation function */
|
|
25
|
+
readonly i18n: I18nContext;
|
|
26
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { banUserDto, changeRoleDto, listUsersDto } from "../../application/dtos/admin.dto.js";
|
|
2
|
+
import type { AdminService } from "../../application/services/admin.service.js";
|
|
3
|
+
import { UserRole } from "../../core/entities/user.entity.js";
|
|
4
|
+
import type { Logger } from "../../core/ports/logger.js";
|
|
5
|
+
import type { TokenService } from "../../core/ports/token-service.js";
|
|
6
|
+
import type { UserId } from "../../core/types/brand.js";
|
|
7
|
+
import type { RequestContext } from "../context.js";
|
|
8
|
+
import { authenticate, authorise } from "../middleware/auth.js";
|
|
9
|
+
import { validateBody } from "../middleware/validate.js";
|
|
10
|
+
import { errorResponse, jsonResponse, noContentResponse } from "./response.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract query parameters from a URL string without allocating a URL object.
|
|
14
|
+
*/
|
|
15
|
+
const extractQuery = (url: string): URLSearchParams => {
|
|
16
|
+
const qIdx = url.indexOf("?");
|
|
17
|
+
return new URLSearchParams(qIdx === -1 ? "" : url.substring(qIdx + 1));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const adminHandlers = (
|
|
21
|
+
adminService: AdminService,
|
|
22
|
+
tokenService: TokenService,
|
|
23
|
+
logger: Logger,
|
|
24
|
+
) => {
|
|
25
|
+
/** Shared admin auth check — requires ADMIN role */
|
|
26
|
+
const requireAdmin = async (req: Request, _ctx: RequestContext) => {
|
|
27
|
+
const authResult = await authenticate(req, tokenService);
|
|
28
|
+
if (!authResult.ok) return authResult;
|
|
29
|
+
return authorise(authResult.value, [UserRole.ADMIN]);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
listUsers: async (req: Request, ctx: RequestContext): Promise<Response> => {
|
|
34
|
+
const auth = await requireAdmin(req, ctx);
|
|
35
|
+
if (!auth.ok) {
|
|
36
|
+
logger.warn("Admin auth failed on listUsers", { requestId: ctx.requestId });
|
|
37
|
+
return errorResponse(auth.error, ctx.requestId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const query = extractQuery(req.url);
|
|
41
|
+
const parsed = validateBody(listUsersDto, {
|
|
42
|
+
cursor: query.get("cursor") ?? undefined,
|
|
43
|
+
limit: query.get("limit") ?? "20",
|
|
44
|
+
search: query.get("search") ?? undefined,
|
|
45
|
+
role: query.get("role") ?? undefined,
|
|
46
|
+
});
|
|
47
|
+
if (!parsed.ok) return errorResponse(parsed.error, ctx.requestId);
|
|
48
|
+
|
|
49
|
+
const dto = { ...parsed.value, limit: parsed.value.limit ?? 20 };
|
|
50
|
+
const result = await adminService.listUsers(dto);
|
|
51
|
+
if (!result.ok) return errorResponse(result.error, ctx.requestId);
|
|
52
|
+
|
|
53
|
+
return jsonResponse(result.value);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
getUser: async (req: Request, ctx: RequestContext, userId: string): Promise<Response> => {
|
|
57
|
+
const auth = await requireAdmin(req, ctx);
|
|
58
|
+
if (!auth.ok) return errorResponse(auth.error, ctx.requestId);
|
|
59
|
+
|
|
60
|
+
const result = await adminService.getUser(userId as UserId);
|
|
61
|
+
if (!result.ok) return errorResponse(result.error, ctx.requestId);
|
|
62
|
+
|
|
63
|
+
return jsonResponse(result.value);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
changeRole: async (req: Request, ctx: RequestContext, userId: string): Promise<Response> => {
|
|
67
|
+
const auth = await requireAdmin(req, ctx);
|
|
68
|
+
if (!auth.ok) return errorResponse(auth.error, ctx.requestId);
|
|
69
|
+
|
|
70
|
+
const body = await req.json().catch(() => null);
|
|
71
|
+
const validated = validateBody(changeRoleDto, body);
|
|
72
|
+
if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
|
|
73
|
+
|
|
74
|
+
const result = await adminService.changeRole(
|
|
75
|
+
userId as UserId,
|
|
76
|
+
validated.value,
|
|
77
|
+
auth.value.sub,
|
|
78
|
+
ctx.ip,
|
|
79
|
+
);
|
|
80
|
+
if (!result.ok) return errorResponse(result.error, ctx.requestId);
|
|
81
|
+
|
|
82
|
+
return jsonResponse(result.value);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
banUser: async (req: Request, ctx: RequestContext, userId: string): Promise<Response> => {
|
|
86
|
+
const auth = await requireAdmin(req, ctx);
|
|
87
|
+
if (!auth.ok) return errorResponse(auth.error, ctx.requestId);
|
|
88
|
+
|
|
89
|
+
const body = await req.json().catch(() => null);
|
|
90
|
+
const validated = validateBody(banUserDto, body);
|
|
91
|
+
if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
|
|
92
|
+
|
|
93
|
+
const result = await adminService.banUser(
|
|
94
|
+
userId as UserId,
|
|
95
|
+
validated.value,
|
|
96
|
+
auth.value.sub,
|
|
97
|
+
ctx.ip,
|
|
98
|
+
);
|
|
99
|
+
if (!result.ok) return errorResponse(result.error, ctx.requestId);
|
|
100
|
+
|
|
101
|
+
return noContentResponse();
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
unbanUser: async (req: Request, ctx: RequestContext, userId: string): Promise<Response> => {
|
|
105
|
+
const auth = await requireAdmin(req, ctx);
|
|
106
|
+
if (!auth.ok) return errorResponse(auth.error, ctx.requestId);
|
|
107
|
+
|
|
108
|
+
const result = await adminService.unbanUser(userId as UserId, auth.value.sub, ctx.ip);
|
|
109
|
+
if (!result.ok) return errorResponse(result.error, ctx.requestId);
|
|
110
|
+
|
|
111
|
+
return noContentResponse();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createApiKeyDto } from "../../application/dtos/auth.dto.js";
|
|
2
|
+
import type { ApiKeyService } from "../../application/services/api-key.service.js";
|
|
3
|
+
import type { Logger } from "../../core/ports/logger.js";
|
|
4
|
+
import type { TokenService } from "../../core/ports/token-service.js";
|
|
5
|
+
import type { RequestContext } from "../context.js";
|
|
6
|
+
import { authenticate } from "../middleware/auth.js";
|
|
7
|
+
import { validateBody } from "../middleware/validate.js";
|
|
8
|
+
import { createdResponse, errorResponse, jsonResponse, noContentResponse } from "./response.js";
|
|
9
|
+
|
|
10
|
+
export const apiKeyHandlers = (
|
|
11
|
+
apiKeyService: ApiKeyService,
|
|
12
|
+
tokenService: TokenService,
|
|
13
|
+
logger: Logger,
|
|
14
|
+
) => ({
|
|
15
|
+
create: async (req: Request, ctx: RequestContext): Promise<Response> => {
|
|
16
|
+
const authResult = await authenticate(req, tokenService);
|
|
17
|
+
if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
|
|
18
|
+
|
|
19
|
+
const body = await req.json().catch(() => null);
|
|
20
|
+
const validated = validateBody(createApiKeyDto, body);
|
|
21
|
+
if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
|
|
22
|
+
|
|
23
|
+
const result = await apiKeyService.create(
|
|
24
|
+
authResult.value.sub,
|
|
25
|
+
validated.value.name,
|
|
26
|
+
validated.value.scopes ?? [],
|
|
27
|
+
validated.value.expiresInDays,
|
|
28
|
+
);
|
|
29
|
+
if (!result.ok) return errorResponse(result.error, ctx.requestId);
|
|
30
|
+
|
|
31
|
+
logger.info("API key created", {
|
|
32
|
+
requestId: ctx.requestId,
|
|
33
|
+
userId: authResult.value.sub,
|
|
34
|
+
keyId: result.value.key.id,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return createdResponse({
|
|
38
|
+
key: result.value.key,
|
|
39
|
+
rawKey: result.value.rawKey,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
list: async (req: Request, ctx: RequestContext): Promise<Response> => {
|
|
44
|
+
const authResult = await authenticate(req, tokenService);
|
|
45
|
+
if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
|
|
46
|
+
|
|
47
|
+
const result = await apiKeyService.list(authResult.value.sub);
|
|
48
|
+
if (!result.ok) return errorResponse(result.error, ctx.requestId);
|
|
49
|
+
|
|
50
|
+
return jsonResponse({ keys: result.value });
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
revoke: async (req: Request, ctx: RequestContext, keyId: string): Promise<Response> => {
|
|
54
|
+
const authResult = await authenticate(req, tokenService);
|
|
55
|
+
if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
|
|
56
|
+
|
|
57
|
+
const result = await apiKeyService.revoke(keyId, authResult.value.sub);
|
|
58
|
+
if (!result.ok) return errorResponse(result.error, ctx.requestId);
|
|
59
|
+
|
|
60
|
+
logger.info("API key revoked", {
|
|
61
|
+
requestId: ctx.requestId,
|
|
62
|
+
userId: authResult.value.sub,
|
|
63
|
+
keyId,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return noContentResponse();
|
|
67
|
+
},
|
|
68
|
+
});
|