@rebasepro/server-postgresql 0.2.1 → 0.2.4
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/dist/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/index.es.js +1202 -369
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1200 -367
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
- package/dist/server-postgresql/src/auth/services.d.ts +43 -1
- package/dist/server-postgresql/src/connection.d.ts +25 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +21 -0
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +7 -6
- package/src/PostgresBackendDriver.ts +13 -7
- package/src/PostgresBootstrapper.ts +27 -8
- package/src/auth/ensure-tables.ts +79 -17
- package/src/auth/services.ts +292 -23
- package/src/cli.ts +5 -0
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +80 -14
- package/src/schema/default-collections.ts +1 -0
- package/src/schema/doctor.ts +82 -41
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/services/EntityFetchService.ts +69 -10
- package/src/services/entityService.ts +2 -0
- package/src/services/realtimeService.ts +214 -2
- package/src/utils/drizzle-conditions.ts +74 -2
- package/src/websocket.ts +10 -2
- package/test/auth-services.test.ts +15 -28
- package/test/drizzle-conditions.test.ts +168 -0
- package/test/postgresDataDriver.test.ts +130 -1
- package/vite.config.ts +1 -1
package/src/auth/services.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
UserRepository,
|
|
7
7
|
RoleRepository,
|
|
8
8
|
TokenRepository,
|
|
9
|
+
MfaRepository,
|
|
9
10
|
AuthRepository,
|
|
10
11
|
UserData,
|
|
11
12
|
CreateUserData,
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
UserIdentityData,
|
|
17
18
|
ListUsersOptions,
|
|
18
19
|
PaginatedUsersResult,
|
|
20
|
+
MfaFactor,
|
|
21
|
+
MfaChallengeInfo,
|
|
19
22
|
RoleData as Role
|
|
20
23
|
// @ts-ignore
|
|
21
24
|
} from "@rebasepro/server-core";
|
|
@@ -97,6 +100,7 @@ export class UserService implements UserRepository {
|
|
|
97
100
|
const emailVerified = (row.email_verified ?? row.emailVerified ?? false) as boolean;
|
|
98
101
|
const emailVerificationToken = (row.email_verification_token ?? row.emailVerificationToken ?? null) as string | null | undefined;
|
|
99
102
|
const emailVerificationSentAt = (row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null) as string | number | Date | null;
|
|
103
|
+
const isAnonymous = (row.is_anonymous ?? row.isAnonymous ?? false) as boolean;
|
|
100
104
|
const createdAt = (row.created_at ?? row.createdAt) as string | number | Date | undefined;
|
|
101
105
|
const updatedAt = (row.updated_at ?? row.updatedAt) as string | number | Date | undefined;
|
|
102
106
|
|
|
@@ -110,6 +114,7 @@ export class UserService implements UserRepository {
|
|
|
110
114
|
"email_verified", "emailVerified",
|
|
111
115
|
"email_verification_token", "emailVerificationToken",
|
|
112
116
|
"email_verification_sent_at", "emailVerificationSentAt",
|
|
117
|
+
"is_anonymous", "isAnonymous",
|
|
113
118
|
"created_at", "createdAt",
|
|
114
119
|
"updated_at", "updatedAt",
|
|
115
120
|
"metadata"
|
|
@@ -131,6 +136,7 @@ export class UserService implements UserRepository {
|
|
|
131
136
|
emailVerified,
|
|
132
137
|
emailVerificationToken,
|
|
133
138
|
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
139
|
+
isAnonymous,
|
|
134
140
|
createdAt: createdAt ? new Date(createdAt) : new Date(),
|
|
135
141
|
updatedAt: updatedAt ? new Date(updatedAt) : new Date(),
|
|
136
142
|
metadata
|
|
@@ -150,6 +156,7 @@ export class UserService implements UserRepository {
|
|
|
150
156
|
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
151
157
|
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
152
158
|
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
159
|
+
const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
|
|
153
160
|
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
154
161
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
155
162
|
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
@@ -162,6 +169,7 @@ export class UserService implements UserRepository {
|
|
|
162
169
|
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
163
170
|
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
164
171
|
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
172
|
+
if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
|
|
165
173
|
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
166
174
|
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
167
175
|
|
|
@@ -179,6 +187,7 @@ export class UserService implements UserRepository {
|
|
|
179
187
|
tableColKey !== emailVerifiedKey &&
|
|
180
188
|
tableColKey !== emailVerificationTokenKey &&
|
|
181
189
|
tableColKey !== emailVerificationSentAtKey &&
|
|
190
|
+
tableColKey !== isAnonymousKey &&
|
|
182
191
|
tableColKey !== createdAtKey &&
|
|
183
192
|
tableColKey !== updatedAtKey &&
|
|
184
193
|
tableColKey !== metadataKey) {
|
|
@@ -424,19 +433,18 @@ export class UserService implements UserRepository {
|
|
|
424
433
|
async getUserRoles(userId: string): Promise<Role[]> {
|
|
425
434
|
const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
|
|
426
435
|
const result = await this.db.execute(sql`
|
|
427
|
-
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
436
|
+
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
428
437
|
FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
429
438
|
INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
430
439
|
WHERE ur.user_id = ${userId}
|
|
431
440
|
`);
|
|
432
441
|
|
|
433
|
-
return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null
|
|
442
|
+
return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null }>).map(row => ({
|
|
434
443
|
id: row.id,
|
|
435
444
|
name: row.name,
|
|
436
445
|
isAdmin: row.is_admin,
|
|
437
446
|
defaultPermissions: row.default_permissions,
|
|
438
|
-
collectionPermissions: row.collection_permissions
|
|
439
|
-
config: row.config
|
|
447
|
+
collectionPermissions: row.collection_permissions
|
|
440
448
|
}));
|
|
441
449
|
}
|
|
442
450
|
|
|
@@ -518,65 +526,61 @@ export class RoleService implements RoleRepository {
|
|
|
518
526
|
async getRoleById(id: string): Promise<Role | null> {
|
|
519
527
|
const tableName = this.getQualifiedRolesTableName();
|
|
520
528
|
const result = await this.db.execute(sql`
|
|
521
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
529
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
522
530
|
FROM ${sql.raw(tableName)}
|
|
523
531
|
WHERE id = ${id}
|
|
524
532
|
`);
|
|
525
533
|
|
|
526
534
|
if (result.rows.length === 0) return null;
|
|
527
535
|
|
|
528
|
-
const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null
|
|
536
|
+
const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null };
|
|
529
537
|
return {
|
|
530
538
|
id: row.id,
|
|
531
539
|
name: row.name,
|
|
532
540
|
isAdmin: row.is_admin,
|
|
533
541
|
defaultPermissions: row.default_permissions,
|
|
534
|
-
collectionPermissions: row.collection_permissions
|
|
535
|
-
config: row.config
|
|
542
|
+
collectionPermissions: row.collection_permissions
|
|
536
543
|
};
|
|
537
544
|
}
|
|
538
545
|
|
|
539
546
|
async listRoles(): Promise<Role[]> {
|
|
540
547
|
const tableName = this.getQualifiedRolesTableName();
|
|
541
548
|
const result = await this.db.execute(sql`
|
|
542
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
549
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
543
550
|
FROM ${sql.raw(tableName)}
|
|
544
551
|
ORDER BY name
|
|
545
552
|
`);
|
|
546
553
|
|
|
547
|
-
return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null
|
|
554
|
+
return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null }>).map(row => ({
|
|
548
555
|
id: row.id,
|
|
549
556
|
name: row.name,
|
|
550
557
|
isAdmin: row.is_admin,
|
|
551
558
|
defaultPermissions: row.default_permissions,
|
|
552
|
-
collectionPermissions: row.collection_permissions
|
|
553
|
-
config: row.config
|
|
559
|
+
collectionPermissions: row.collection_permissions
|
|
554
560
|
}));
|
|
555
561
|
}
|
|
556
562
|
|
|
557
563
|
async createRole(data: Omit<Role, "isAdmin" | "collectionPermissions"> & { isAdmin?: boolean; collectionPermissions?: Role["collectionPermissions"] }): Promise<Role> {
|
|
558
564
|
const tableName = this.getQualifiedRolesTableName();
|
|
559
565
|
const result = await this.db.execute(sql`
|
|
560
|
-
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions
|
|
566
|
+
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
|
|
561
567
|
VALUES (
|
|
562
568
|
${data.id},
|
|
563
569
|
${data.name},
|
|
564
570
|
${data.isAdmin ?? false},
|
|
565
571
|
${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
|
|
566
|
-
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
567
|
-
${data.config ? JSON.stringify(data.config) : null}::jsonb
|
|
572
|
+
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
568
573
|
)
|
|
569
|
-
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
574
|
+
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
570
575
|
`);
|
|
571
576
|
|
|
572
|
-
const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null
|
|
577
|
+
const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null };
|
|
573
578
|
return {
|
|
574
579
|
id: row.id,
|
|
575
580
|
name: row.name,
|
|
576
581
|
isAdmin: row.is_admin,
|
|
577
582
|
defaultPermissions: row.default_permissions,
|
|
578
|
-
collectionPermissions: row.collection_permissions
|
|
579
|
-
config: row.config
|
|
583
|
+
collectionPermissions: row.collection_permissions
|
|
580
584
|
};
|
|
581
585
|
}
|
|
582
586
|
|
|
@@ -591,8 +595,7 @@ export class RoleService implements RoleRepository {
|
|
|
591
595
|
name = ${data.name ?? existing.name},
|
|
592
596
|
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
593
597
|
default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
|
|
594
|
-
collection_permissions = ${data.collectionPermissions !== undefined ? (data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null) : (existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null)}::jsonb
|
|
595
|
-
config = ${data.config ? JSON.stringify(data.config) : (existing.config ? JSON.stringify(existing.config) : null)}::jsonb
|
|
598
|
+
collection_permissions = ${data.collectionPermissions !== undefined ? (data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null) : (existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null)}::jsonb
|
|
596
599
|
WHERE id = ${id}
|
|
597
600
|
`);
|
|
598
601
|
|
|
@@ -979,8 +982,7 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
979
982
|
return this.roleService.createRole({
|
|
980
983
|
...data,
|
|
981
984
|
defaultPermissions: data.defaultPermissions ?? null,
|
|
982
|
-
collectionPermissions: data.collectionPermissions ?? null
|
|
983
|
-
config: data.config ?? null
|
|
985
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
984
986
|
});
|
|
985
987
|
}
|
|
986
988
|
|
|
@@ -1037,6 +1039,273 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
1037
1039
|
async deleteExpiredTokens(): Promise<void> {
|
|
1038
1040
|
await this.tokenRepository.deleteExpiredTokens();
|
|
1039
1041
|
}
|
|
1042
|
+
|
|
1043
|
+
// MFA operations (delegate to MfaService)
|
|
1044
|
+
|
|
1045
|
+
private _mfaService: MfaService | null = null;
|
|
1046
|
+
private getMfaService(): MfaService {
|
|
1047
|
+
if (!this._mfaService) {
|
|
1048
|
+
this._mfaService = new MfaService(this.db);
|
|
1049
|
+
}
|
|
1050
|
+
return this._mfaService;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async createMfaFactor(userId: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {
|
|
1054
|
+
return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async getMfaFactors(userId: string): Promise<MfaFactor[]> {
|
|
1058
|
+
return this.getMfaService().getMfaFactors(userId);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
1062
|
+
return this.getMfaService().getMfaFactorById(factorId);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async verifyMfaFactor(factorId: string): Promise<void> {
|
|
1066
|
+
return this.getMfaService().verifyMfaFactor(factorId);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async deleteMfaFactor(factorId: string, userId: string): Promise<void> {
|
|
1070
|
+
return this.getMfaService().deleteMfaFactor(factorId, userId);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
|
|
1074
|
+
return this.getMfaService().createMfaChallenge(factorId, ipAddress);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {
|
|
1078
|
+
return this.getMfaService().getMfaChallengeById(challengeId);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async verifyMfaChallenge(challengeId: string): Promise<void> {
|
|
1082
|
+
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void> {
|
|
1086
|
+
return this.getMfaService().createRecoveryCodes(userId, codeHashes);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async useRecoveryCode(userId: string, codeHash: string): Promise<boolean> {
|
|
1090
|
+
return this.getMfaService().useRecoveryCode(userId, codeHash);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async getUnusedRecoveryCodeCount(userId: string): Promise<number> {
|
|
1094
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(userId);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
async deleteAllRecoveryCodes(userId: string): Promise<void> {
|
|
1098
|
+
return this.getMfaService().deleteAllRecoveryCodes(userId);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async hasVerifiedMfaFactors(userId: string): Promise<boolean> {
|
|
1102
|
+
return this.getMfaService().hasVerifiedMfaFactors(userId);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// =============================================================================
|
|
1107
|
+
// MFA SERVICE
|
|
1108
|
+
// =============================================================================
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* PostgreSQL implementation of MfaRepository.
|
|
1112
|
+
* Handles all MFA-related database operations.
|
|
1113
|
+
*/
|
|
1114
|
+
export class MfaService implements MfaRepository {
|
|
1115
|
+
constructor(private db: NodePgDatabase, private schemaName: string = "rebase") {}
|
|
1116
|
+
|
|
1117
|
+
private qualify(tableName: string): string {
|
|
1118
|
+
return `"${this.schemaName}"."${tableName}"`;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async createMfaFactor(
|
|
1122
|
+
userId: string,
|
|
1123
|
+
factorType: "totp",
|
|
1124
|
+
secretEncrypted: string,
|
|
1125
|
+
friendlyName?: string
|
|
1126
|
+
): Promise<MfaFactor> {
|
|
1127
|
+
const tableName = this.qualify("mfa_factors");
|
|
1128
|
+
const result = await this.db.execute(sql`
|
|
1129
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
|
|
1130
|
+
VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
1131
|
+
RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
1132
|
+
`);
|
|
1133
|
+
|
|
1134
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
1135
|
+
return {
|
|
1136
|
+
id: row.id as string,
|
|
1137
|
+
userId: row.user_id as string,
|
|
1138
|
+
factorType: row.factor_type as "totp",
|
|
1139
|
+
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1140
|
+
verified: row.verified as boolean,
|
|
1141
|
+
createdAt: new Date(row.created_at as string),
|
|
1142
|
+
updatedAt: new Date(row.updated_at as string)
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async getMfaFactors(userId: string): Promise<MfaFactor[]> {
|
|
1147
|
+
const tableName = this.qualify("mfa_factors");
|
|
1148
|
+
const result = await this.db.execute(sql`
|
|
1149
|
+
SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
1150
|
+
FROM ${sql.raw(tableName)}
|
|
1151
|
+
WHERE user_id = ${userId}
|
|
1152
|
+
ORDER BY created_at
|
|
1153
|
+
`);
|
|
1154
|
+
|
|
1155
|
+
return (result.rows as Array<Record<string, unknown>>).map(row => ({
|
|
1156
|
+
id: row.id as string,
|
|
1157
|
+
userId: row.user_id as string,
|
|
1158
|
+
factorType: row.factor_type as "totp",
|
|
1159
|
+
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1160
|
+
verified: row.verified as boolean,
|
|
1161
|
+
createdAt: new Date(row.created_at as string),
|
|
1162
|
+
updatedAt: new Date(row.updated_at as string)
|
|
1163
|
+
}));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
1167
|
+
const tableName = this.qualify("mfa_factors");
|
|
1168
|
+
const result = await this.db.execute(sql`
|
|
1169
|
+
SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
1170
|
+
FROM ${sql.raw(tableName)}
|
|
1171
|
+
WHERE id = ${factorId}
|
|
1172
|
+
`);
|
|
1173
|
+
|
|
1174
|
+
if (result.rows.length === 0) return null;
|
|
1175
|
+
|
|
1176
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
1177
|
+
return {
|
|
1178
|
+
id: row.id as string,
|
|
1179
|
+
userId: row.user_id as string,
|
|
1180
|
+
factorType: row.factor_type as "totp",
|
|
1181
|
+
secretEncrypted: row.secret_encrypted as string,
|
|
1182
|
+
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1183
|
+
verified: row.verified as boolean,
|
|
1184
|
+
createdAt: new Date(row.created_at as string),
|
|
1185
|
+
updatedAt: new Date(row.updated_at as string)
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
async verifyMfaFactor(factorId: string): Promise<void> {
|
|
1190
|
+
const tableName = this.qualify("mfa_factors");
|
|
1191
|
+
await this.db.execute(sql`
|
|
1192
|
+
UPDATE ${sql.raw(tableName)}
|
|
1193
|
+
SET verified = TRUE, updated_at = NOW()
|
|
1194
|
+
WHERE id = ${factorId}
|
|
1195
|
+
`);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async deleteMfaFactor(factorId: string, userId: string): Promise<void> {
|
|
1199
|
+
const tableName = this.qualify("mfa_factors");
|
|
1200
|
+
await this.db.execute(sql`
|
|
1201
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
1202
|
+
WHERE id = ${factorId} AND user_id = ${userId}
|
|
1203
|
+
`);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
|
|
1207
|
+
const tableName = this.qualify("mfa_challenges");
|
|
1208
|
+
// Challenges expire in 5 minutes
|
|
1209
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
|
|
1210
|
+
const result = await this.db.execute(sql`
|
|
1211
|
+
INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
|
|
1212
|
+
VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
|
|
1213
|
+
RETURNING id, factor_id, created_at, verified_at, ip_address
|
|
1214
|
+
`);
|
|
1215
|
+
|
|
1216
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
1217
|
+
return {
|
|
1218
|
+
id: row.id as string,
|
|
1219
|
+
factorId: row.factor_id as string,
|
|
1220
|
+
createdAt: new Date(row.created_at as string),
|
|
1221
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,
|
|
1222
|
+
ipAddress: (row.ip_address as string | null) ?? undefined
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {
|
|
1227
|
+
const tableName = this.qualify("mfa_challenges");
|
|
1228
|
+
const result = await this.db.execute(sql`
|
|
1229
|
+
SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
|
|
1230
|
+
FROM ${sql.raw(tableName)}
|
|
1231
|
+
WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
|
|
1232
|
+
`);
|
|
1233
|
+
|
|
1234
|
+
if (result.rows.length === 0) return null;
|
|
1235
|
+
|
|
1236
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
1237
|
+
return {
|
|
1238
|
+
id: row.id as string,
|
|
1239
|
+
factorId: row.factor_id as string,
|
|
1240
|
+
createdAt: new Date(row.created_at as string),
|
|
1241
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,
|
|
1242
|
+
ipAddress: (row.ip_address as string | null) ?? undefined
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async verifyMfaChallenge(challengeId: string): Promise<void> {
|
|
1247
|
+
const tableName = this.qualify("mfa_challenges");
|
|
1248
|
+
await this.db.execute(sql`
|
|
1249
|
+
UPDATE ${sql.raw(tableName)}
|
|
1250
|
+
SET verified_at = NOW()
|
|
1251
|
+
WHERE id = ${challengeId}
|
|
1252
|
+
`);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
async createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void> {
|
|
1256
|
+
const tableName = this.qualify("recovery_codes");
|
|
1257
|
+
// Delete existing codes first
|
|
1258
|
+
await this.db.execute(sql`
|
|
1259
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
1260
|
+
`);
|
|
1261
|
+
|
|
1262
|
+
// Insert new codes
|
|
1263
|
+
for (const hash of codeHashes) {
|
|
1264
|
+
await this.db.execute(sql`
|
|
1265
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
|
|
1266
|
+
VALUES (${userId}, ${hash})
|
|
1267
|
+
`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
async useRecoveryCode(userId: string, codeHash: string): Promise<boolean> {
|
|
1272
|
+
const tableName = this.qualify("recovery_codes");
|
|
1273
|
+
const result = await this.db.execute(sql`
|
|
1274
|
+
UPDATE ${sql.raw(tableName)}
|
|
1275
|
+
SET used_at = NOW()
|
|
1276
|
+
WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
1277
|
+
RETURNING id
|
|
1278
|
+
`);
|
|
1279
|
+
|
|
1280
|
+
return result.rows.length > 0;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
async getUnusedRecoveryCodeCount(userId: string): Promise<number> {
|
|
1284
|
+
const tableName = this.qualify("recovery_codes");
|
|
1285
|
+
const result = await this.db.execute(sql`
|
|
1286
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1287
|
+
WHERE user_id = ${userId} AND used_at IS NULL
|
|
1288
|
+
`);
|
|
1289
|
+
|
|
1290
|
+
return (result.rows[0] as { count: number }).count;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
async deleteAllRecoveryCodes(userId: string): Promise<void> {
|
|
1294
|
+
const tableName = this.qualify("recovery_codes");
|
|
1295
|
+
await this.db.execute(sql`
|
|
1296
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
1297
|
+
`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
async hasVerifiedMfaFactors(userId: string): Promise<boolean> {
|
|
1301
|
+
const tableName = this.qualify("mfa_factors");
|
|
1302
|
+
const result = await this.db.execute(sql`
|
|
1303
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1304
|
+
WHERE user_id = ${userId} AND verified = TRUE
|
|
1305
|
+
`);
|
|
1306
|
+
|
|
1307
|
+
return (result.rows[0] as { count: number }).count > 0;
|
|
1308
|
+
}
|
|
1040
1309
|
}
|
|
1041
1310
|
|
|
1042
1311
|
// =============================================================================
|
package/src/cli.ts
CHANGED
|
@@ -478,11 +478,16 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
|
|
|
478
478
|
const errorOutput = stderr || stdout;
|
|
479
479
|
if (errorOutput) {
|
|
480
480
|
const lines = errorOutput.split("\n").filter((l: string) => l.trim());
|
|
481
|
+
let printedCount = 0;
|
|
481
482
|
for (const line of lines) {
|
|
482
483
|
if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates") || line.includes("permission denied")) {
|
|
483
484
|
console.error(chalk.red(` ${line.trim()}`));
|
|
485
|
+
printedCount++;
|
|
484
486
|
}
|
|
485
487
|
}
|
|
488
|
+
if (printedCount === 0) {
|
|
489
|
+
lines.slice(0, 10).forEach(line => console.error(chalk.red(` ${line.trim()}`)));
|
|
490
|
+
}
|
|
486
491
|
}
|
|
487
492
|
console.error("");
|
|
488
493
|
process.exit(1);
|
package/src/connection.ts
CHANGED
|
@@ -82,3 +82,80 @@ export function createPostgresDatabaseConnection(
|
|
|
82
82
|
pool,
|
|
83
83
|
connectionString };
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a direct (non-pooled) connection for operations that require
|
|
88
|
+
* session-level features incompatible with PgBouncer transaction mode,
|
|
89
|
+
* such as LISTEN/NOTIFY, prepared statements, or advisory locks.
|
|
90
|
+
*
|
|
91
|
+
* Uses a smaller pool since this is only for specific use cases.
|
|
92
|
+
*/
|
|
93
|
+
export function createDirectDatabaseConnection(
|
|
94
|
+
connectionString: string,
|
|
95
|
+
schema?: Record<string, unknown>,
|
|
96
|
+
poolConfig?: PostgresPoolConfig
|
|
97
|
+
) {
|
|
98
|
+
const opts = {
|
|
99
|
+
...DEFAULT_POOL,
|
|
100
|
+
max: 5,
|
|
101
|
+
...poolConfig
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const pgPoolConfig: PoolConfig = {
|
|
105
|
+
connectionString,
|
|
106
|
+
max: opts.max,
|
|
107
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
108
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
109
|
+
query_timeout: opts.queryTimeout,
|
|
110
|
+
statement_timeout: opts.statementTimeout,
|
|
111
|
+
keepAlive: opts.keepAlive,
|
|
112
|
+
keepAliveInitialDelayMillis: 0
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const pool = new Pool(pgPoolConfig);
|
|
116
|
+
|
|
117
|
+
pool.on("error", (err) => {
|
|
118
|
+
console.error("[pg-direct-pool] Unexpected pool error:", err.message);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
|
|
122
|
+
|
|
123
|
+
return { db, pool, connectionString };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a read-only connection for routing read queries to replicas.
|
|
128
|
+
* Uses a moderate pool size since reads are distributed across replicas.
|
|
129
|
+
*/
|
|
130
|
+
export function createReadReplicaConnection(
|
|
131
|
+
connectionString: string,
|
|
132
|
+
schema?: Record<string, unknown>,
|
|
133
|
+
poolConfig?: PostgresPoolConfig
|
|
134
|
+
) {
|
|
135
|
+
const opts = {
|
|
136
|
+
...DEFAULT_POOL,
|
|
137
|
+
max: 10,
|
|
138
|
+
...poolConfig
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const pgPoolConfig: PoolConfig = {
|
|
142
|
+
connectionString,
|
|
143
|
+
max: opts.max,
|
|
144
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
145
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
146
|
+
query_timeout: opts.queryTimeout,
|
|
147
|
+
statement_timeout: opts.statementTimeout,
|
|
148
|
+
keepAlive: opts.keepAlive,
|
|
149
|
+
keepAliveInitialDelayMillis: 0
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const pool = new Pool(pgPoolConfig);
|
|
153
|
+
|
|
154
|
+
pool.on("error", (err) => {
|
|
155
|
+
console.error("[pg-replica-pool] Unexpected pool error:", err.message);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
|
|
159
|
+
|
|
160
|
+
return { db, pool, connectionString };
|
|
161
|
+
}
|
package/src/data-transformer.ts
CHANGED
|
@@ -263,8 +263,8 @@ export function serializePropertyToServer(value: unknown, property: Property): u
|
|
|
263
263
|
if (value instanceof Vector) {
|
|
264
264
|
return value.value;
|
|
265
265
|
}
|
|
266
|
-
if (value && typeof value === "object" && "value" in value && Array.isArray((value as
|
|
267
|
-
return (value as
|
|
266
|
+
if (value && typeof value === "object" && "value" in value && Array.isArray((value as { value: unknown }).value)) {
|
|
267
|
+
return (value as { value: unknown[] }).value.map(Number);
|
|
268
268
|
}
|
|
269
269
|
if (Array.isArray(value)) {
|
|
270
270
|
return value.map(Number);
|