@rebasepro/server-postgresql 0.1.0 → 0.2.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/LICENSE +22 -6
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/index.es.js +1250 -1665
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1196 -1611
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
- package/dist/server-postgresql/src/auth/services.d.ts +37 -15
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
- package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +21 -15
- package/src/PostgresAdapter.ts +59 -0
- package/src/PostgresBackendDriver.ts +57 -8
- package/src/PostgresBootstrapper.ts +35 -15
- package/src/auth/ensure-tables.ts +82 -189
- package/src/auth/services.ts +421 -170
- package/src/cli.ts +44 -13
- package/src/data-transformer.ts +78 -8
- package/src/history/HistoryService.ts +25 -2
- package/src/index.ts +1 -0
- package/src/schema/auth-schema.ts +130 -98
- package/src/schema/default-collections.ts +68 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +85 -8
- package/src/schema/generate-drizzle-schema-logic.ts +74 -27
- package/src/schema/generate-drizzle-schema.ts +13 -3
- package/src/schema/introspect-db-inference.ts +5 -5
- package/src/schema/introspect-db-logic.ts +9 -2
- package/src/schema/introspect-db.ts +14 -3
- package/src/services/EntityFetchService.ts +5 -5
- package/src/services/RelationService.ts +2 -2
- package/src/services/entity-helpers.ts +1 -1
- package/src/services/realtimeService.ts +145 -136
- package/src/utils/drizzle-conditions.ts +16 -2
- package/src/websocket.ts +113 -37
- package/test/auth-services.test.ts +163 -74
- package/test/data-transformer-hardening.test.ts +57 -0
- package/test/data-transformer.test.ts +43 -0
- package/test/generate-drizzle-schema.test.ts +7 -5
- package/test/introspect-db-utils.test.ts +4 -1
- package/test/postgresDataDriver.test.ts +17 -0
- package/test/realtimeService.test.ts +7 -7
- package/test/websocket.test.ts +139 -0
- package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +0 -21
- package/examples/sdk-demo/node_modules/esbuild/README.md +0 -3
- package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +0 -223
- package/examples/sdk-demo/node_modules/esbuild/install.js +0 -289
- package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +0 -716
- package/examples/sdk-demo/node_modules/esbuild/lib/main.js +0 -2242
- package/examples/sdk-demo/node_modules/esbuild/package.json +0 -49
package/src/auth/services.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { eq, sql } from "drizzle-orm";
|
|
1
|
+
import { eq, getTableName, sql } from "drizzle-orm";
|
|
2
2
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
-
import {
|
|
3
|
+
import { getTableConfig, PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
|
|
4
|
+
import { users, roles, userRoles, refreshTokens, passwordResetTokens, userIdentities } from "../schema/auth-schema";
|
|
4
5
|
import {
|
|
5
6
|
UserRepository,
|
|
6
7
|
RoleRepository,
|
|
@@ -18,61 +19,224 @@ import {
|
|
|
18
19
|
RoleData as Role
|
|
19
20
|
// @ts-ignore
|
|
20
21
|
} from "@rebasepro/server-core";
|
|
22
|
+
import { toSnakeCase, camelCase } from "@rebasepro/utils";
|
|
21
23
|
|
|
22
24
|
export type { Role };
|
|
23
25
|
|
|
26
|
+
export interface AuthSchemaTables {
|
|
27
|
+
users: PgTable & Record<string, AnyPgColumn>;
|
|
28
|
+
roles: PgTable & Record<string, AnyPgColumn>;
|
|
29
|
+
userRoles: PgTable & Record<string, AnyPgColumn>;
|
|
30
|
+
refreshTokens: PgTable & Record<string, AnyPgColumn>;
|
|
31
|
+
passwordResetTokens: PgTable & Record<string, AnyPgColumn>;
|
|
32
|
+
appConfig: PgTable & Record<string, AnyPgColumn>;
|
|
33
|
+
userIdentities: PgTable & Record<string, AnyPgColumn>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getColumnKey(table: (PgTable & Record<string, AnyPgColumn>) | undefined, ...keys: string[]): string | undefined {
|
|
37
|
+
if (!table) return undefined;
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
if (key in table) return key;
|
|
40
|
+
const snake = toSnakeCase(key);
|
|
41
|
+
if (snake in table) return snake;
|
|
42
|
+
const camel = camelCase(key);
|
|
43
|
+
if (camel in table) return camel;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getColumn(table: (PgTable & Record<string, AnyPgColumn>) | undefined, ...keys: string[]): AnyPgColumn | undefined {
|
|
49
|
+
if (!table) return undefined;
|
|
50
|
+
const key = getColumnKey(table, ...keys);
|
|
51
|
+
return key ? table[key] : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
24
54
|
/**
|
|
25
55
|
* PostgreSQL implementation of UserRepository.
|
|
26
56
|
* Handles all user-related database operations using Drizzle ORM.
|
|
27
57
|
*/
|
|
28
58
|
export class UserService implements UserRepository {
|
|
29
|
-
|
|
59
|
+
private usersTable: PgTable & Record<string, AnyPgColumn>;
|
|
60
|
+
private userIdentitiesTable: PgTable & Record<string, AnyPgColumn>;
|
|
61
|
+
private userRolesTable: PgTable & Record<string, AnyPgColumn>;
|
|
62
|
+
private rolesTable: PgTable & Record<string, AnyPgColumn>;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
private db: NodePgDatabase,
|
|
66
|
+
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
67
|
+
) {
|
|
68
|
+
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users || (tableOrTables as Partial<AuthSchemaTables>).roles)) {
|
|
69
|
+
const tables = tableOrTables as Partial<AuthSchemaTables>;
|
|
70
|
+
this.usersTable = (tables.users || users) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
71
|
+
this.userIdentitiesTable = (tables.userIdentities || userIdentities) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
72
|
+
this.userRolesTable = (tables.userRoles || userRoles) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
73
|
+
this.rolesTable = (tables.roles || roles) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
74
|
+
} else {
|
|
75
|
+
const table = tableOrTables as (PgTable & Record<string, AnyPgColumn>) | undefined;
|
|
76
|
+
this.usersTable = table || (users as unknown as PgTable & Record<string, AnyPgColumn>);
|
|
77
|
+
this.userIdentitiesTable = userIdentities as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
78
|
+
this.userRolesTable = userRoles as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
79
|
+
this.rolesTable = roles as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private getQualifiedUsersTableName(): string {
|
|
84
|
+
const name = getTableName(this.usersTable);
|
|
85
|
+
const schema = getTableConfig(this.usersTable).schema || "public";
|
|
86
|
+
return `"${schema}"."${name}"`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private mapRowToUser(row: Record<string, unknown>): UserData {
|
|
90
|
+
if (!row) return row as unknown as UserData;
|
|
91
|
+
|
|
92
|
+
const id = (row.id ?? row.uid) as string;
|
|
93
|
+
const email = row.email as string;
|
|
94
|
+
const passwordHash = (row.password_hash ?? row.passwordHash ?? null) as string | null | undefined;
|
|
95
|
+
const displayName = (row.display_name ?? row.displayName ?? null) as string | null | undefined;
|
|
96
|
+
const photoUrl = (row.photo_url ?? row.photoUrl ?? row.photoURL ?? null) as string | null | undefined;
|
|
97
|
+
const emailVerified = (row.email_verified ?? row.emailVerified ?? false) as boolean;
|
|
98
|
+
const emailVerificationToken = (row.email_verification_token ?? row.emailVerificationToken ?? null) as string | null | undefined;
|
|
99
|
+
const emailVerificationSentAt = (row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null) as string | number | Date | null;
|
|
100
|
+
const createdAt = (row.created_at ?? row.createdAt) as string | number | Date | undefined;
|
|
101
|
+
const updatedAt = (row.updated_at ?? row.updatedAt) as string | number | Date | undefined;
|
|
102
|
+
|
|
103
|
+
const metadata: Record<string, any> = { ...((row.metadata as Record<string, any> | undefined) || {}) };
|
|
104
|
+
|
|
105
|
+
const knownKeys = new Set([
|
|
106
|
+
"id", "uid", "email",
|
|
107
|
+
"password_hash", "passwordHash",
|
|
108
|
+
"display_name", "displayName",
|
|
109
|
+
"photo_url", "photoUrl", "photoURL",
|
|
110
|
+
"email_verified", "emailVerified",
|
|
111
|
+
"email_verification_token", "emailVerificationToken",
|
|
112
|
+
"email_verification_sent_at", "emailVerificationSentAt",
|
|
113
|
+
"created_at", "createdAt",
|
|
114
|
+
"updated_at", "updatedAt",
|
|
115
|
+
"metadata"
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
for (const [key, val] of Object.entries(row)) {
|
|
119
|
+
if (!knownKeys.has(key)) {
|
|
120
|
+
const camelKey = camelCase(key);
|
|
121
|
+
metadata[camelKey] = val;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
30
124
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
email,
|
|
128
|
+
passwordHash,
|
|
129
|
+
displayName,
|
|
130
|
+
photoUrl,
|
|
131
|
+
emailVerified,
|
|
132
|
+
emailVerificationToken,
|
|
133
|
+
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
134
|
+
createdAt: createdAt ? new Date(createdAt) : new Date(),
|
|
135
|
+
updatedAt: updatedAt ? new Date(updatedAt) : new Date(),
|
|
136
|
+
metadata
|
|
137
|
+
};
|
|
34
138
|
}
|
|
35
139
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
140
|
+
private mapPayload(data: Partial<CreateUserData>): Record<string, unknown> {
|
|
141
|
+
if (!data) return {};
|
|
142
|
+
|
|
143
|
+
const payload: Record<string, unknown> = {};
|
|
144
|
+
|
|
145
|
+
const idKey = getColumnKey(this.usersTable, "id") || "id";
|
|
146
|
+
const emailKey = getColumnKey(this.usersTable, "email") || "email";
|
|
147
|
+
const passwordHashKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
148
|
+
const displayNameKey = getColumnKey(this.usersTable, "displayName", "display_name") || "displayName";
|
|
149
|
+
const photoUrlKey = getColumnKey(this.usersTable, "photoUrl", "photo_url") || "photoUrl";
|
|
150
|
+
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
151
|
+
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
152
|
+
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
153
|
+
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
154
|
+
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
155
|
+
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
156
|
+
|
|
157
|
+
if ("id" in data) payload[idKey] = data.id;
|
|
158
|
+
if ("email" in data) payload[emailKey] = data.email;
|
|
159
|
+
if ("passwordHash" in data) payload[passwordHashKey] = data.passwordHash;
|
|
160
|
+
if ("displayName" in data) payload[displayNameKey] = data.displayName;
|
|
161
|
+
if ("photoUrl" in data) payload[photoUrlKey] = data.photoUrl;
|
|
162
|
+
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
163
|
+
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
164
|
+
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
165
|
+
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
166
|
+
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
167
|
+
|
|
168
|
+
const metadata: Record<string, any> = { ...(data.metadata || {}) };
|
|
169
|
+
const remainingMetadata: Record<string, any> = {};
|
|
170
|
+
|
|
171
|
+
for (const [key, val] of Object.entries(metadata)) {
|
|
172
|
+
const tableColKey = getColumnKey(this.usersTable, key);
|
|
173
|
+
if (tableColKey &&
|
|
174
|
+
tableColKey !== idKey &&
|
|
175
|
+
tableColKey !== emailKey &&
|
|
176
|
+
tableColKey !== passwordHashKey &&
|
|
177
|
+
tableColKey !== displayNameKey &&
|
|
178
|
+
tableColKey !== photoUrlKey &&
|
|
179
|
+
tableColKey !== emailVerifiedKey &&
|
|
180
|
+
tableColKey !== emailVerificationTokenKey &&
|
|
181
|
+
tableColKey !== emailVerificationSentAtKey &&
|
|
182
|
+
tableColKey !== createdAtKey &&
|
|
183
|
+
tableColKey !== updatedAtKey &&
|
|
184
|
+
tableColKey !== metadataKey) {
|
|
185
|
+
payload[tableColKey] = val;
|
|
186
|
+
} else {
|
|
187
|
+
remainingMetadata[key] = val;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (metadataKey in this.usersTable) {
|
|
192
|
+
payload[metadataKey] = remainingMetadata;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return payload;
|
|
39
196
|
}
|
|
40
197
|
|
|
41
|
-
async
|
|
42
|
-
const
|
|
43
|
-
|
|
198
|
+
async createUser(data: CreateUserData): Promise<UserData> {
|
|
199
|
+
const payload = this.mapPayload(data);
|
|
200
|
+
const [row] = (await this.db.insert(this.usersTable).values(payload).returning()) as Record<string, unknown>[];
|
|
201
|
+
return this.mapRowToUser(row);
|
|
44
202
|
}
|
|
45
203
|
|
|
46
|
-
async
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
LIMIT 1
|
|
53
|
-
`);
|
|
204
|
+
async getUserById(id: string): Promise<UserData | null> {
|
|
205
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
206
|
+
if (!idCol) return null;
|
|
207
|
+
const [row] = await this.db.select().from(this.usersTable).where(eq(idCol, id));
|
|
208
|
+
return row ? this.mapRowToUser(row as Record<string, unknown>) : null;
|
|
209
|
+
}
|
|
54
210
|
|
|
55
|
-
|
|
211
|
+
async getUserByEmail(email: string): Promise<UserData | null> {
|
|
212
|
+
const emailCol = getColumn(this.usersTable, "email");
|
|
213
|
+
if (!emailCol) return null;
|
|
214
|
+
const [row] = await this.db.select().from(this.usersTable).where(eq(emailCol, email.toLowerCase()));
|
|
215
|
+
return row ? this.mapRowToUser(row as Record<string, unknown>) : null;
|
|
216
|
+
}
|
|
56
217
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
218
|
+
async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {
|
|
219
|
+
const userIdCol = getColumn(this.usersTable, "id");
|
|
220
|
+
if (!userIdCol) return null;
|
|
221
|
+
|
|
222
|
+
const result = await this.db
|
|
223
|
+
.select({ user: this.usersTable })
|
|
224
|
+
.from(this.usersTable)
|
|
225
|
+
.innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.userId))
|
|
226
|
+
.where(
|
|
227
|
+
sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`
|
|
228
|
+
)
|
|
229
|
+
.limit(1);
|
|
230
|
+
|
|
231
|
+
if (result.length === 0) return null;
|
|
232
|
+
return this.mapRowToUser(result[0].user as Record<string, unknown>);
|
|
70
233
|
}
|
|
71
234
|
|
|
72
235
|
async getUserIdentities(userId: string): Promise<UserIdentityData[]> {
|
|
236
|
+
const schema = getTableConfig(this.userIdentitiesTable).schema || "public";
|
|
73
237
|
const result = await this.db.execute(sql`
|
|
74
238
|
SELECT id, user_id, provider, provider_id, profile_data, created_at, updated_at
|
|
75
|
-
FROM
|
|
239
|
+
FROM ${sql.raw(`"${schema}"."user_identities"`)}
|
|
76
240
|
WHERE user_id = ${userId}
|
|
77
241
|
`);
|
|
78
242
|
|
|
@@ -88,30 +252,38 @@ export class UserService implements UserRepository {
|
|
|
88
252
|
}
|
|
89
253
|
|
|
90
254
|
async linkUserIdentity(userId: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
|
|
91
|
-
await this.db.insert(
|
|
255
|
+
await this.db.insert(this.userIdentitiesTable).values({
|
|
92
256
|
userId,
|
|
93
257
|
provider,
|
|
94
258
|
providerId,
|
|
95
259
|
profileData: profileData || null
|
|
96
|
-
}).onConflictDoNothing({ target: [
|
|
260
|
+
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] });
|
|
97
261
|
}
|
|
98
262
|
|
|
99
|
-
async updateUser(id: string, data: Partial<Omit<
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
263
|
+
async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
|
|
264
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
265
|
+
if (!idCol) return null;
|
|
266
|
+
const payload = this.mapPayload(data);
|
|
267
|
+
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
268
|
+
payload[updatedAtKey] = new Date();
|
|
269
|
+
|
|
270
|
+
const [row] = (await this.db
|
|
271
|
+
.update(this.usersTable)
|
|
272
|
+
.set(payload)
|
|
273
|
+
.where(eq(idCol, id))
|
|
274
|
+
.returning()) as Record<string, unknown>[];
|
|
275
|
+
return row ? this.mapRowToUser(row) : null;
|
|
107
276
|
}
|
|
108
277
|
|
|
109
278
|
async deleteUser(id: string): Promise<void> {
|
|
110
|
-
|
|
279
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
280
|
+
if (!idCol) return;
|
|
281
|
+
await this.db.delete(this.usersTable).where(eq(idCol, id));
|
|
111
282
|
}
|
|
112
283
|
|
|
113
|
-
async listUsers(): Promise<
|
|
114
|
-
|
|
284
|
+
async listUsers(): Promise<UserData[]> {
|
|
285
|
+
const rows = await this.db.select().from(this.usersTable);
|
|
286
|
+
return (rows as Record<string, unknown>[]).map(row => this.mapRowToUser(row));
|
|
115
287
|
}
|
|
116
288
|
|
|
117
289
|
async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {
|
|
@@ -122,125 +294,139 @@ updatedAt: new Date() })
|
|
|
122
294
|
const orderDir = options?.orderDir || "desc";
|
|
123
295
|
const roleId = options?.roleId;
|
|
124
296
|
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
email: "email",
|
|
128
|
-
displayName: "display_name",
|
|
129
|
-
createdAt: "created_at",
|
|
130
|
-
updatedAt: "updated_at",
|
|
131
|
-
provider: "provider"
|
|
132
|
-
};
|
|
133
|
-
const orderColumn = columnMap[orderBy] || "created_at";
|
|
297
|
+
const orderCol = getColumn(this.usersTable, orderBy);
|
|
298
|
+
const orderColumn = orderCol ? orderCol.name : "created_at";
|
|
134
299
|
const direction = orderDir === "asc" ? sql`ASC` : sql`DESC`;
|
|
135
300
|
|
|
301
|
+
const emailCol = getColumn(this.usersTable, "email");
|
|
302
|
+
const emailColumn = emailCol ? emailCol.name : "email";
|
|
303
|
+
const displayNameCol = getColumn(this.usersTable, "displayName", "display_name");
|
|
304
|
+
const displayNameColumn = displayNameCol ? displayNameCol.name : "display_name";
|
|
305
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
306
|
+
const idColumn = idCol ? idCol.name : "id";
|
|
307
|
+
|
|
308
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
309
|
+
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
310
|
+
|
|
136
311
|
const conditions = [];
|
|
137
312
|
if (roleId) {
|
|
138
|
-
conditions.push(sql`EXISTS (SELECT 1 FROM
|
|
313
|
+
conditions.push(sql`EXISTS (SELECT 1 FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)} AND ur.role_id = ${roleId})`);
|
|
139
314
|
}
|
|
140
315
|
if (search) {
|
|
141
316
|
const pattern = `%${search}%`;
|
|
142
|
-
conditions.push(sql`(
|
|
317
|
+
conditions.push(sql`(${sql.raw(usersTableName)}.${sql.raw(emailColumn)} ILIKE ${pattern} OR ${sql.raw(usersTableName)}.${sql.raw(displayNameColumn)} ILIKE ${pattern})`);
|
|
143
318
|
}
|
|
144
319
|
|
|
145
320
|
const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
|
|
146
321
|
|
|
147
322
|
// Sorting: users with roles first if no role filter, then by requested order
|
|
148
323
|
const orderByClause = roleId
|
|
149
|
-
? sql`ORDER BY ${sql.raw(orderColumn)} ${direction}`
|
|
150
|
-
: sql`ORDER BY (SELECT count(*) FROM
|
|
324
|
+
? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`
|
|
325
|
+
: sql`ORDER BY (SELECT count(*) FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)}) DESC, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
|
|
151
326
|
|
|
152
327
|
const countResult = await this.db.execute(sql`
|
|
153
|
-
SELECT count(*)::int as total FROM
|
|
328
|
+
SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}
|
|
154
329
|
${whereClause}
|
|
155
330
|
`);
|
|
156
331
|
const total = (countResult.rows[0] as { total: number }).total;
|
|
157
332
|
|
|
158
333
|
const dataResult = await this.db.execute(sql`
|
|
159
|
-
SELECT * FROM
|
|
334
|
+
SELECT * FROM ${sql.raw(usersTableName)}
|
|
160
335
|
${whereClause}
|
|
161
336
|
${orderByClause}
|
|
162
337
|
LIMIT ${limit} OFFSET ${offset}
|
|
163
338
|
`);
|
|
164
|
-
const rows = dataResult.rows
|
|
339
|
+
const rows = dataResult.rows;
|
|
165
340
|
|
|
166
|
-
// Map
|
|
167
|
-
const mappedUsers:
|
|
168
|
-
id: row.id as string,
|
|
169
|
-
email: row.email as string,
|
|
170
|
-
passwordHash: ((row.password_hash ?? row.passwordHash) as string | null) ?? null,
|
|
171
|
-
displayName: ((row.display_name ?? row.displayName) as string | null) ?? null,
|
|
172
|
-
photoUrl: ((row.photo_url ?? row.photoUrl) as string | null) ?? null,
|
|
173
|
-
emailVerified: ((row.email_verified ?? row.emailVerified) as boolean | undefined) ?? false,
|
|
174
|
-
emailVerificationToken: ((row.email_verification_token ?? row.emailVerificationToken) as string | null) ?? null,
|
|
175
|
-
emailVerificationSentAt: ((row.email_verification_sent_at ?? row.emailVerificationSentAt) as Date | null) ?? null,
|
|
176
|
-
createdAt: (row.created_at ?? row.createdAt) as Date,
|
|
177
|
-
updatedAt: (row.updated_at ?? row.updatedAt) as Date
|
|
178
|
-
})) as User[];
|
|
341
|
+
// Map rows to camelCase UserData
|
|
342
|
+
const mappedUsers: UserData[] = (rows as Record<string, unknown>[]).map((row) => this.mapRowToUser(row));
|
|
179
343
|
|
|
180
344
|
return { users: mappedUsers,
|
|
181
|
-
total,
|
|
182
|
-
limit,
|
|
183
|
-
offset };
|
|
345
|
+
total,
|
|
346
|
+
limit,
|
|
347
|
+
offset };
|
|
184
348
|
}
|
|
185
349
|
|
|
186
350
|
/**
|
|
187
351
|
* Update user's password hash
|
|
188
352
|
*/
|
|
189
353
|
async updatePassword(id: string, passwordHash: string): Promise<void> {
|
|
354
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
355
|
+
if (!idCol) return;
|
|
356
|
+
const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
357
|
+
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
358
|
+
|
|
190
359
|
await this.db
|
|
191
|
-
.update(
|
|
192
|
-
.set({
|
|
193
|
-
|
|
194
|
-
|
|
360
|
+
.update(this.usersTable)
|
|
361
|
+
.set({
|
|
362
|
+
[passwordHashColKey]: passwordHash,
|
|
363
|
+
[updatedAtColKey]: new Date()
|
|
364
|
+
})
|
|
365
|
+
.where(eq(idCol, id));
|
|
195
366
|
}
|
|
196
367
|
|
|
197
368
|
/**
|
|
198
369
|
* Set email verification status
|
|
199
370
|
*/
|
|
200
371
|
async setEmailVerified(id: string, verified: boolean): Promise<void> {
|
|
372
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
373
|
+
if (!idCol) return;
|
|
374
|
+
const emailVerifiedColKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
375
|
+
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
376
|
+
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
377
|
+
|
|
201
378
|
await this.db
|
|
202
|
-
.update(
|
|
379
|
+
.update(this.usersTable)
|
|
203
380
|
.set({
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
381
|
+
[emailVerifiedColKey]: verified,
|
|
382
|
+
[emailVerificationTokenColKey]: null,
|
|
383
|
+
[updatedAtColKey]: new Date()
|
|
207
384
|
})
|
|
208
|
-
.where(eq(
|
|
385
|
+
.where(eq(idCol, id));
|
|
209
386
|
}
|
|
210
387
|
|
|
211
388
|
/**
|
|
212
389
|
* Set email verification token
|
|
213
390
|
*/
|
|
214
391
|
async setVerificationToken(id: string, token: string | null): Promise<void> {
|
|
392
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
393
|
+
if (!idCol) return;
|
|
394
|
+
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
395
|
+
const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
396
|
+
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
397
|
+
|
|
215
398
|
await this.db
|
|
216
|
-
.update(
|
|
399
|
+
.update(this.usersTable)
|
|
217
400
|
.set({
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
401
|
+
[emailVerificationTokenColKey]: token,
|
|
402
|
+
[emailVerificationSentAtColKey]: token ? new Date() : null,
|
|
403
|
+
[updatedAtColKey]: new Date()
|
|
221
404
|
})
|
|
222
|
-
.where(eq(
|
|
405
|
+
.where(eq(idCol, id));
|
|
223
406
|
}
|
|
224
407
|
|
|
225
408
|
/**
|
|
226
409
|
* Find user by email verification token
|
|
227
410
|
*/
|
|
228
|
-
async getUserByVerificationToken(token: string): Promise<
|
|
229
|
-
const
|
|
411
|
+
async getUserByVerificationToken(token: string): Promise<UserData | null> {
|
|
412
|
+
const tokenCol = getColumn(this.usersTable, "emailVerificationToken", "email_verification_token");
|
|
413
|
+
if (!tokenCol) return null;
|
|
414
|
+
const [row] = await this.db
|
|
230
415
|
.select()
|
|
231
|
-
.from(
|
|
232
|
-
.where(eq(
|
|
233
|
-
return
|
|
416
|
+
.from(this.usersTable)
|
|
417
|
+
.where(eq(tokenCol, token));
|
|
418
|
+
return row ? this.mapRowToUser(row as Record<string, unknown>) : null;
|
|
234
419
|
}
|
|
235
420
|
|
|
236
421
|
/**
|
|
237
422
|
* Get roles for a user from database
|
|
238
423
|
*/
|
|
239
424
|
async getUserRoles(userId: string): Promise<Role[]> {
|
|
425
|
+
const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
|
|
240
426
|
const result = await this.db.execute(sql`
|
|
241
427
|
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
|
|
242
|
-
FROM
|
|
243
|
-
INNER JOIN
|
|
428
|
+
FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
429
|
+
INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
244
430
|
WHERE ur.user_id = ${userId}
|
|
245
431
|
`);
|
|
246
432
|
|
|
@@ -266,13 +452,14 @@ updatedAt: new Date() })
|
|
|
266
452
|
* Set roles for a user
|
|
267
453
|
*/
|
|
268
454
|
async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
|
|
455
|
+
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
269
456
|
// Delete existing roles
|
|
270
|
-
await this.db.execute(sql`DELETE FROM
|
|
457
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} WHERE user_id = ${userId}`);
|
|
271
458
|
|
|
272
459
|
// Insert new roles
|
|
273
460
|
for (const roleId of roleIds) {
|
|
274
461
|
await this.db.execute(sql`
|
|
275
|
-
INSERT INTO
|
|
462
|
+
INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
|
|
276
463
|
VALUES (${userId}, ${roleId})
|
|
277
464
|
ON CONFLICT DO NOTHING
|
|
278
465
|
`);
|
|
@@ -283,8 +470,9 @@ updatedAt: new Date() })
|
|
|
283
470
|
* Assign a specific role to new user
|
|
284
471
|
*/
|
|
285
472
|
async assignDefaultRole(userId: string, roleId: string): Promise<void> {
|
|
473
|
+
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
286
474
|
await this.db.execute(sql`
|
|
287
|
-
INSERT INTO
|
|
475
|
+
INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
|
|
288
476
|
VALUES (${userId}, ${roleId})
|
|
289
477
|
ON CONFLICT DO NOTHING
|
|
290
478
|
`);
|
|
@@ -293,13 +481,13 @@ updatedAt: new Date() })
|
|
|
293
481
|
/**
|
|
294
482
|
* Get user with their roles
|
|
295
483
|
*/
|
|
296
|
-
async getUserWithRoles(userId: string): Promise<{ user:
|
|
484
|
+
async getUserWithRoles(userId: string): Promise<{ user: UserData; roles: Role[] } | null> {
|
|
297
485
|
const user = await this.getUserById(userId);
|
|
298
486
|
if (!user) return null;
|
|
299
487
|
|
|
300
488
|
const roles = await this.getUserRoles(userId);
|
|
301
489
|
return { user,
|
|
302
|
-
roles };
|
|
490
|
+
roles };
|
|
303
491
|
}
|
|
304
492
|
}
|
|
305
493
|
|
|
@@ -308,12 +496,30 @@ roles };
|
|
|
308
496
|
* Handles all role-related database operations using Drizzle ORM.
|
|
309
497
|
*/
|
|
310
498
|
export class RoleService implements RoleRepository {
|
|
311
|
-
|
|
499
|
+
private rolesTable: PgTable & Record<string, AnyPgColumn>;
|
|
500
|
+
|
|
501
|
+
constructor(
|
|
502
|
+
private db: NodePgDatabase,
|
|
503
|
+
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
504
|
+
) {
|
|
505
|
+
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).roles || (tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
506
|
+
this.rolesTable = ((tableOrTables as Partial<AuthSchemaTables>).roles || roles) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
507
|
+
} else {
|
|
508
|
+
this.rolesTable = (tableOrTables as unknown as PgTable & Record<string, AnyPgColumn>) || (roles as unknown as PgTable & Record<string, AnyPgColumn>);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private getQualifiedRolesTableName(): string {
|
|
513
|
+
const name = getTableName(this.rolesTable);
|
|
514
|
+
const schema = getTableConfig(this.rolesTable).schema || "public";
|
|
515
|
+
return `"${schema}"."${name}"`;
|
|
516
|
+
}
|
|
312
517
|
|
|
313
518
|
async getRoleById(id: string): Promise<Role | null> {
|
|
519
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
314
520
|
const result = await this.db.execute(sql`
|
|
315
521
|
SELECT id, name, is_admin, default_permissions, collection_permissions, config
|
|
316
|
-
FROM
|
|
522
|
+
FROM ${sql.raw(tableName)}
|
|
317
523
|
WHERE id = ${id}
|
|
318
524
|
`);
|
|
319
525
|
|
|
@@ -331,9 +537,10 @@ export class RoleService implements RoleRepository {
|
|
|
331
537
|
}
|
|
332
538
|
|
|
333
539
|
async listRoles(): Promise<Role[]> {
|
|
540
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
334
541
|
const result = await this.db.execute(sql`
|
|
335
542
|
SELECT id, name, is_admin, default_permissions, collection_permissions, config
|
|
336
|
-
FROM
|
|
543
|
+
FROM ${sql.raw(tableName)}
|
|
337
544
|
ORDER BY name
|
|
338
545
|
`);
|
|
339
546
|
|
|
@@ -348,8 +555,9 @@ export class RoleService implements RoleRepository {
|
|
|
348
555
|
}
|
|
349
556
|
|
|
350
557
|
async createRole(data: Omit<Role, "isAdmin" | "collectionPermissions"> & { isAdmin?: boolean; collectionPermissions?: Role["collectionPermissions"] }): Promise<Role> {
|
|
558
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
351
559
|
const result = await this.db.execute(sql`
|
|
352
|
-
INSERT INTO
|
|
560
|
+
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
|
|
353
561
|
VALUES (
|
|
354
562
|
${data.id},
|
|
355
563
|
${data.name},
|
|
@@ -373,12 +581,12 @@ export class RoleService implements RoleRepository {
|
|
|
373
581
|
}
|
|
374
582
|
|
|
375
583
|
async updateRole(id: string, data: Partial<Omit<Role, "id">>): Promise<Role | null> {
|
|
376
|
-
// For now, use simpler approach
|
|
377
584
|
const existing = await this.getRoleById(id);
|
|
378
585
|
if (!existing) return null;
|
|
379
586
|
|
|
587
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
380
588
|
await this.db.execute(sql`
|
|
381
|
-
UPDATE
|
|
589
|
+
UPDATE ${sql.raw(tableName)}
|
|
382
590
|
SET
|
|
383
591
|
name = ${data.name ?? existing.name},
|
|
384
592
|
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
@@ -392,12 +600,30 @@ export class RoleService implements RoleRepository {
|
|
|
392
600
|
}
|
|
393
601
|
|
|
394
602
|
async deleteRole(id: string): Promise<void> {
|
|
395
|
-
|
|
603
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
604
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(tableName)} WHERE id = ${id}`);
|
|
396
605
|
}
|
|
397
606
|
}
|
|
398
607
|
|
|
399
608
|
export class RefreshTokenService {
|
|
400
|
-
|
|
609
|
+
private refreshTokensTable: PgTable & Record<string, AnyPgColumn>;
|
|
610
|
+
|
|
611
|
+
constructor(
|
|
612
|
+
private db: NodePgDatabase,
|
|
613
|
+
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
614
|
+
) {
|
|
615
|
+
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
616
|
+
this.refreshTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || refreshTokens) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
617
|
+
} else {
|
|
618
|
+
this.refreshTokensTable = (tableOrTables as unknown as PgTable & Record<string, AnyPgColumn>) || (refreshTokens as unknown as PgTable & Record<string, AnyPgColumn>);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private getQualifiedRefreshTokensTableName(): string {
|
|
623
|
+
const name = getTableName(this.refreshTokensTable);
|
|
624
|
+
const schema = getTableConfig(this.refreshTokensTable).schema || "public";
|
|
625
|
+
return `"${schema}"."${name}"`;
|
|
626
|
+
}
|
|
401
627
|
|
|
402
628
|
async createToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
|
|
403
629
|
// Fallback to empty string because UNIQUE constraints treat NULLs as strictly distinct in standard Postgres.
|
|
@@ -407,14 +633,15 @@ export class RefreshTokenService {
|
|
|
407
633
|
|
|
408
634
|
// Delete any existing session for this user/device combo, then insert.
|
|
409
635
|
// This approach doesn't require the unique_device_session constraint to exist.
|
|
636
|
+
const tableName = this.getQualifiedRefreshTokensTableName();
|
|
410
637
|
await this.db.execute(sql`
|
|
411
|
-
DELETE FROM
|
|
638
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
412
639
|
WHERE user_id = ${userId}
|
|
413
640
|
AND user_agent = ${safeUserAgent}
|
|
414
641
|
AND ip_address = ${safeIpAddress}
|
|
415
642
|
`);
|
|
416
643
|
|
|
417
|
-
await this.db.insert(
|
|
644
|
+
await this.db.insert(this.refreshTokensTable)
|
|
418
645
|
.values({
|
|
419
646
|
userId,
|
|
420
647
|
tokenHash,
|
|
@@ -427,49 +654,49 @@ export class RefreshTokenService {
|
|
|
427
654
|
async findByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
|
|
428
655
|
const [token] = await this.db
|
|
429
656
|
.select({
|
|
430
|
-
id:
|
|
431
|
-
userId:
|
|
432
|
-
tokenHash:
|
|
433
|
-
expiresAt:
|
|
434
|
-
createdAt:
|
|
435
|
-
userAgent:
|
|
436
|
-
ipAddress:
|
|
657
|
+
id: this.refreshTokensTable.id,
|
|
658
|
+
userId: this.refreshTokensTable.userId,
|
|
659
|
+
tokenHash: this.refreshTokensTable.tokenHash,
|
|
660
|
+
expiresAt: this.refreshTokensTable.expiresAt,
|
|
661
|
+
createdAt: this.refreshTokensTable.createdAt,
|
|
662
|
+
userAgent: this.refreshTokensTable.userAgent,
|
|
663
|
+
ipAddress: this.refreshTokensTable.ipAddress
|
|
437
664
|
})
|
|
438
|
-
.from(
|
|
439
|
-
.where(eq(
|
|
665
|
+
.from(this.refreshTokensTable)
|
|
666
|
+
.where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
440
667
|
|
|
441
|
-
return token || null;
|
|
668
|
+
return (token as RefreshTokenInfo) || null;
|
|
442
669
|
}
|
|
443
670
|
|
|
444
671
|
async deleteByHash(tokenHash: string): Promise<void> {
|
|
445
|
-
await this.db.delete(
|
|
672
|
+
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
446
673
|
}
|
|
447
674
|
|
|
448
675
|
async deleteAllForUser(userId: string): Promise<void> {
|
|
449
|
-
await this.db.delete(
|
|
676
|
+
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.userId, userId));
|
|
450
677
|
}
|
|
451
678
|
|
|
452
679
|
async listForUser(userId: string): Promise<RefreshTokenInfo[]> {
|
|
453
680
|
const tokens = await this.db
|
|
454
681
|
.select({
|
|
455
|
-
id:
|
|
456
|
-
userId:
|
|
457
|
-
tokenHash:
|
|
458
|
-
expiresAt:
|
|
459
|
-
createdAt:
|
|
460
|
-
userAgent:
|
|
461
|
-
ipAddress:
|
|
682
|
+
id: this.refreshTokensTable.id,
|
|
683
|
+
userId: this.refreshTokensTable.userId,
|
|
684
|
+
tokenHash: this.refreshTokensTable.tokenHash,
|
|
685
|
+
expiresAt: this.refreshTokensTable.expiresAt,
|
|
686
|
+
createdAt: this.refreshTokensTable.createdAt,
|
|
687
|
+
userAgent: this.refreshTokensTable.userAgent,
|
|
688
|
+
ipAddress: this.refreshTokensTable.ipAddress
|
|
462
689
|
})
|
|
463
|
-
.from(
|
|
464
|
-
.where(eq(
|
|
465
|
-
.orderBy(
|
|
690
|
+
.from(this.refreshTokensTable)
|
|
691
|
+
.where(eq(this.refreshTokensTable.userId, userId))
|
|
692
|
+
.orderBy(this.refreshTokensTable.createdAt);
|
|
466
693
|
|
|
467
|
-
return tokens;
|
|
694
|
+
return tokens as RefreshTokenInfo[];
|
|
468
695
|
}
|
|
469
696
|
|
|
470
697
|
async deleteById(id: string, userId: string): Promise<void> {
|
|
471
|
-
await this.db.delete(
|
|
472
|
-
.where(sql`${
|
|
698
|
+
await this.db.delete(this.refreshTokensTable)
|
|
699
|
+
.where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.userId} = ${userId}`);
|
|
473
700
|
}
|
|
474
701
|
}
|
|
475
702
|
|
|
@@ -477,19 +704,37 @@ export class RefreshTokenService {
|
|
|
477
704
|
* Password reset token service
|
|
478
705
|
*/
|
|
479
706
|
export class PasswordResetTokenService {
|
|
480
|
-
|
|
707
|
+
private passwordResetTokensTable: PgTable & Record<string, AnyPgColumn>;
|
|
708
|
+
|
|
709
|
+
constructor(
|
|
710
|
+
private db: NodePgDatabase,
|
|
711
|
+
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
712
|
+
) {
|
|
713
|
+
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
714
|
+
this.passwordResetTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || passwordResetTokens) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
715
|
+
} else {
|
|
716
|
+
this.passwordResetTokensTable = (tableOrTables as unknown as PgTable & Record<string, AnyPgColumn>) || (passwordResetTokens as unknown as PgTable & Record<string, AnyPgColumn>);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private getQualifiedPasswordResetTokensTableName(): string {
|
|
721
|
+
const name = getTableName(this.passwordResetTokensTable);
|
|
722
|
+
const schema = getTableConfig(this.passwordResetTokensTable).schema || "public";
|
|
723
|
+
return `"${schema}"."${name}"`;
|
|
724
|
+
}
|
|
481
725
|
|
|
482
726
|
/**
|
|
483
727
|
* Create a password reset token
|
|
484
728
|
*/
|
|
485
729
|
async createToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
486
730
|
// Delete any existing unused tokens for this user
|
|
731
|
+
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
487
732
|
await this.db.execute(sql`
|
|
488
|
-
DELETE FROM
|
|
733
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
489
734
|
WHERE user_id = ${userId} AND used_at IS NULL
|
|
490
735
|
`);
|
|
491
736
|
|
|
492
|
-
await this.db.insert(
|
|
737
|
+
await this.db.insert(this.passwordResetTokensTable).values({
|
|
493
738
|
userId,
|
|
494
739
|
tokenHash,
|
|
495
740
|
expiresAt
|
|
@@ -502,18 +747,19 @@ export class PasswordResetTokenService {
|
|
|
502
747
|
async findValidByHash(tokenHash: string): Promise<{ userId: string; expiresAt: Date } | null> {
|
|
503
748
|
const [token] = await this.db
|
|
504
749
|
.select({
|
|
505
|
-
userId:
|
|
506
|
-
expiresAt:
|
|
750
|
+
userId: this.passwordResetTokensTable.userId,
|
|
751
|
+
expiresAt: this.passwordResetTokensTable.expiresAt
|
|
507
752
|
})
|
|
508
|
-
.from(
|
|
509
|
-
.where(eq(
|
|
753
|
+
.from(this.passwordResetTokensTable)
|
|
754
|
+
.where(eq(this.passwordResetTokensTable.tokenHash, tokenHash)) as unknown as Array<{ userId: string; expiresAt: Date }>;
|
|
510
755
|
|
|
511
756
|
if (!token) return null;
|
|
512
757
|
|
|
513
758
|
// Check if expired or used
|
|
759
|
+
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
514
760
|
const result = await this.db.execute(sql`
|
|
515
761
|
SELECT user_id, expires_at
|
|
516
|
-
FROM
|
|
762
|
+
FROM ${sql.raw(tableName)}
|
|
517
763
|
WHERE token_hash = ${tokenHash}
|
|
518
764
|
AND used_at IS NULL
|
|
519
765
|
AND expires_at > NOW()
|
|
@@ -533,24 +779,25 @@ export class PasswordResetTokenService {
|
|
|
533
779
|
*/
|
|
534
780
|
async markAsUsed(tokenHash: string): Promise<void> {
|
|
535
781
|
await this.db
|
|
536
|
-
.update(
|
|
782
|
+
.update(this.passwordResetTokensTable)
|
|
537
783
|
.set({ usedAt: new Date() })
|
|
538
|
-
.where(eq(
|
|
784
|
+
.where(eq(this.passwordResetTokensTable.tokenHash, tokenHash));
|
|
539
785
|
}
|
|
540
786
|
|
|
541
787
|
/**
|
|
542
788
|
* Delete all tokens for a user
|
|
543
789
|
*/
|
|
544
790
|
async deleteAllForUser(userId: string): Promise<void> {
|
|
545
|
-
await this.db.delete(
|
|
791
|
+
await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.userId, userId));
|
|
546
792
|
}
|
|
547
793
|
|
|
548
794
|
/**
|
|
549
795
|
* Clean up expired tokens
|
|
550
796
|
*/
|
|
551
797
|
async deleteExpired(): Promise<void> {
|
|
798
|
+
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
552
799
|
await this.db.execute(sql`
|
|
553
|
-
DELETE FROM
|
|
800
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
554
801
|
WHERE expires_at < NOW()
|
|
555
802
|
`);
|
|
556
803
|
}
|
|
@@ -564,9 +811,12 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
564
811
|
private refreshTokenService: RefreshTokenService;
|
|
565
812
|
private passwordResetTokenService: PasswordResetTokenService;
|
|
566
813
|
|
|
567
|
-
constructor(
|
|
568
|
-
|
|
569
|
-
|
|
814
|
+
constructor(
|
|
815
|
+
private db: NodePgDatabase,
|
|
816
|
+
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
817
|
+
) {
|
|
818
|
+
this.refreshTokenService = new RefreshTokenService(db, tableOrTables);
|
|
819
|
+
this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);
|
|
570
820
|
}
|
|
571
821
|
|
|
572
822
|
// Refresh token operations
|
|
@@ -628,28 +878,31 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
628
878
|
private roleService: RoleService;
|
|
629
879
|
private tokenRepository: PostgresTokenRepository;
|
|
630
880
|
|
|
631
|
-
constructor(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
881
|
+
constructor(
|
|
882
|
+
private db: NodePgDatabase,
|
|
883
|
+
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
884
|
+
) {
|
|
885
|
+
this.userService = new UserService(db, tableOrTables);
|
|
886
|
+
this.roleService = new RoleService(db, tableOrTables);
|
|
887
|
+
this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
|
|
635
888
|
}
|
|
636
889
|
|
|
637
890
|
// User operations (delegate to UserService)
|
|
638
891
|
|
|
639
892
|
async createUser(data: CreateUserData): Promise<UserData> {
|
|
640
|
-
return this.userService.createUser(data
|
|
893
|
+
return this.userService.createUser(data);
|
|
641
894
|
}
|
|
642
895
|
|
|
643
896
|
async getUserById(id: string): Promise<UserData | null> {
|
|
644
|
-
return this.userService.getUserById(id)
|
|
897
|
+
return this.userService.getUserById(id);
|
|
645
898
|
}
|
|
646
899
|
|
|
647
900
|
async getUserByEmail(email: string): Promise<UserData | null> {
|
|
648
|
-
return this.userService.getUserByEmail(email)
|
|
901
|
+
return this.userService.getUserByEmail(email);
|
|
649
902
|
}
|
|
650
903
|
|
|
651
904
|
async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {
|
|
652
|
-
return this.userService.getUserByIdentity(provider, providerId)
|
|
905
|
+
return this.userService.getUserByIdentity(provider, providerId);
|
|
653
906
|
}
|
|
654
907
|
|
|
655
908
|
async getUserIdentities(userId: string): Promise<UserIdentityData[]> {
|
|
@@ -661,7 +914,7 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
661
914
|
}
|
|
662
915
|
|
|
663
916
|
async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
|
|
664
|
-
return this.userService.updateUser(id, data)
|
|
917
|
+
return this.userService.updateUser(id, data);
|
|
665
918
|
}
|
|
666
919
|
|
|
667
920
|
async deleteUser(id: string): Promise<void> {
|
|
@@ -669,7 +922,7 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
669
922
|
}
|
|
670
923
|
|
|
671
924
|
async listUsers(): Promise<UserData[]> {
|
|
672
|
-
return this.userService.listUsers()
|
|
925
|
+
return this.userService.listUsers();
|
|
673
926
|
}
|
|
674
927
|
|
|
675
928
|
async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {
|
|
@@ -689,7 +942,7 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
689
942
|
}
|
|
690
943
|
|
|
691
944
|
async getUserByVerificationToken(token: string): Promise<UserData | null> {
|
|
692
|
-
return this.userService.getUserByVerificationToken(token)
|
|
945
|
+
return this.userService.getUserByVerificationToken(token);
|
|
693
946
|
}
|
|
694
947
|
|
|
695
948
|
async getUserRoles(userId: string): Promise<RoleData[]> {
|
|
@@ -709,8 +962,7 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
709
962
|
}
|
|
710
963
|
|
|
711
964
|
async getUserWithRoles(userId: string): Promise<{ user: UserData; roles: RoleData[] } | null> {
|
|
712
|
-
|
|
713
|
-
return result as { user: UserData; roles: RoleData[] } | null;
|
|
965
|
+
return this.userService.getUserWithRoles(userId);
|
|
714
966
|
}
|
|
715
967
|
|
|
716
968
|
// Role operations (delegate to RoleService)
|
|
@@ -796,4 +1048,3 @@ export type PostgresUserRepository = UserService;
|
|
|
796
1048
|
|
|
797
1049
|
/** PostgreSQL role repository implementation */
|
|
798
1050
|
export type PostgresRoleRepository = RoleService;
|
|
799
|
-
|