@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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { User } from "../../core/entities/user.entity.js";
|
|
2
|
+
import { type AppError, conflict, notFound } from "../../core/errors/app-error.js";
|
|
3
|
+
import type {
|
|
4
|
+
CreateUserData,
|
|
5
|
+
UpdateUserData,
|
|
6
|
+
UserListOptions,
|
|
7
|
+
UserRepository,
|
|
8
|
+
} from "../../core/ports/user.repository.js";
|
|
9
|
+
import type { UserId } from "../../core/types/brand.js";
|
|
10
|
+
import { brand } from "../../core/types/brand.js";
|
|
11
|
+
import { decodeCursor, encodeCursor } from "../../core/types/pagination.js";
|
|
12
|
+
import { type Result, err, ok } from "../../core/types/result.js";
|
|
13
|
+
import { generateId } from "../../shared/utils/id.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* In-memory user repository — swap for Postgres/SQLite adapter in production.
|
|
17
|
+
* Demonstrates the port/adapter contract; zero external DB deps by default.
|
|
18
|
+
*/
|
|
19
|
+
export const createInMemoryUserRepository = (): UserRepository => {
|
|
20
|
+
const store = new Map<string, User>();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
async findById(id: UserId): Promise<Result<User, AppError>> {
|
|
24
|
+
const user = store.get(id);
|
|
25
|
+
return user ? ok(user) : err(notFound("User"));
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async findByEmail(email: string): Promise<Result<User, AppError>> {
|
|
29
|
+
for (const user of store.values()) {
|
|
30
|
+
if (user.email === email) return ok(user);
|
|
31
|
+
}
|
|
32
|
+
return err(notFound("User"));
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async create(data: CreateUserData): Promise<Result<User, AppError>> {
|
|
36
|
+
// Check unique email
|
|
37
|
+
for (const user of store.values()) {
|
|
38
|
+
if (user.email === data.email) {
|
|
39
|
+
return err(conflict("Email already exists"));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const now = brand<number, "Timestamp">(Date.now());
|
|
44
|
+
const user: User = {
|
|
45
|
+
id: brand<string, "UserId">(generateId()),
|
|
46
|
+
email: data.email,
|
|
47
|
+
passwordHash: data.passwordHash,
|
|
48
|
+
role: data.role,
|
|
49
|
+
emailVerified: false,
|
|
50
|
+
mfaEnabled: false,
|
|
51
|
+
mfaSecret: null,
|
|
52
|
+
passwordChangedAt: null,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
store.set(user.id, user);
|
|
58
|
+
return ok(user);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: update handles many optional fields
|
|
62
|
+
async update(id: UserId, data: UpdateUserData): Promise<Result<User, AppError>> {
|
|
63
|
+
const existing = store.get(id);
|
|
64
|
+
if (!existing) return err(notFound("User"));
|
|
65
|
+
|
|
66
|
+
const updated: User = {
|
|
67
|
+
...existing,
|
|
68
|
+
...(data.email !== undefined ? { email: data.email } : {}),
|
|
69
|
+
...(data.passwordHash !== undefined ? { passwordHash: data.passwordHash } : {}),
|
|
70
|
+
...(data.role !== undefined ? { role: data.role } : {}),
|
|
71
|
+
...(data.emailVerified !== undefined ? { emailVerified: data.emailVerified } : {}),
|
|
72
|
+
...(data.mfaEnabled !== undefined ? { mfaEnabled: data.mfaEnabled } : {}),
|
|
73
|
+
...(data.mfaSecret !== undefined ? { mfaSecret: data.mfaSecret } : {}),
|
|
74
|
+
...(data.passwordChangedAt !== undefined
|
|
75
|
+
? {
|
|
76
|
+
passwordChangedAt: data.passwordChangedAt as
|
|
77
|
+
| import("../../core/types/brand.js").Timestamp
|
|
78
|
+
| null,
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
81
|
+
updatedAt: brand<number, "Timestamp">(Date.now()),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
store.set(id, updated);
|
|
85
|
+
return ok(updated);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async delete(id: UserId): Promise<Result<void, AppError>> {
|
|
89
|
+
if (!store.has(id)) return err(notFound("User"));
|
|
90
|
+
store.delete(id);
|
|
91
|
+
return ok(undefined);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async list(options: UserListOptions) {
|
|
95
|
+
let users = Array.from(store.values());
|
|
96
|
+
|
|
97
|
+
// Sort by createdAt descending
|
|
98
|
+
users.sort((a, b) => b.createdAt - a.createdAt);
|
|
99
|
+
|
|
100
|
+
// Cursor filter
|
|
101
|
+
if (options.cursor !== undefined) {
|
|
102
|
+
const decoded = decodeCursor(options.cursor);
|
|
103
|
+
if (decoded !== null) {
|
|
104
|
+
const ts = Number(decoded);
|
|
105
|
+
users = users.filter((u) => u.createdAt < ts);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Role filter
|
|
110
|
+
if (options.role !== undefined) {
|
|
111
|
+
users = users.filter((u) => u.role === options.role);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Search filter
|
|
115
|
+
if (options.search !== undefined) {
|
|
116
|
+
const q = options.search.toLowerCase();
|
|
117
|
+
users = users.filter((u) => u.email.includes(q));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const limit = Math.min(options.limit, 100);
|
|
121
|
+
const hasMore = users.length > limit;
|
|
122
|
+
const items = users.slice(0, limit);
|
|
123
|
+
const lastItem = items[items.length - 1];
|
|
124
|
+
const nextCursor =
|
|
125
|
+
hasMore && lastItem !== undefined ? encodeCursor(String(lastItem.createdAt)) : null;
|
|
126
|
+
|
|
127
|
+
return ok({ items, nextCursor, hasMore });
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async count() {
|
|
131
|
+
return ok(store.size);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { createInMemoryUserRepository } from "./in-memory-user.repository.js";
|
|
2
|
+
export { createSqliteUserRepository } from "./sqlite-user.repository.js";
|
|
3
|
+
export { createSqliteTokenBlacklist } from "./sqlite-token-blacklist.js";
|
|
4
|
+
export { createSqliteAccountLockout } from "./sqlite-account-lockout.js";
|
|
5
|
+
export { createSqliteAuditLog } from "./sqlite-audit-log.js";
|
|
6
|
+
export { createSqliteVerificationTokenRepo } from "./sqlite-verification-tokens.js";
|
|
7
|
+
export { createSqliteRefreshTokenStore } from "./sqlite-refresh-token-store.js";
|
|
8
|
+
export { createSqliteApiKeyRepository } from "./sqlite-api-keys.js";
|
|
9
|
+
export { createSqlitePasswordHistory } from "./sqlite-password-history.js";
|
|
10
|
+
export { createSqliteOAuthAccountRepo } from "./sqlite-oauth-accounts.js";
|
|
11
|
+
export { migrateUp, migrateDown } from "./migrations/runner.js";
|
|
12
|
+
export {
|
|
13
|
+
createPgUserRepository,
|
|
14
|
+
createPgTokenBlacklist,
|
|
15
|
+
createPgAccountLockout,
|
|
16
|
+
createPgAuditLog,
|
|
17
|
+
createPgVerificationTokenRepo,
|
|
18
|
+
createPgRefreshTokenStore,
|
|
19
|
+
createPgApiKeyRepository,
|
|
20
|
+
createPgPasswordHistory,
|
|
21
|
+
createPgOAuthAccountRepo,
|
|
22
|
+
pgMigrateUp,
|
|
23
|
+
pgMigrateDown,
|
|
24
|
+
} from "./postgres/index.js";
|
|
25
|
+
export {
|
|
26
|
+
createMssqlUserRepository,
|
|
27
|
+
createMssqlTokenBlacklist,
|
|
28
|
+
createMssqlAccountLockout,
|
|
29
|
+
createMssqlAuditLog,
|
|
30
|
+
createMssqlVerificationTokenRepo,
|
|
31
|
+
createMssqlRefreshTokenStore,
|
|
32
|
+
createMssqlApiKeyRepository,
|
|
33
|
+
createMssqlPasswordHistory,
|
|
34
|
+
createMssqlOAuthAccountRepo,
|
|
35
|
+
mssqlMigrateUp,
|
|
36
|
+
mssqlMigrateDown,
|
|
37
|
+
} from "./mssql/index.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration 001: Create users table
|
|
5
|
+
*/
|
|
6
|
+
export const up = (db: Database): void => {
|
|
7
|
+
db.run(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
email TEXT NOT NULL UNIQUE,
|
|
11
|
+
password_hash TEXT NOT NULL,
|
|
12
|
+
role TEXT NOT NULL DEFAULT 'user',
|
|
13
|
+
failed_login_attempts INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
locked_until INTEGER,
|
|
15
|
+
created_at INTEGER NOT NULL,
|
|
16
|
+
updated_at INTEGER NOT NULL
|
|
17
|
+
)
|
|
18
|
+
`);
|
|
19
|
+
|
|
20
|
+
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email)");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const down = (db: Database): void => {
|
|
24
|
+
db.run("DROP INDEX IF EXISTS idx_users_email");
|
|
25
|
+
db.run("DROP TABLE IF EXISTS users");
|
|
26
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration 002: Create token blacklist table for logout
|
|
5
|
+
*/
|
|
6
|
+
export const up = (db: Database): void => {
|
|
7
|
+
db.run(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS token_blacklist (
|
|
9
|
+
token_hash TEXT PRIMARY KEY,
|
|
10
|
+
expires_at INTEGER NOT NULL,
|
|
11
|
+
created_at INTEGER NOT NULL
|
|
12
|
+
)
|
|
13
|
+
`);
|
|
14
|
+
|
|
15
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_token_blacklist_expires ON token_blacklist(expires_at)");
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const down = (db: Database): void => {
|
|
19
|
+
db.run("DROP INDEX IF EXISTS idx_token_blacklist_expires");
|
|
20
|
+
db.run("DROP TABLE IF EXISTS token_blacklist");
|
|
21
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration 003: Create audit_log table
|
|
5
|
+
* Append-only ledger of significant system events.
|
|
6
|
+
*/
|
|
7
|
+
export const up = (db: Database): void => {
|
|
8
|
+
db.run(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
user_id TEXT,
|
|
12
|
+
action TEXT NOT NULL,
|
|
13
|
+
resource TEXT NOT NULL,
|
|
14
|
+
resource_id TEXT,
|
|
15
|
+
detail TEXT,
|
|
16
|
+
ip TEXT NOT NULL,
|
|
17
|
+
timestamp INTEGER NOT NULL
|
|
18
|
+
)
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log(user_id)");
|
|
22
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action)");
|
|
23
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp)");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const down = (db: Database): void => {
|
|
27
|
+
db.run("DROP INDEX IF EXISTS idx_audit_log_timestamp");
|
|
28
|
+
db.run("DROP INDEX IF EXISTS idx_audit_log_action");
|
|
29
|
+
db.run("DROP INDEX IF EXISTS idx_audit_log_user");
|
|
30
|
+
db.run("DROP TABLE IF EXISTS audit_log");
|
|
31
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration 004: Auth Platform
|
|
5
|
+
* - Add email_verified, mfa_secret, mfa_enabled, password_changed_at to users
|
|
6
|
+
* - Create verification_tokens table (email verification + password reset)
|
|
7
|
+
* - Create refresh_token_families table (token rotation with reuse detection)
|
|
8
|
+
* - Create api_keys table (service-to-service auth)
|
|
9
|
+
* - Create password_history table (password reuse prevention)
|
|
10
|
+
* - Create oauth_accounts table (OAuth2/SSO provider linking)
|
|
11
|
+
*/
|
|
12
|
+
export const up = (db: Database): void => {
|
|
13
|
+
// Extend users table
|
|
14
|
+
db.run("ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0");
|
|
15
|
+
db.run("ALTER TABLE users ADD COLUMN mfa_secret TEXT");
|
|
16
|
+
db.run("ALTER TABLE users ADD COLUMN mfa_enabled INTEGER NOT NULL DEFAULT 0");
|
|
17
|
+
db.run("ALTER TABLE users ADD COLUMN password_changed_at INTEGER");
|
|
18
|
+
|
|
19
|
+
// Verification tokens (email verification + password reset)
|
|
20
|
+
db.run(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS verification_tokens (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
24
|
+
type TEXT NOT NULL CHECK (type IN ('email_verification', 'password_reset')),
|
|
25
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
26
|
+
expires_at INTEGER NOT NULL,
|
|
27
|
+
used_at INTEGER,
|
|
28
|
+
created_at INTEGER NOT NULL
|
|
29
|
+
)
|
|
30
|
+
`);
|
|
31
|
+
db.run(
|
|
32
|
+
"CREATE INDEX IF NOT EXISTS idx_verification_tokens_hash ON verification_tokens(token_hash)",
|
|
33
|
+
);
|
|
34
|
+
db.run(
|
|
35
|
+
"CREATE INDEX IF NOT EXISTS idx_verification_tokens_user ON verification_tokens(user_id, type)",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Refresh token families (rotation with reuse detection)
|
|
39
|
+
db.run(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS refresh_token_families (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
43
|
+
current_token_hash TEXT NOT NULL,
|
|
44
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
created_at INTEGER NOT NULL,
|
|
46
|
+
updated_at INTEGER NOT NULL
|
|
47
|
+
)
|
|
48
|
+
`);
|
|
49
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_refresh_families_user ON refresh_token_families(user_id)");
|
|
50
|
+
db.run(
|
|
51
|
+
"CREATE INDEX IF NOT EXISTS idx_refresh_families_token ON refresh_token_families(current_token_hash)",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// API keys
|
|
55
|
+
db.run(`
|
|
56
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
59
|
+
name TEXT NOT NULL,
|
|
60
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
61
|
+
key_prefix TEXT NOT NULL,
|
|
62
|
+
scopes TEXT NOT NULL DEFAULT '[]',
|
|
63
|
+
expires_at INTEGER,
|
|
64
|
+
last_used_at INTEGER,
|
|
65
|
+
created_at INTEGER NOT NULL
|
|
66
|
+
)
|
|
67
|
+
`);
|
|
68
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)");
|
|
69
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id)");
|
|
70
|
+
|
|
71
|
+
// Password history
|
|
72
|
+
db.run(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS password_history (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
76
|
+
password_hash TEXT NOT NULL,
|
|
77
|
+
created_at INTEGER NOT NULL
|
|
78
|
+
)
|
|
79
|
+
`);
|
|
80
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_password_history_user ON password_history(user_id)");
|
|
81
|
+
|
|
82
|
+
// OAuth accounts
|
|
83
|
+
db.run(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS oauth_accounts (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
87
|
+
provider TEXT NOT NULL,
|
|
88
|
+
provider_user_id TEXT NOT NULL,
|
|
89
|
+
email TEXT,
|
|
90
|
+
created_at INTEGER NOT NULL,
|
|
91
|
+
UNIQUE(provider, provider_user_id)
|
|
92
|
+
)
|
|
93
|
+
`);
|
|
94
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_oauth_accounts_user ON oauth_accounts(user_id)");
|
|
95
|
+
db.run(
|
|
96
|
+
"CREATE INDEX IF NOT EXISTS idx_oauth_accounts_provider ON oauth_accounts(provider, provider_user_id)",
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const down = (db: Database): void => {
|
|
101
|
+
db.run("DROP TABLE IF EXISTS oauth_accounts");
|
|
102
|
+
db.run("DROP TABLE IF EXISTS password_history");
|
|
103
|
+
db.run("DROP TABLE IF EXISTS api_keys");
|
|
104
|
+
db.run("DROP TABLE IF EXISTS refresh_token_families");
|
|
105
|
+
db.run("DROP TABLE IF EXISTS verification_tokens");
|
|
106
|
+
|
|
107
|
+
// SQLite doesn't support DROP COLUMN before 3.35.0, but bun:sqlite supports it
|
|
108
|
+
db.run("ALTER TABLE users DROP COLUMN password_changed_at");
|
|
109
|
+
db.run("ALTER TABLE users DROP COLUMN mfa_enabled");
|
|
110
|
+
db.run("ALTER TABLE users DROP COLUMN mfa_secret");
|
|
111
|
+
db.run("ALTER TABLE users DROP COLUMN email_verified");
|
|
112
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { Logger } from "../../../core/ports/logger.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Database migration runner.
|
|
6
|
+
* Tracks applied migrations in a `_migrations` table.
|
|
7
|
+
* Supports up/down with versioned TypeScript migration files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface Migration {
|
|
11
|
+
readonly version: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly up: (db: Database) => void;
|
|
14
|
+
readonly down: (db: Database) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Initialize the migrations tracking table */
|
|
18
|
+
const ensureMigrationsTable = (db: Database): void => {
|
|
19
|
+
db.run(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
21
|
+
version TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
applied_at INTEGER NOT NULL
|
|
24
|
+
)
|
|
25
|
+
`);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Get all applied migration versions */
|
|
29
|
+
const getAppliedVersions = (db: Database): Set<string> => {
|
|
30
|
+
const rows = db.query("SELECT version FROM _migrations ORDER BY version").all() as Array<{
|
|
31
|
+
version: string;
|
|
32
|
+
}>;
|
|
33
|
+
return new Set(rows.map((r) => r.version));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Load all migrations from the migrations directory */
|
|
37
|
+
const loadMigrations = async (): Promise<Migration[]> => {
|
|
38
|
+
const { up: up001, down: down001 } = await import("./001_create_users.js");
|
|
39
|
+
const { up: up002, down: down002 } = await import("./002_create_token_blacklist.js");
|
|
40
|
+
const { up: up003, down: down003 } = await import("./003_create_audit_log.js");
|
|
41
|
+
const { up: up004, down: down004 } = await import("./004_auth_platform.js");
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
{ version: "001", name: "create_users", up: up001, down: down001 },
|
|
45
|
+
{ version: "002", name: "create_token_blacklist", up: up002, down: down002 },
|
|
46
|
+
{ version: "003", name: "create_audit_log", up: up003, down: down003 },
|
|
47
|
+
{ version: "004", name: "auth_platform", up: up004, down: down004 },
|
|
48
|
+
];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run all pending migrations (up).
|
|
53
|
+
* Returns the number of migrations applied.
|
|
54
|
+
*/
|
|
55
|
+
export const migrateUp = async (db: Database, logger: Logger): Promise<number> => {
|
|
56
|
+
ensureMigrationsTable(db);
|
|
57
|
+
const applied = getAppliedVersions(db);
|
|
58
|
+
const migrations = await loadMigrations();
|
|
59
|
+
let count = 0;
|
|
60
|
+
|
|
61
|
+
for (const migration of migrations) {
|
|
62
|
+
if (applied.has(migration.version)) continue;
|
|
63
|
+
|
|
64
|
+
logger.info(`Applying migration ${migration.version}: ${migration.name}`);
|
|
65
|
+
|
|
66
|
+
db.transaction(() => {
|
|
67
|
+
migration.up(db);
|
|
68
|
+
db.run("INSERT INTO _migrations (version, name, applied_at) VALUES (?, ?, ?)", [
|
|
69
|
+
migration.version,
|
|
70
|
+
migration.name,
|
|
71
|
+
Date.now(),
|
|
72
|
+
]);
|
|
73
|
+
})();
|
|
74
|
+
|
|
75
|
+
count++;
|
|
76
|
+
logger.info(`Migration ${migration.version} applied successfully`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (count === 0) {
|
|
80
|
+
logger.debug("No pending migrations");
|
|
81
|
+
} else {
|
|
82
|
+
logger.info(`Applied ${count} migration(s)`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return count;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Rollback the last applied migration (down).
|
|
90
|
+
* Returns the version that was rolled back, or null if nothing to rollback.
|
|
91
|
+
*/
|
|
92
|
+
export const migrateDown = async (db: Database, logger: Logger): Promise<string | null> => {
|
|
93
|
+
ensureMigrationsTable(db);
|
|
94
|
+
const migrations = await loadMigrations();
|
|
95
|
+
|
|
96
|
+
const lastApplied = db
|
|
97
|
+
.query("SELECT version FROM _migrations ORDER BY version DESC LIMIT 1")
|
|
98
|
+
.get() as { version: string } | null;
|
|
99
|
+
|
|
100
|
+
if (!lastApplied) {
|
|
101
|
+
logger.info("No migrations to rollback");
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const migration = migrations.find((m) => m.version === lastApplied.version);
|
|
106
|
+
if (!migration) {
|
|
107
|
+
logger.error(`Migration ${lastApplied.version} not found in migration files`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
logger.info(`Rolling back migration ${migration.version}: ${migration.name}`);
|
|
112
|
+
|
|
113
|
+
db.transaction(() => {
|
|
114
|
+
migration.down(db);
|
|
115
|
+
db.run("DELETE FROM _migrations WHERE version = ?", [migration.version]);
|
|
116
|
+
})();
|
|
117
|
+
|
|
118
|
+
logger.info(`Migration ${migration.version} rolled back successfully`);
|
|
119
|
+
return migration.version;
|
|
120
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Server (mssql) adapters — barrel export.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createMssqlUserRepository } from "./mssql-user.repository.js";
|
|
6
|
+
export { createMssqlTokenBlacklist } from "./mssql-token-blacklist.js";
|
|
7
|
+
export { createMssqlAccountLockout } from "./mssql-account-lockout.js";
|
|
8
|
+
export { createMssqlAuditLog } from "./mssql-audit-log.js";
|
|
9
|
+
export { createMssqlVerificationTokenRepo } from "./mssql-verification-tokens.js";
|
|
10
|
+
export { createMssqlRefreshTokenStore } from "./mssql-refresh-token-store.js";
|
|
11
|
+
export { createMssqlApiKeyRepository } from "./mssql-api-keys.js";
|
|
12
|
+
export { createMssqlPasswordHistory } from "./mssql-password-history.js";
|
|
13
|
+
export { createMssqlOAuthAccountRepo } from "./mssql-oauth-accounts.js";
|
|
14
|
+
export { mssqlMigrateUp, mssqlMigrateDown } from "./migrations.js";
|