create-forgeon 0.3.19 → 0.3.21
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/package.json +1 -1
- package/src/cli/add-help.mjs +3 -2
- package/src/core/docs.test.mjs +1 -0
- package/src/core/scaffold.test.mjs +1 -0
- package/src/modules/accounts.mjs +9 -18
- package/src/modules/dependencies.mjs +153 -4
- package/src/modules/dependencies.test.mjs +58 -0
- package/src/modules/executor.test.mjs +544 -515
- package/src/modules/files-access.mjs +375 -375
- package/src/modules/files-image.mjs +512 -510
- package/src/modules/files-quotas.mjs +365 -365
- package/src/modules/files.mjs +5 -6
- package/src/modules/idempotency.test.mjs +3 -2
- package/src/modules/registry.mjs +20 -0
- package/src/modules/shared/files-runtime-wiring.mjs +13 -10
- package/src/run-add-module.mjs +39 -26
- package/src/run-add-module.test.mjs +228 -152
- package/src/run-scan-integrations.mjs +1 -0
- package/templates/base/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
- package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
- package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
- package/templates/module-presets/files/packages/files/package.json +1 -0
- package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
- package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
- package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
- package/templates/module-presets/files/packages/files/src/index.ts +1 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -20
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -118
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +18 -18
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
|
@@ -12,12 +12,9 @@ import type {
|
|
|
12
12
|
UserRecordDto,
|
|
13
13
|
} from '@forgeon/accounts-contracts';
|
|
14
14
|
import { ACCOUNTS_EMAIL_PORT, type AccountsEmailPort } from './accounts-email.port';
|
|
15
|
-
import {
|
|
16
|
-
ACCOUNTS_PERSISTENCE_PORT,
|
|
17
|
-
type AccountsPersistencePort,
|
|
18
|
-
} from './accounts-persistence.port';
|
|
19
15
|
import { AuthJwtService } from './auth-jwt.service';
|
|
20
16
|
import { AuthPasswordService } from './auth-password.service';
|
|
17
|
+
import { AuthStore } from './auth.store';
|
|
21
18
|
import type { AuthRefreshTokenPayload } from './auth.types';
|
|
22
19
|
import { UsersService } from './users.service';
|
|
23
20
|
import { toUserRecordDto } from './users.types';
|
|
@@ -33,8 +30,7 @@ const AUTH_ERROR_CODES = {
|
|
|
33
30
|
@Injectable()
|
|
34
31
|
export class AuthCoreService {
|
|
35
32
|
constructor(
|
|
36
|
-
|
|
37
|
-
private readonly persistence: AccountsPersistencePort,
|
|
33
|
+
private readonly authStore: AuthStore,
|
|
38
34
|
@Inject(ACCOUNTS_EMAIL_PORT)
|
|
39
35
|
private readonly emailPort: AccountsEmailPort,
|
|
40
36
|
private readonly authJwtService: AuthJwtService,
|
|
@@ -44,7 +40,7 @@ export class AuthCoreService {
|
|
|
44
40
|
|
|
45
41
|
async registerWithPassword(input: RegisterRequest): Promise<AuthSessionResponse> {
|
|
46
42
|
const email = input.email.trim().toLowerCase();
|
|
47
|
-
const existing = await this.
|
|
43
|
+
const existing = await this.authStore.findPasswordAccountByEmail(email);
|
|
48
44
|
if (existing) {
|
|
49
45
|
throw new ConflictException({
|
|
50
46
|
message: 'Email is already registered',
|
|
@@ -53,7 +49,7 @@ export class AuthCoreService {
|
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
const passwordHash = await this.authPasswordService.hash(input.password);
|
|
56
|
-
const account = await this.
|
|
52
|
+
const account = await this.authStore.createPasswordAccount({
|
|
57
53
|
email,
|
|
58
54
|
passwordHash,
|
|
59
55
|
status: 'active',
|
|
@@ -88,7 +84,7 @@ export class AuthCoreService {
|
|
|
88
84
|
|
|
89
85
|
async loginWithPassword(emailInput: string, password: string): Promise<AuthSessionResponse> {
|
|
90
86
|
const email = emailInput.trim().toLowerCase();
|
|
91
|
-
const account = await this.
|
|
87
|
+
const account = await this.authStore.findPasswordAccountByEmail(email);
|
|
92
88
|
if (!account?.passwordHash) {
|
|
93
89
|
throw this.invalidCredentialsError();
|
|
94
90
|
}
|
|
@@ -119,7 +115,7 @@ export class AuthCoreService {
|
|
|
119
115
|
});
|
|
120
116
|
}
|
|
121
117
|
|
|
122
|
-
const record = await this.
|
|
118
|
+
const record = await this.authStore.findRefreshTokenById(payload.jti);
|
|
123
119
|
if (!record || record.revokedAt || record.userId !== payload.sub || record.expiresAt <= new Date()) {
|
|
124
120
|
throw new UnauthorizedException({
|
|
125
121
|
message: 'Refresh token is invalid or expired',
|
|
@@ -129,14 +125,14 @@ export class AuthCoreService {
|
|
|
129
125
|
|
|
130
126
|
const matched = await this.authPasswordService.verify(refreshToken, record.tokenHash);
|
|
131
127
|
if (!matched) {
|
|
132
|
-
await this.
|
|
128
|
+
await this.authStore.revokeRefreshToken(record.id, new Date());
|
|
133
129
|
throw new UnauthorizedException({
|
|
134
130
|
message: 'Refresh token is invalid or expired',
|
|
135
131
|
details: { code: AUTH_ERROR_CODES.refreshInvalid },
|
|
136
132
|
});
|
|
137
133
|
}
|
|
138
134
|
|
|
139
|
-
const account = await this.
|
|
135
|
+
const account = await this.authStore.findAccountByUserId(payload.sub);
|
|
140
136
|
if (!account) {
|
|
141
137
|
throw new UnauthorizedException({
|
|
142
138
|
message: 'Refresh token is invalid or expired',
|
|
@@ -145,7 +141,7 @@ export class AuthCoreService {
|
|
|
145
141
|
}
|
|
146
142
|
|
|
147
143
|
this.assertAccountActive(account);
|
|
148
|
-
await this.
|
|
144
|
+
await this.authStore.revokeRefreshToken(record.id, new Date());
|
|
149
145
|
return this.issueSession(toUserRecordDto(account));
|
|
150
146
|
}
|
|
151
147
|
|
|
@@ -158,18 +154,18 @@ export class AuthCoreService {
|
|
|
158
154
|
});
|
|
159
155
|
}
|
|
160
156
|
|
|
161
|
-
await this.
|
|
157
|
+
await this.authStore.revokeRefreshToken(payload.jti, new Date());
|
|
162
158
|
}
|
|
163
159
|
|
|
164
160
|
async changePassword(userId: string, newPassword: string): Promise<void> {
|
|
165
161
|
const passwordHash = await this.authPasswordService.hash(newPassword);
|
|
166
|
-
await this.
|
|
167
|
-
await this.
|
|
162
|
+
await this.authStore.updatePassword(userId, passwordHash);
|
|
163
|
+
await this.authStore.revokeRefreshTokensForUser(userId, new Date());
|
|
168
164
|
}
|
|
169
165
|
|
|
170
166
|
async requestPasswordReset(emailInput: string) {
|
|
171
167
|
const email = emailInput.trim().toLowerCase();
|
|
172
|
-
const account = await this.
|
|
168
|
+
const account = await this.authStore.findPasswordAccountByEmail(email);
|
|
173
169
|
if (account) {
|
|
174
170
|
await this.emailPort.sendPasswordResetEmail({
|
|
175
171
|
email,
|
|
@@ -218,7 +214,7 @@ export class AuthCoreService {
|
|
|
218
214
|
return {
|
|
219
215
|
status: 'ok',
|
|
220
216
|
feature: 'accounts',
|
|
221
|
-
storage: 'db-
|
|
217
|
+
storage: 'db-prisma',
|
|
222
218
|
emailDelivery: 'stub',
|
|
223
219
|
selfServiceRoutes: [
|
|
224
220
|
'/api/users/:id',
|
|
@@ -245,7 +241,7 @@ export class AuthCoreService {
|
|
|
245
241
|
]);
|
|
246
242
|
|
|
247
243
|
const tokenHash = await this.authPasswordService.hash(refreshToken);
|
|
248
|
-
await this.
|
|
244
|
+
await this.authStore.createRefreshToken({
|
|
249
245
|
id: refreshId,
|
|
250
246
|
userId: user.id,
|
|
251
247
|
tokenHash,
|
|
@@ -1,15 +1,43 @@
|
|
|
1
|
-
import { Prisma } from '@prisma/client';
|
|
2
|
-
import {
|
|
3
|
-
type AccountsPersistencePort,
|
|
4
|
-
type CreatePasswordAccountInput,
|
|
5
|
-
type PasswordAccountRecord,
|
|
6
|
-
type RefreshTokenRecord,
|
|
7
|
-
} from '@forgeon/accounts-api';
|
|
8
|
-
import { PrismaService } from '@forgeon/db-prisma';
|
|
9
1
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import type { IdentityProvider, JsonObject } from '@forgeon/accounts-contracts';
|
|
3
|
+
import { PrismaService } from '@forgeon/db-prisma';
|
|
4
|
+
import type { UserRecord } from './users.types';
|
|
5
|
+
import { mapUserRecord, toPrismaJsonInput } from './users.types';
|
|
6
|
+
|
|
7
|
+
export type PasswordAccountRecord = UserRecord & {
|
|
8
|
+
provider: IdentityProvider;
|
|
9
|
+
providerId: string;
|
|
10
|
+
passwordHash: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RefreshTokenRecord = {
|
|
14
|
+
id: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
tokenHash: string;
|
|
17
|
+
expiresAt: Date;
|
|
18
|
+
revokedAt: Date | null;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface CreatePasswordAccountInput {
|
|
23
|
+
email: string;
|
|
24
|
+
passwordHash: string;
|
|
25
|
+
status: string;
|
|
26
|
+
userData: JsonObject | null;
|
|
27
|
+
profile: {
|
|
28
|
+
name: string | null;
|
|
29
|
+
avatar: string | null;
|
|
30
|
+
data: JsonObject | null;
|
|
31
|
+
};
|
|
32
|
+
settings: {
|
|
33
|
+
theme: string | null;
|
|
34
|
+
locale: string | null;
|
|
35
|
+
data: JsonObject | null;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
10
38
|
|
|
11
39
|
@Injectable()
|
|
12
|
-
export class
|
|
40
|
+
export class AuthStore {
|
|
13
41
|
constructor(private readonly prisma: PrismaService) {}
|
|
14
42
|
|
|
15
43
|
async createPasswordAccount(input: CreatePasswordAccountInput): Promise<PasswordAccountRecord> {
|
|
@@ -17,19 +45,19 @@ export class PrismaAccountsPersistenceStore implements AccountsPersistencePort {
|
|
|
17
45
|
const user = await tx.user.create({
|
|
18
46
|
data: {
|
|
19
47
|
status: input.status,
|
|
20
|
-
data:
|
|
48
|
+
data: toPrismaJsonInput(input.userData),
|
|
21
49
|
profile: {
|
|
22
50
|
create: {
|
|
23
51
|
name: input.profile.name,
|
|
24
52
|
avatar: input.profile.avatar,
|
|
25
|
-
data:
|
|
53
|
+
data: toPrismaJsonInput(input.profile.data),
|
|
26
54
|
},
|
|
27
55
|
},
|
|
28
56
|
settings: {
|
|
29
57
|
create: {
|
|
30
58
|
theme: input.settings.theme,
|
|
31
59
|
locale: input.settings.locale,
|
|
32
|
-
data:
|
|
60
|
+
data: toPrismaJsonInput(input.settings.data),
|
|
33
61
|
},
|
|
34
62
|
},
|
|
35
63
|
},
|
|
@@ -159,174 +187,24 @@ export class PrismaAccountsPersistenceStore implements AccountsPersistencePort {
|
|
|
159
187
|
});
|
|
160
188
|
}
|
|
161
189
|
|
|
162
|
-
async findUserById(userId: string) {
|
|
163
|
-
const user = await this.prisma.user.findUnique({
|
|
164
|
-
where: { id: userId },
|
|
165
|
-
include: {
|
|
166
|
-
profile: true,
|
|
167
|
-
settings: true,
|
|
168
|
-
authIdentities: {
|
|
169
|
-
where: { provider: 'email' },
|
|
170
|
-
select: { providerId: true },
|
|
171
|
-
take: 1,
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return user ? this.mapUser(user) : null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async updateUser(input: { userId: string; data: Record<string, unknown> | null }) {
|
|
180
|
-
const user = await this.prisma.user.update({
|
|
181
|
-
where: { id: input.userId },
|
|
182
|
-
data: {
|
|
183
|
-
data: this.toNullableJson(input.data),
|
|
184
|
-
},
|
|
185
|
-
include: {
|
|
186
|
-
profile: true,
|
|
187
|
-
settings: true,
|
|
188
|
-
authIdentities: {
|
|
189
|
-
where: { provider: 'email' },
|
|
190
|
-
select: { providerId: true },
|
|
191
|
-
take: 1,
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return this.mapUser(user);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async updateUserProfile(input: {
|
|
200
|
-
userId: string;
|
|
201
|
-
name: string | null;
|
|
202
|
-
avatar: string | null;
|
|
203
|
-
data: Record<string, unknown> | null;
|
|
204
|
-
}) {
|
|
205
|
-
await this.prisma.userProfile.upsert({
|
|
206
|
-
where: { userId: input.userId },
|
|
207
|
-
create: {
|
|
208
|
-
userId: input.userId,
|
|
209
|
-
name: input.name,
|
|
210
|
-
avatar: input.avatar,
|
|
211
|
-
data: this.toNullableJson(input.data),
|
|
212
|
-
},
|
|
213
|
-
update: {
|
|
214
|
-
name: input.name,
|
|
215
|
-
avatar: input.avatar,
|
|
216
|
-
data: this.toNullableJson(input.data),
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const user = await this.findUserById(input.userId);
|
|
221
|
-
if (!user) {
|
|
222
|
-
throw new NotFoundException('User not found');
|
|
223
|
-
}
|
|
224
|
-
return user;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async updateUserSettings(input: {
|
|
228
|
-
userId: string;
|
|
229
|
-
theme: string | null;
|
|
230
|
-
locale: string | null;
|
|
231
|
-
data: Record<string, unknown> | null;
|
|
232
|
-
}) {
|
|
233
|
-
await this.prisma.userSettings.upsert({
|
|
234
|
-
where: { userId: input.userId },
|
|
235
|
-
create: {
|
|
236
|
-
userId: input.userId,
|
|
237
|
-
theme: input.theme,
|
|
238
|
-
locale: input.locale,
|
|
239
|
-
data: this.toNullableJson(input.data),
|
|
240
|
-
},
|
|
241
|
-
update: {
|
|
242
|
-
theme: input.theme,
|
|
243
|
-
locale: input.locale,
|
|
244
|
-
data: this.toNullableJson(input.data),
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const user = await this.findUserById(input.userId);
|
|
249
|
-
if (!user) {
|
|
250
|
-
throw new NotFoundException('User not found');
|
|
251
|
-
}
|
|
252
|
-
return user;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async softDeleteUser(userId: string, deletedAt: Date): Promise<void> {
|
|
256
|
-
await this.prisma.user.update({
|
|
257
|
-
where: { id: userId },
|
|
258
|
-
data: {
|
|
259
|
-
status: 'deleted',
|
|
260
|
-
deletedAt,
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
190
|
private mapPasswordAccount(user: {
|
|
266
191
|
id: string;
|
|
267
192
|
status: string;
|
|
268
|
-
data:
|
|
193
|
+
data: unknown;
|
|
269
194
|
createdAt: Date;
|
|
270
195
|
updatedAt: Date;
|
|
271
196
|
deletedAt: Date | null;
|
|
272
|
-
profile: { name: string | null; avatar: string | null; data:
|
|
273
|
-
settings: { theme: string | null; locale: string | null; data:
|
|
197
|
+
profile: { name: string | null; avatar: string | null; data: unknown } | null;
|
|
198
|
+
settings: { theme: string | null; locale: string | null; data: unknown } | null;
|
|
274
199
|
authCredential: { passwordHash: string } | null;
|
|
275
200
|
authIdentities: Array<{ provider?: string; providerId: string }>;
|
|
276
201
|
}): PasswordAccountRecord {
|
|
277
202
|
const emailIdentity = user.authIdentities[0];
|
|
278
203
|
return {
|
|
279
|
-
...
|
|
204
|
+
...mapUserRecord(user),
|
|
280
205
|
provider: 'email',
|
|
281
206
|
providerId: emailIdentity?.providerId ?? '',
|
|
282
207
|
passwordHash: user.authCredential?.passwordHash ?? null,
|
|
283
208
|
};
|
|
284
209
|
}
|
|
285
|
-
|
|
286
|
-
private mapUser(user: {
|
|
287
|
-
id: string;
|
|
288
|
-
status: string;
|
|
289
|
-
data: Prisma.JsonValue | null;
|
|
290
|
-
createdAt: Date;
|
|
291
|
-
updatedAt: Date;
|
|
292
|
-
deletedAt: Date | null;
|
|
293
|
-
profile: { name: string | null; avatar: string | null; data: Prisma.JsonValue | null } | null;
|
|
294
|
-
settings: { theme: string | null; locale: string | null; data: Prisma.JsonValue | null } | null;
|
|
295
|
-
authIdentities: Array<{ providerId: string }>;
|
|
296
|
-
}) {
|
|
297
|
-
return {
|
|
298
|
-
id: user.id,
|
|
299
|
-
email: user.authIdentities[0]?.providerId ?? null,
|
|
300
|
-
status: user.status,
|
|
301
|
-
data: this.fromJson(user.data),
|
|
302
|
-
createdAt: user.createdAt,
|
|
303
|
-
updatedAt: user.updatedAt,
|
|
304
|
-
deletedAt: user.deletedAt,
|
|
305
|
-
profile: user.profile
|
|
306
|
-
? {
|
|
307
|
-
name: user.profile.name,
|
|
308
|
-
avatar: user.profile.avatar,
|
|
309
|
-
data: this.fromJson(user.profile.data),
|
|
310
|
-
}
|
|
311
|
-
: null,
|
|
312
|
-
settings: user.settings
|
|
313
|
-
? {
|
|
314
|
-
theme: user.settings.theme,
|
|
315
|
-
locale: user.settings.locale,
|
|
316
|
-
data: this.fromJson(user.settings.data),
|
|
317
|
-
}
|
|
318
|
-
: null,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private toNullableJson(value: Record<string, unknown> | null) {
|
|
323
|
-
return value === null ? Prisma.JsonNull : (value as Prisma.InputJsonValue);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private fromJson(value: Prisma.JsonValue | null): Record<string, unknown> | null {
|
|
327
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
return value as Record<string, unknown>;
|
|
331
|
-
}
|
|
332
210
|
}
|
package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
2
|
DynamicModule,
|
|
3
3
|
Module,
|
|
4
4
|
ModuleMetadata,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
} from '@nestjs/common';
|
|
7
7
|
import { JwtModule } from '@nestjs/jwt';
|
|
8
8
|
import { PassportModule } from '@nestjs/passport';
|
|
9
|
+
import { DbPrismaModule } from '@forgeon/db-prisma';
|
|
9
10
|
import {
|
|
10
11
|
ACCOUNTS_AUTHZ_CLAIMS_RESOLVER,
|
|
11
12
|
NoopAccountsAuthzClaimsResolver,
|
|
@@ -17,12 +18,14 @@ import { AuthCoreService } from './auth-core.service';
|
|
|
17
18
|
import { AuthJwtService } from './auth-jwt.service';
|
|
18
19
|
import { AuthPasswordService } from './auth-password.service';
|
|
19
20
|
import { AuthService } from './auth.service';
|
|
21
|
+
import { AuthStore } from './auth.store';
|
|
20
22
|
import { JwtAuthGuard } from './access-token.guard';
|
|
21
23
|
import { JwtStrategy } from './jwt.strategy';
|
|
22
24
|
import { OwnerAccessGuard } from './owner-access.guard';
|
|
23
25
|
import { UsersController } from './users.controller';
|
|
24
26
|
import { UsersModule, USERS_MODULE_OPTIONS, type UsersModuleOptions } from './users-config';
|
|
25
27
|
import { UsersService } from './users.service';
|
|
28
|
+
import { UsersStore } from './users.store';
|
|
26
29
|
|
|
27
30
|
export interface ForgeonAccountsModuleOptions {
|
|
28
31
|
imports?: ModuleMetadata['imports'];
|
|
@@ -37,6 +40,7 @@ export class ForgeonAccountsModule {
|
|
|
37
40
|
module: ForgeonAccountsModule,
|
|
38
41
|
imports: [
|
|
39
42
|
AuthConfigModule,
|
|
43
|
+
DbPrismaModule,
|
|
40
44
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
|
41
45
|
JwtModule.register({}),
|
|
42
46
|
...(options.imports ?? []),
|
|
@@ -55,6 +59,8 @@ export class ForgeonAccountsModule {
|
|
|
55
59
|
provide: ACCOUNTS_AUTHZ_CLAIMS_RESOLVER,
|
|
56
60
|
useClass: NoopAccountsAuthzClaimsResolver,
|
|
57
61
|
},
|
|
62
|
+
AuthStore,
|
|
63
|
+
UsersStore,
|
|
58
64
|
AuthCoreService,
|
|
59
65
|
AuthJwtService,
|
|
60
66
|
AuthPasswordService,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './accounts-persistence.port';
|
|
1
|
+
export * from './accounts-email.port';
|
|
3
2
|
export * from './accounts-rbac.port';
|
|
4
3
|
export * from './auth-config.loader';
|
|
5
4
|
export * from './auth-config.module';
|
|
@@ -10,6 +9,7 @@ export * from './auth-env.schema';
|
|
|
10
9
|
export * from './auth-jwt.service';
|
|
11
10
|
export * from './auth-password.service';
|
|
12
11
|
export * from './auth.service';
|
|
12
|
+
export * from './auth.store';
|
|
13
13
|
export * from './auth.types';
|
|
14
14
|
export * from './dto';
|
|
15
15
|
export * from './forgeon-accounts.module';
|
|
@@ -19,6 +19,5 @@ export * from './owner-access.guard';
|
|
|
19
19
|
export * from './users-config';
|
|
20
20
|
export * from './users.controller';
|
|
21
21
|
export * from './users.service';
|
|
22
|
+
export * from './users.store';
|
|
22
23
|
export * from './users.types';
|
|
23
|
-
|
|
24
|
-
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
2
2
|
import type { UpdateUserProfileRequest, UpdateUserSettingsRequest, UpdateUserRequest } from '@forgeon/accounts-contracts';
|
|
3
|
-
import { ACCOUNTS_PERSISTENCE_PORT, type AccountsPersistencePort } from './accounts-persistence.port';
|
|
4
3
|
import { USERS_MODULE_OPTIONS, type UsersModuleOptions } from './users-config';
|
|
4
|
+
import { UsersStore } from './users.store';
|
|
5
5
|
import { mergeObjects, normalizeObject, toUserRecordDto } from './users.types';
|
|
6
6
|
|
|
7
7
|
@Injectable()
|
|
8
8
|
export class UsersService {
|
|
9
9
|
constructor(
|
|
10
|
-
|
|
11
|
-
private readonly persistence: AccountsPersistencePort,
|
|
10
|
+
private readonly usersStore: UsersStore,
|
|
12
11
|
@Inject(USERS_MODULE_OPTIONS)
|
|
13
12
|
private readonly usersModuleOptions: UsersModuleOptions,
|
|
14
13
|
) {}
|
|
15
14
|
|
|
16
15
|
async findById(userId: string) {
|
|
17
|
-
const user = await this.
|
|
16
|
+
const user = await this.usersStore.findById(userId);
|
|
18
17
|
return user ? toUserRecordDto(user) : null;
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -27,12 +26,12 @@ export class UsersService {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
async update(userId: string, input: UpdateUserRequest) {
|
|
30
|
-
const current = await this.
|
|
29
|
+
const current = await this.usersStore.findById(userId);
|
|
31
30
|
if (!current) {
|
|
32
31
|
throw new NotFoundException('User not found');
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
const updated = await this.
|
|
34
|
+
const updated = await this.usersStore.updateUser({
|
|
36
35
|
userId,
|
|
37
36
|
data: mergeObjects(current.data ?? this.usersModuleOptions.user, input.data),
|
|
38
37
|
});
|
|
@@ -40,12 +39,12 @@ export class UsersService {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
async updateProfile(userId: string, input: UpdateUserProfileRequest) {
|
|
43
|
-
const current = await this.
|
|
42
|
+
const current = await this.usersStore.findById(userId);
|
|
44
43
|
if (!current) {
|
|
45
44
|
throw new NotFoundException('User not found');
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
const updated = await this.
|
|
47
|
+
const updated = await this.usersStore.updateUserProfile({
|
|
49
48
|
userId,
|
|
50
49
|
name: input.name ?? current.profile?.name ?? null,
|
|
51
50
|
avatar: input.avatar ?? current.profile?.avatar ?? null,
|
|
@@ -55,12 +54,12 @@ export class UsersService {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
async updateSettings(userId: string, input: UpdateUserSettingsRequest) {
|
|
58
|
-
const current = await this.
|
|
57
|
+
const current = await this.usersStore.findById(userId);
|
|
59
58
|
if (!current) {
|
|
60
59
|
throw new NotFoundException('User not found');
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
const updated = await this.
|
|
62
|
+
const updated = await this.usersStore.updateUserSettings({
|
|
64
63
|
userId,
|
|
65
64
|
theme: input.theme ?? current.settings?.theme ?? null,
|
|
66
65
|
locale: input.locale ?? current.settings?.locale ?? null,
|
|
@@ -70,7 +69,7 @@ export class UsersService {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
async softDelete(userId: string): Promise<void> {
|
|
73
|
-
await this.
|
|
72
|
+
await this.usersStore.softDelete(userId, new Date());
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
resolveUserData(input: unknown) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import type { JsonObject } from '@forgeon/accounts-contracts';
|
|
3
|
+
import { PrismaService } from '@forgeon/db-prisma';
|
|
4
|
+
import type { UserRecord } from './users.types';
|
|
5
|
+
import { mapUserRecord, toPrismaJsonInput } from './users.types';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class UsersStore {
|
|
9
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
10
|
+
|
|
11
|
+
async findById(userId: string): Promise<UserRecord | null> {
|
|
12
|
+
const user = await this.prisma.user.findUnique({
|
|
13
|
+
where: { id: userId },
|
|
14
|
+
include: {
|
|
15
|
+
profile: true,
|
|
16
|
+
settings: true,
|
|
17
|
+
authIdentities: {
|
|
18
|
+
where: { provider: 'email' },
|
|
19
|
+
select: { providerId: true },
|
|
20
|
+
take: 1,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return user ? mapUserRecord(user) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async updateUser(input: { userId: string; data: JsonObject | null }): Promise<UserRecord> {
|
|
29
|
+
const user = await this.prisma.user.update({
|
|
30
|
+
where: { id: input.userId },
|
|
31
|
+
data: {
|
|
32
|
+
data: toPrismaJsonInput(input.data),
|
|
33
|
+
},
|
|
34
|
+
include: {
|
|
35
|
+
profile: true,
|
|
36
|
+
settings: true,
|
|
37
|
+
authIdentities: {
|
|
38
|
+
where: { provider: 'email' },
|
|
39
|
+
select: { providerId: true },
|
|
40
|
+
take: 1,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return mapUserRecord(user);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async updateUserProfile(input: {
|
|
49
|
+
userId: string;
|
|
50
|
+
name: string | null;
|
|
51
|
+
avatar: string | null;
|
|
52
|
+
data: JsonObject | null;
|
|
53
|
+
}): Promise<UserRecord> {
|
|
54
|
+
await this.prisma.userProfile.upsert({
|
|
55
|
+
where: { userId: input.userId },
|
|
56
|
+
create: {
|
|
57
|
+
userId: input.userId,
|
|
58
|
+
name: input.name,
|
|
59
|
+
avatar: input.avatar,
|
|
60
|
+
data: toPrismaJsonInput(input.data),
|
|
61
|
+
},
|
|
62
|
+
update: {
|
|
63
|
+
name: input.name,
|
|
64
|
+
avatar: input.avatar,
|
|
65
|
+
data: toPrismaJsonInput(input.data),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const user = await this.findById(input.userId);
|
|
70
|
+
if (!user) {
|
|
71
|
+
throw new NotFoundException('User not found');
|
|
72
|
+
}
|
|
73
|
+
return user;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async updateUserSettings(input: {
|
|
77
|
+
userId: string;
|
|
78
|
+
theme: string | null;
|
|
79
|
+
locale: string | null;
|
|
80
|
+
data: JsonObject | null;
|
|
81
|
+
}): Promise<UserRecord> {
|
|
82
|
+
await this.prisma.userSettings.upsert({
|
|
83
|
+
where: { userId: input.userId },
|
|
84
|
+
create: {
|
|
85
|
+
userId: input.userId,
|
|
86
|
+
theme: input.theme,
|
|
87
|
+
locale: input.locale,
|
|
88
|
+
data: toPrismaJsonInput(input.data),
|
|
89
|
+
},
|
|
90
|
+
update: {
|
|
91
|
+
theme: input.theme,
|
|
92
|
+
locale: input.locale,
|
|
93
|
+
data: toPrismaJsonInput(input.data),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const user = await this.findById(input.userId);
|
|
98
|
+
if (!user) {
|
|
99
|
+
throw new NotFoundException('User not found');
|
|
100
|
+
}
|
|
101
|
+
return user;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async softDelete(userId: string, deletedAt: Date): Promise<void> {
|
|
105
|
+
await this.prisma.user.update({
|
|
106
|
+
where: { id: userId },
|
|
107
|
+
data: {
|
|
108
|
+
status: 'deleted',
|
|
109
|
+
deletedAt,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -34,6 +34,54 @@ export function mergeObjects(baseValue: unknown, patchValue: unknown): JsonObjec
|
|
|
34
34
|
return Object.keys(merged).length > 0 ? merged : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function toPrismaJsonInput(value: JsonObject | null) {
|
|
38
|
+
return (value ?? undefined) as never;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mapUserRecord(source: {
|
|
42
|
+
id: string;
|
|
43
|
+
status: string;
|
|
44
|
+
data: unknown;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
updatedAt: Date;
|
|
47
|
+
deletedAt: Date | null;
|
|
48
|
+
profile?: {
|
|
49
|
+
name: string | null;
|
|
50
|
+
avatar: string | null;
|
|
51
|
+
data: unknown;
|
|
52
|
+
} | null;
|
|
53
|
+
settings?: {
|
|
54
|
+
theme: string | null;
|
|
55
|
+
locale: string | null;
|
|
56
|
+
data: unknown;
|
|
57
|
+
} | null;
|
|
58
|
+
authIdentities?: Array<{ providerId: string }>;
|
|
59
|
+
}): UserRecord {
|
|
60
|
+
return {
|
|
61
|
+
id: source.id,
|
|
62
|
+
email: source.authIdentities?.[0]?.providerId ?? null,
|
|
63
|
+
status: source.status,
|
|
64
|
+
data: normalizeObject(source.data),
|
|
65
|
+
createdAt: source.createdAt,
|
|
66
|
+
updatedAt: source.updatedAt,
|
|
67
|
+
deletedAt: source.deletedAt,
|
|
68
|
+
profile: source.profile
|
|
69
|
+
? {
|
|
70
|
+
name: source.profile.name,
|
|
71
|
+
avatar: source.profile.avatar,
|
|
72
|
+
data: normalizeObject(source.profile.data),
|
|
73
|
+
}
|
|
74
|
+
: null,
|
|
75
|
+
settings: source.settings
|
|
76
|
+
? {
|
|
77
|
+
theme: source.settings.theme,
|
|
78
|
+
locale: source.settings.locale,
|
|
79
|
+
data: normalizeObject(source.settings.data),
|
|
80
|
+
}
|
|
81
|
+
: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
37
85
|
export function toProfileDto(record: UserRecord['profile']): UserProfileDto {
|
|
38
86
|
return {
|
|
39
87
|
name: record?.name ?? null,
|