@hed-hog/core 0.0.296 → 0.0.298

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.
Files changed (67) hide show
  1. package/dist/auth/auth.controller.d.ts +14 -14
  2. package/dist/auth/auth.service.d.ts +14 -14
  3. package/dist/challenge/challenge.service.d.ts +2 -2
  4. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +15 -1
  5. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  6. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
  7. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  8. package/dist/dashboard/dashboard-core/dashboard-core.module.d.ts.map +1 -1
  9. package/dist/dashboard/dashboard-core/dashboard-core.module.js +6 -1
  10. package/dist/dashboard/dashboard-core/dashboard-core.module.js.map +1 -1
  11. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +175 -3
  12. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  13. package/dist/dashboard/dashboard-core/dashboard-core.service.js +531 -5
  14. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  15. package/dist/file/file.controller.d.ts.map +1 -1
  16. package/dist/file/file.controller.js +16 -0
  17. package/dist/file/file.controller.js.map +1 -1
  18. package/dist/file/file.service.d.ts +7 -1
  19. package/dist/file/file.service.d.ts.map +1 -1
  20. package/dist/file/file.service.js +38 -1
  21. package/dist/file/file.service.js.map +1 -1
  22. package/dist/file/provider/s3.provider.d.ts +1 -0
  23. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  24. package/dist/file/provider/s3.provider.js +38 -29
  25. package/dist/file/provider/s3.provider.js.map +1 -1
  26. package/dist/oauth/oauth.service.d.ts.map +1 -1
  27. package/dist/oauth/oauth.service.js +2 -1
  28. package/dist/oauth/oauth.service.js.map +1 -1
  29. package/dist/profile/profile.controller.d.ts +6 -6
  30. package/dist/profile/profile.service.d.ts +6 -6
  31. package/dist/session/session.controller.d.ts +2 -2
  32. package/dist/session/session.service.d.ts +3 -3
  33. package/dist/setting/setting.controller.d.ts +9 -9
  34. package/dist/setting/setting.service.d.ts +10 -10
  35. package/dist/user/constants/user.constants.d.ts +1 -0
  36. package/dist/user/constants/user.constants.d.ts.map +1 -1
  37. package/dist/user/constants/user.constants.js +2 -1
  38. package/dist/user/constants/user.constants.js.map +1 -1
  39. package/dist/user/user.controller.d.ts +15 -15
  40. package/dist/user/user.service.d.ts +39 -39
  41. package/dist/user/user.service.d.ts.map +1 -1
  42. package/dist/user/user.service.js +2 -1
  43. package/dist/user/user.service.js.map +1 -1
  44. package/hedhog/data/dashboard_item.yaml +11 -11
  45. package/hedhog/data/route.yaml +8 -0
  46. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +76 -15
  47. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +85 -61
  48. package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +139 -280
  49. package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +161 -407
  50. package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +150 -271
  51. package/hedhog/frontend/app/dashboard/components/widgets/profile-card.tsx.ejs +3 -3
  52. package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +161 -305
  53. package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +184 -246
  54. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +12 -14
  55. package/hedhog/frontend/messages/en.json +90 -0
  56. package/hedhog/frontend/messages/pt.json +90 -0
  57. package/hedhog/table/mail_sent_user.yaml +75 -0
  58. package/package.json +4 -4
  59. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
  60. package/src/dashboard/dashboard-core/dashboard-core.module.ts +6 -1
  61. package/src/dashboard/dashboard-core/dashboard-core.service.ts +766 -3
  62. package/src/file/file.controller.ts +37 -13
  63. package/src/file/file.service.ts +47 -5
  64. package/src/file/provider/s3.provider.ts +39 -29
  65. package/src/oauth/oauth.service.ts +8 -7
  66. package/src/user/constants/user.constants.ts +1 -0
  67. package/src/user/user.service.ts +2 -1
@@ -1,10 +1,646 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { PrismaService } from '@hed-hog/api-prisma';
3
- import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
3
+ import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common';
4
+ import { SettingService } from '../../setting/setting.service';
5
+
6
+ type DashboardCoreConfigStatus = {
7
+ isConfigured: boolean;
8
+ };
9
+
10
+ type DashboardCoreLocaleConfigOverview = {
11
+ status: DashboardCoreConfigStatus & {
12
+ enabledLocaleCount: number;
13
+ disabledLocaleCount: number;
14
+ };
15
+ settings: {
16
+ dateFormat: string | null;
17
+ timeFormat: string | null;
18
+ timezone: string | null;
19
+ };
20
+ locales: Array<{
21
+ id: number;
22
+ code: string;
23
+ region: string;
24
+ name: string;
25
+ enabled: boolean;
26
+ }>;
27
+ };
28
+
29
+ type DashboardCoreMailProviderOverview = {
30
+ id: string;
31
+ label: string;
32
+ selected: boolean;
33
+ configured: boolean;
34
+ missingKeys: string[];
35
+ };
36
+
37
+ type DashboardCoreMailConfigOverview = {
38
+ status: DashboardCoreConfigStatus & {
39
+ selectedProvider: string | null;
40
+ configuredProvider: string | null;
41
+ };
42
+ sender: {
43
+ from: string | null;
44
+ };
45
+ metrics: {
46
+ templateCount: number;
47
+ sentCount: number;
48
+ sentLast30Days: number;
49
+ };
50
+ providers: DashboardCoreMailProviderOverview[];
51
+ };
52
+
53
+ type DashboardCoreOAuthProviderOverview = {
54
+ id: string;
55
+ label: string;
56
+ enabled: boolean;
57
+ configured: boolean;
58
+ missingKeys: string[];
59
+ scopesCount: number;
60
+ connectedUsers: number;
61
+ };
62
+
63
+ type DashboardCoreOAuthConfigOverview = {
64
+ status: DashboardCoreConfigStatus & {
65
+ enabledProviderCount: number;
66
+ configuredProviderCount: number;
67
+ connectedAccountCount: number;
68
+ };
69
+ providers: DashboardCoreOAuthProviderOverview[];
70
+ };
71
+
72
+ type DashboardCoreStorageConfigOverview = {
73
+ status: DashboardCoreConfigStatus & {
74
+ totalProfiles: number;
75
+ activeProfiles: number;
76
+ defaultProfileId: number | null;
77
+ };
78
+ providers: Array<{
79
+ providerType: string;
80
+ total: number;
81
+ active: number;
82
+ defaults: number;
83
+ }>;
84
+ profiles: Array<{
85
+ id: number;
86
+ name: string;
87
+ providerType: string;
88
+ bucketName: string;
89
+ region: string | null;
90
+ endpointUrl: string | null;
91
+ basePath: string | null;
92
+ pathTemplate: string | null;
93
+ forcePathStyle: boolean;
94
+ isDefault: boolean;
95
+ isActive: boolean;
96
+ testStatus: string;
97
+ lastTestedAt: Date | null;
98
+ updatedAt: Date;
99
+ }>;
100
+ };
101
+
102
+ type DashboardCoreThemePaletteMode = {
103
+ primary: string | null;
104
+ primaryForeground: string | null;
105
+ secondary: string | null;
106
+ secondaryForeground: string | null;
107
+ accent: string | null;
108
+ accentForeground: string | null;
109
+ muted: string | null;
110
+ mutedForeground: string | null;
111
+ background: string | null;
112
+ backgroundForeground: string | null;
113
+ card: string | null;
114
+ cardForeground: string | null;
115
+ };
116
+
117
+ type DashboardCoreThemeConfigOverview = {
118
+ status: DashboardCoreConfigStatus & {
119
+ configuredTokenCount: number;
120
+ };
121
+ branding: {
122
+ systemName: string | null;
123
+ systemSlogan: string | null;
124
+ iconUrl: string | null;
125
+ imageUrl: string | null;
126
+ };
127
+ presentation: {
128
+ mode: string | null;
129
+ font: string | null;
130
+ textSize: string | null;
131
+ radius: string | null;
132
+ };
133
+ palette: {
134
+ light: DashboardCoreThemePaletteMode;
135
+ dark: DashboardCoreThemePaletteMode;
136
+ };
137
+ };
138
+
139
+ export type DashboardCoreConfigOverview = {
140
+ localeConfig: DashboardCoreLocaleConfigOverview;
141
+ mailConfig: DashboardCoreMailConfigOverview;
142
+ oauthConfig: DashboardCoreOAuthConfigOverview;
143
+ storageConfig: DashboardCoreStorageConfigOverview;
144
+ themeConfig: DashboardCoreThemeConfigOverview;
145
+ };
146
+
147
+ const MAIL_PROVIDER_REQUIREMENTS: Record<string, string[]> = {
148
+ SMTP: ['mail-from', 'mail-smtp-host', 'mail-smtp-port', 'mail-client-secret'],
149
+ GMAIL: [
150
+ 'mail-from',
151
+ 'mail-gmail-client-id',
152
+ 'mail-gmail-client-secret',
153
+ 'mail-gmail-refresh-token',
154
+ ],
155
+ SES: [
156
+ 'mail-from',
157
+ 'mail-aws-access-key-id',
158
+ 'mail-aws-secret-access-key',
159
+ 'mail-aws-region',
160
+ ],
161
+ };
162
+
163
+ const MAIL_PROVIDER_LABELS: Record<string, string> = {
164
+ SMTP: 'SMTP',
165
+ GMAIL: 'Gmail',
166
+ SES: 'Amazon SES',
167
+ };
168
+
169
+ const OAUTH_PROVIDER_DEFINITIONS = [
170
+ {
171
+ id: 'google',
172
+ label: 'Google',
173
+ requiredKeys: ['google_client_id', 'google_client_secret', 'url'],
174
+ scopeKey: 'google_scopes',
175
+ },
176
+ {
177
+ id: 'facebook',
178
+ label: 'Facebook',
179
+ requiredKeys: ['facebook_client_id', 'facebook_client_secret', 'url'],
180
+ scopeKey: 'facebook_scopes',
181
+ },
182
+ {
183
+ id: 'github',
184
+ label: 'GitHub',
185
+ requiredKeys: ['github_client_id', 'github_client_secret', 'api-url'],
186
+ scopeKey: 'github_scopes',
187
+ },
188
+ {
189
+ id: 'microsoft',
190
+ label: 'Microsoft',
191
+ requiredKeys: ['microsoft_client_id', 'microsoft_client_secret', 'url'],
192
+ scopeKey: 'microsoft_scopes',
193
+ },
194
+ {
195
+ id: 'microsoft_entra_id',
196
+ label: 'Microsoft Entra ID',
197
+ requiredKeys: [
198
+ 'microsoft_entra_id_client_id',
199
+ 'microsoft_entra_id_client_secret',
200
+ 'microsoft_entra_id_tenant_id',
201
+ 'url',
202
+ ],
203
+ scopeKey: 'microsoft_entra_id_scopes',
204
+ },
205
+ ] as const;
206
+
207
+ const LOCALE_SETTING_KEYS = ['date-format', 'time-format', 'timezone'] as const;
208
+
209
+ const MAIL_SETTING_KEYS = [
210
+ 'mail-provider',
211
+ 'mail-from',
212
+ 'mail-gmail-client-id',
213
+ 'mail-gmail-client-secret',
214
+ 'mail-gmail-refresh-token',
215
+ 'mail-smtp-host',
216
+ 'mail-smtp-port',
217
+ 'mail-smtp-secure',
218
+ 'mail-client-secret',
219
+ 'mail-aws-access-key-id',
220
+ 'mail-aws-secret-access-key',
221
+ 'mail-aws-region',
222
+ ] as const;
223
+
224
+ const OAUTH_SETTING_KEYS = [
225
+ 'providers',
226
+ 'url',
227
+ 'api-url',
228
+ 'google_client_id',
229
+ 'google_client_secret',
230
+ 'google_scopes',
231
+ 'facebook_client_id',
232
+ 'facebook_client_secret',
233
+ 'facebook_scopes',
234
+ 'github_client_id',
235
+ 'github_client_secret',
236
+ 'github_scopes',
237
+ 'microsoft_client_id',
238
+ 'microsoft_client_secret',
239
+ 'microsoft_scopes',
240
+ 'microsoft_entra_id_client_id',
241
+ 'microsoft_entra_id_client_secret',
242
+ 'microsoft_entra_id_tenant_id',
243
+ 'microsoft_entra_id_scopes',
244
+ ] as const;
245
+
246
+ const THEME_SETTING_KEYS = [
247
+ 'system-name',
248
+ 'system-slogan',
249
+ 'icon-url',
250
+ 'image-url',
251
+ 'theme-mode',
252
+ 'theme-font',
253
+ 'theme-text-size',
254
+ 'theme-radius',
255
+ 'theme-primary-light',
256
+ 'theme-primary-foreground-light',
257
+ 'theme-secondary-light',
258
+ 'theme-secondary-foreground-light',
259
+ 'theme-accent-light',
260
+ 'theme-accent-foreground-light',
261
+ 'theme-muted-light',
262
+ 'theme-muted-foreground-light',
263
+ 'theme-background-light',
264
+ 'theme-background-foreground-light',
265
+ 'theme-card-light',
266
+ 'theme-card-foreground-light',
267
+ 'theme-primary-dark',
268
+ 'theme-primary-foreground-dark',
269
+ 'theme-secondary-dark',
270
+ 'theme-secondary-foreground-dark',
271
+ 'theme-accent-dark',
272
+ 'theme-accent-foreground-dark',
273
+ 'theme-muted-dark',
274
+ 'theme-muted-foreground-dark',
275
+ 'theme-background-dark',
276
+ 'theme-background-foreground-dark',
277
+ 'theme-card-dark',
278
+ 'theme-card-foreground-dark',
279
+ ] as const;
4
280
 
5
281
  @Injectable()
6
282
  export class DashboardCoreService {
7
- constructor(private readonly prismaService: PrismaService) {}
283
+ private readonly logger = new Logger(DashboardCoreService.name);
284
+
285
+ constructor(
286
+ private readonly prismaService: PrismaService,
287
+ private readonly settingService: SettingService,
288
+ ) {}
289
+
290
+ async getConfigOverview(): Promise<DashboardCoreConfigOverview> {
291
+ const [localeConfig, mailConfig, oauthConfig, storageConfig, themeConfig] =
292
+ await Promise.all([
293
+ this.getLocaleConfigOverview(),
294
+ this.getMailConfigOverview(),
295
+ this.getOAuthConfigOverview(),
296
+ this.getStorageConfigOverview(),
297
+ this.getThemeConfigOverview(),
298
+ ]);
299
+
300
+ return {
301
+ localeConfig,
302
+ mailConfig,
303
+ oauthConfig,
304
+ storageConfig,
305
+ themeConfig,
306
+ };
307
+ }
308
+
309
+ private async getLocaleConfigOverview(): Promise<DashboardCoreLocaleConfigOverview> {
310
+ const [settings, locales] = await Promise.all([
311
+ this.settingService.getSettingValues([...LOCALE_SETTING_KEYS]),
312
+ this.prismaService.locale.findMany({
313
+ orderBy: [{ enabled: 'desc' }, { code: 'asc' }],
314
+ select: {
315
+ id: true,
316
+ code: true,
317
+ region: true,
318
+ name: true,
319
+ enabled: true,
320
+ },
321
+ }),
322
+ ]);
323
+
324
+ const enabledLocaleCount = locales.filter((locale) => locale.enabled).length;
325
+ const disabledLocaleCount = locales.length - enabledLocaleCount;
326
+
327
+ return {
328
+ status: {
329
+ isConfigured:
330
+ enabledLocaleCount > 0 &&
331
+ this.hasConfigValue(settings['date-format']) &&
332
+ this.hasConfigValue(settings['time-format']) &&
333
+ this.hasConfigValue(settings['timezone']),
334
+ enabledLocaleCount,
335
+ disabledLocaleCount,
336
+ },
337
+ settings: {
338
+ dateFormat: this.toNullableString(settings['date-format']),
339
+ timeFormat: this.toNullableString(settings['time-format']),
340
+ timezone: this.toNullableString(settings['timezone']),
341
+ },
342
+ locales,
343
+ };
344
+ }
345
+
346
+ private async getMailConfigOverview(): Promise<DashboardCoreMailConfigOverview> {
347
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
348
+
349
+ const [settings, templateCount, sentCount, sentLast30Days] = await Promise.all([
350
+ this.settingService.getSettingValues([...MAIL_SETTING_KEYS]),
351
+ this.prismaService.mail.count(),
352
+ this.prismaService.mail_sent.count(),
353
+ this.prismaService.mail_sent.count({
354
+ where: {
355
+ created_at: {
356
+ gte: thirtyDaysAgo,
357
+ },
358
+ },
359
+ }),
360
+ ]);
361
+
362
+ const selectedProvider = this.toNullableUppercaseString(settings['mail-provider']);
363
+
364
+ const providers = Object.entries(MAIL_PROVIDER_REQUIREMENTS).map(
365
+ ([providerId, requiredKeys]) => {
366
+ const missingKeys = this.getMissingSettingKeys(settings, requiredKeys);
367
+
368
+ return {
369
+ id: providerId,
370
+ label: MAIL_PROVIDER_LABELS[providerId] ?? providerId,
371
+ selected: providerId === selectedProvider,
372
+ configured: missingKeys.length === 0,
373
+ missingKeys,
374
+ };
375
+ },
376
+ );
377
+
378
+ const configuredProvider = providers.find(
379
+ (provider) => provider.id === selectedProvider && provider.configured,
380
+ );
381
+
382
+ return {
383
+ status: {
384
+ isConfigured: configuredProvider !== undefined,
385
+ selectedProvider,
386
+ configuredProvider: configuredProvider?.id ?? null,
387
+ },
388
+ sender: {
389
+ from: this.toNullableString(settings['mail-from']),
390
+ },
391
+ metrics: {
392
+ templateCount,
393
+ sentCount,
394
+ sentLast30Days,
395
+ },
396
+ providers,
397
+ };
398
+ }
399
+
400
+ private async getOAuthConfigOverview(): Promise<DashboardCoreOAuthConfigOverview> {
401
+ const [settings, connectedAccounts] = await Promise.all([
402
+ this.settingService.getSettingValues([...OAUTH_SETTING_KEYS]),
403
+ this.prismaService.user_account.groupBy({
404
+ by: ['provider'],
405
+ _count: {
406
+ _all: true,
407
+ },
408
+ }),
409
+ ]);
410
+
411
+ const enabledProviders = this.normalizeProviderList(settings['providers']);
412
+ const connectedAccountsByProvider = new Map(
413
+ connectedAccounts.map((entry) => [String(entry.provider), entry._count._all]),
414
+ );
415
+
416
+ const providers = OAUTH_PROVIDER_DEFINITIONS.map((provider) => {
417
+ const missingKeys = this.getMissingSettingKeys(settings, provider.requiredKeys);
418
+ const scopes = Array.isArray(settings[provider.scopeKey])
419
+ ? (settings[provider.scopeKey] as unknown[])
420
+ : [];
421
+
422
+ return {
423
+ id: provider.id,
424
+ label: provider.label,
425
+ enabled: enabledProviders.includes(provider.id),
426
+ configured: missingKeys.length === 0,
427
+ missingKeys,
428
+ scopesCount: scopes.length,
429
+ connectedUsers: connectedAccountsByProvider.get(provider.id) ?? 0,
430
+ };
431
+ });
432
+
433
+ return {
434
+ status: {
435
+ isConfigured: providers.some((provider) => provider.enabled && provider.configured),
436
+ enabledProviderCount: providers.filter((provider) => provider.enabled).length,
437
+ configuredProviderCount: providers.filter((provider) => provider.configured).length,
438
+ connectedAccountCount: providers.reduce(
439
+ (total, provider) => total + provider.connectedUsers,
440
+ 0,
441
+ ),
442
+ },
443
+ providers,
444
+ };
445
+ }
446
+
447
+ private async getStorageConfigOverview(): Promise<DashboardCoreStorageConfigOverview> {
448
+ const profiles = await this.prismaService.storage_profile.findMany({
449
+ where: {
450
+ deleted_at: null,
451
+ },
452
+ orderBy: [{ is_default: 'desc' }, { is_active: 'desc' }, { name: 'asc' }],
453
+ select: {
454
+ id: true,
455
+ name: true,
456
+ provider_type: true,
457
+ bucket_name: true,
458
+ region: true,
459
+ endpoint_url: true,
460
+ base_path: true,
461
+ path_template: true,
462
+ force_path_style: true,
463
+ is_default: true,
464
+ is_active: true,
465
+ test_status: true,
466
+ last_tested_at: true,
467
+ updated_at: true,
468
+ },
469
+ });
470
+
471
+ const providerMap = new Map<
472
+ string,
473
+ { providerType: string; total: number; active: number; defaults: number }
474
+ >();
475
+
476
+ profiles.forEach((profile) => {
477
+ const providerType = String(profile.provider_type);
478
+ const current = providerMap.get(providerType) ?? {
479
+ providerType,
480
+ total: 0,
481
+ active: 0,
482
+ defaults: 0,
483
+ };
484
+
485
+ current.total += 1;
486
+ current.active += profile.is_active ? 1 : 0;
487
+ current.defaults += profile.is_default ? 1 : 0;
488
+ providerMap.set(providerType, current);
489
+ });
490
+
491
+ const activeProfiles = profiles.filter((profile) => profile.is_active).length;
492
+ const defaultProfile = profiles.find((profile) => profile.is_default) ?? null;
493
+
494
+ return {
495
+ status: {
496
+ isConfigured: activeProfiles > 0,
497
+ totalProfiles: profiles.length,
498
+ activeProfiles,
499
+ defaultProfileId: defaultProfile?.id ?? null,
500
+ },
501
+ providers: Array.from(providerMap.values()),
502
+ profiles: profiles.map((profile) => ({
503
+ id: profile.id,
504
+ name: profile.name,
505
+ providerType: String(profile.provider_type),
506
+ bucketName: profile.bucket_name,
507
+ region: profile.region,
508
+ endpointUrl: profile.endpoint_url,
509
+ basePath: profile.base_path,
510
+ pathTemplate: profile.path_template,
511
+ forcePathStyle: profile.force_path_style,
512
+ isDefault: profile.is_default,
513
+ isActive: profile.is_active,
514
+ testStatus: String(profile.test_status),
515
+ lastTestedAt: profile.last_tested_at,
516
+ updatedAt: profile.updated_at,
517
+ })),
518
+ };
519
+ }
520
+
521
+ private async getThemeConfigOverview(): Promise<DashboardCoreThemeConfigOverview> {
522
+ const settings = await this.settingService.getSettingValues([...THEME_SETTING_KEYS]);
523
+ const configuredTokenCount = THEME_SETTING_KEYS.filter((key) =>
524
+ this.hasConfigValue(settings[key]),
525
+ ).length;
526
+
527
+ return {
528
+ status: {
529
+ isConfigured:
530
+ this.hasConfigValue(settings['system-name']) ||
531
+ this.hasConfigValue(settings['theme-primary-light']) ||
532
+ this.hasConfigValue(settings['theme-primary-dark']),
533
+ configuredTokenCount,
534
+ },
535
+ branding: {
536
+ systemName: this.toNullableString(settings['system-name']),
537
+ systemSlogan: this.toNullableString(settings['system-slogan']),
538
+ iconUrl: this.toNullableString(settings['icon-url']),
539
+ imageUrl: this.toNullableString(settings['image-url']),
540
+ },
541
+ presentation: {
542
+ mode: this.toNullableString(settings['theme-mode']),
543
+ font: this.toNullableString(settings['theme-font']),
544
+ textSize: this.toNullableString(settings['theme-text-size']),
545
+ radius: this.toNullableString(settings['theme-radius']),
546
+ },
547
+ palette: {
548
+ light: {
549
+ primary: this.toNullableString(settings['theme-primary-light']),
550
+ primaryForeground: this.toNullableString(
551
+ settings['theme-primary-foreground-light'],
552
+ ),
553
+ secondary: this.toNullableString(settings['theme-secondary-light']),
554
+ secondaryForeground: this.toNullableString(
555
+ settings['theme-secondary-foreground-light'],
556
+ ),
557
+ accent: this.toNullableString(settings['theme-accent-light']),
558
+ accentForeground: this.toNullableString(
559
+ settings['theme-accent-foreground-light'],
560
+ ),
561
+ muted: this.toNullableString(settings['theme-muted-light']),
562
+ mutedForeground: this.toNullableString(
563
+ settings['theme-muted-foreground-light'],
564
+ ),
565
+ background: this.toNullableString(settings['theme-background-light']),
566
+ backgroundForeground: this.toNullableString(
567
+ settings['theme-background-foreground-light'],
568
+ ),
569
+ card: this.toNullableString(settings['theme-card-light']),
570
+ cardForeground: this.toNullableString(
571
+ settings['theme-card-foreground-light'],
572
+ ),
573
+ },
574
+ dark: {
575
+ primary: this.toNullableString(settings['theme-primary-dark']),
576
+ primaryForeground: this.toNullableString(
577
+ settings['theme-primary-foreground-dark'],
578
+ ),
579
+ secondary: this.toNullableString(settings['theme-secondary-dark']),
580
+ secondaryForeground: this.toNullableString(
581
+ settings['theme-secondary-foreground-dark'],
582
+ ),
583
+ accent: this.toNullableString(settings['theme-accent-dark']),
584
+ accentForeground: this.toNullableString(
585
+ settings['theme-accent-foreground-dark'],
586
+ ),
587
+ muted: this.toNullableString(settings['theme-muted-dark']),
588
+ mutedForeground: this.toNullableString(
589
+ settings['theme-muted-foreground-dark'],
590
+ ),
591
+ background: this.toNullableString(settings['theme-background-dark']),
592
+ backgroundForeground: this.toNullableString(
593
+ settings['theme-background-foreground-dark'],
594
+ ),
595
+ card: this.toNullableString(settings['theme-card-dark']),
596
+ cardForeground: this.toNullableString(
597
+ settings['theme-card-foreground-dark'],
598
+ ),
599
+ },
600
+ },
601
+ };
602
+ }
603
+
604
+ private getMissingSettingKeys(
605
+ settings: Record<string, unknown>,
606
+ requiredKeys: readonly string[],
607
+ ): string[] {
608
+ return requiredKeys.filter((key) => !this.hasConfigValue(settings[key]));
609
+ }
610
+
611
+ private hasConfigValue(value: unknown): boolean {
612
+ if (Array.isArray(value)) {
613
+ return value.length > 0;
614
+ }
615
+
616
+ if (typeof value === 'string') {
617
+ return value.trim().length > 0;
618
+ }
619
+
620
+ return value !== null && value !== undefined;
621
+ }
622
+
623
+ private toNullableString(value: unknown): string | null {
624
+ if (typeof value !== 'string') {
625
+ return value === null || value === undefined ? null : String(value);
626
+ }
627
+
628
+ const normalized = value.trim();
629
+ return normalized.length > 0 ? normalized : null;
630
+ }
631
+
632
+ private toNullableUppercaseString(value: unknown): string | null {
633
+ const normalized = this.toNullableString(value);
634
+ return normalized ? normalized.toUpperCase() : null;
635
+ }
636
+
637
+ private normalizeProviderList(value: unknown): string[] {
638
+ if (!Array.isArray(value)) {
639
+ return [];
640
+ }
641
+
642
+ return value.map((provider) => String(provider).toLowerCase());
643
+ }
8
644
 
9
645
  async getHome(userId: number, locale: string) {
10
646
  const user = await this.prismaService.user.findUnique({
@@ -864,11 +1500,137 @@ export class DashboardCoreService {
864
1500
  }));
865
1501
  }
866
1502
 
1503
+ async getEmailNotificationStats(userId: number) {
1504
+ const now = new Date();
1505
+ const periodStart = new Date(now);
1506
+ periodStart.setHours(0, 0, 0, 0);
1507
+ periodStart.setDate(periodStart.getDate() - 13);
1508
+
1509
+ const periodEnd = new Date(now);
1510
+ periodEnd.setHours(23, 59, 59, 999);
1511
+
1512
+ const toDateKey = (date: Date) => {
1513
+ const year = date.getFullYear();
1514
+ const month = String(date.getMonth() + 1).padStart(2, '0');
1515
+ const day = String(date.getDate()).padStart(2, '0');
1516
+ return `${year}-${month}-${day}`;
1517
+ };
1518
+
1519
+ try {
1520
+ const [cardsRows, chartRows] = await Promise.all([
1521
+ this.prismaService.$queryRaw<
1522
+ Array<{ received: bigint; read: bigint; unread: bigint; error: bigint }>
1523
+ >`
1524
+ SELECT
1525
+ COUNT(*) FILTER (WHERE "status" IN ('received', 'read'))::bigint as received,
1526
+ COUNT(*) FILTER (WHERE "status" = 'read')::bigint as read,
1527
+ COUNT(*) FILTER (WHERE "status" = 'received' AND "read_at" IS NULL)::bigint as unread,
1528
+ COUNT(*) FILTER (WHERE "status" = 'error')::bigint as error
1529
+ FROM "mail_sent_user"
1530
+ WHERE "user_id" = ${userId}
1531
+ AND "created_at" >= ${periodStart}
1532
+ AND "created_at" <= ${periodEnd}
1533
+ `,
1534
+ this.prismaService.$queryRaw<Array<{ date: Date; received: bigint; read: bigint }>>`
1535
+ SELECT
1536
+ DATE("created_at") as date,
1537
+ COUNT(*) FILTER (WHERE "status" IN ('received', 'read'))::bigint as received,
1538
+ COUNT(*) FILTER (WHERE "status" = 'read')::bigint as read
1539
+ FROM "mail_sent_user"
1540
+ WHERE "user_id" = ${userId}
1541
+ AND "created_at" >= ${periodStart}
1542
+ AND "created_at" <= ${periodEnd}
1543
+ GROUP BY DATE("created_at")
1544
+ ORDER BY date ASC
1545
+ `,
1546
+ ]);
1547
+
1548
+ const cards = cardsRows[0] ?? {
1549
+ received: BigInt(0),
1550
+ read: BigInt(0),
1551
+ unread: BigInt(0),
1552
+ error: BigInt(0),
1553
+ };
1554
+
1555
+ const chartMap = new Map<string, { received: number; read: number }>();
1556
+ for (const row of chartRows) {
1557
+ const rowDate = new Date(row.date);
1558
+ rowDate.setHours(0, 0, 0, 0);
1559
+ const key = toDateKey(rowDate);
1560
+
1561
+ chartMap.set(key, {
1562
+ received: Number(row.received),
1563
+ read: Number(row.read),
1564
+ });
1565
+ }
1566
+
1567
+ const dateFormatter = new Intl.DateTimeFormat('pt-BR', {
1568
+ day: '2-digit',
1569
+ month: '2-digit',
1570
+ });
1571
+
1572
+ const chart: Array<{ date: string; received: number; read: number }> = [];
1573
+ for (let i = 13; i >= 0; i--) {
1574
+ const day = new Date(now);
1575
+ day.setDate(day.getDate() - i);
1576
+ day.setHours(0, 0, 0, 0);
1577
+
1578
+ const key = toDateKey(day);
1579
+ const values = chartMap.get(key) ?? { received: 0, read: 0 };
1580
+
1581
+ chart.push({
1582
+ date: dateFormatter.format(day),
1583
+ received: values.received,
1584
+ read: values.read,
1585
+ });
1586
+ }
1587
+
1588
+ return {
1589
+ cards: {
1590
+ received: Number(cards.received),
1591
+ read: Number(cards.read),
1592
+ unread: Number(cards.unread),
1593
+ error: Number(cards.error),
1594
+ },
1595
+ chart,
1596
+ };
1597
+ } catch (error) {
1598
+ this.logger.error('Error loading email notification stats:', error);
1599
+
1600
+ const dateFormatter = new Intl.DateTimeFormat('pt-BR', {
1601
+ day: '2-digit',
1602
+ month: '2-digit',
1603
+ });
1604
+
1605
+ const chart: Array<{ date: string; received: number; read: number }> = [];
1606
+ for (let i = 13; i >= 0; i--) {
1607
+ const day = new Date(now);
1608
+ day.setDate(day.getDate() - i);
1609
+ chart.push({
1610
+ date: dateFormatter.format(day),
1611
+ received: 0,
1612
+ read: 0,
1613
+ });
1614
+ }
1615
+
1616
+ return {
1617
+ cards: {
1618
+ received: 0,
1619
+ read: 0,
1620
+ unread: 0,
1621
+ error: 0,
1622
+ },
1623
+ chart,
1624
+ };
1625
+ }
1626
+ }
1627
+
867
1628
  async getWidgetsData(userId: number, locale: string) {
868
- const [accountSecurity, activityTimeline, loginHistory, profile, quickStats, userRoles, userSessions] =
1629
+ const [accountSecurity, activityTimeline, emailNotifications, loginHistory, profile, quickStats, userRoles, userSessions] =
869
1630
  await Promise.all([
870
1631
  this.getAccountSecurity(userId),
871
1632
  this.getActivityTimeline(userId),
1633
+ this.getEmailNotificationStats(userId),
872
1634
  this.getLoginHistory(userId),
873
1635
  this.getProfile(userId),
874
1636
  this.getQuickStats(userId),
@@ -879,6 +1641,7 @@ export class DashboardCoreService {
879
1641
  return {
880
1642
  accountSecurity,
881
1643
  activityTimeline,
1644
+ emailNotifications,
882
1645
  loginHistory,
883
1646
  profile,
884
1647
  quickStats,