@hed-hog/core 0.0.297 → 0.0.299

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 (183) hide show
  1. package/dist/auth/auth.controller.d.ts +10 -10
  2. package/dist/auth/auth.service.d.ts +10 -10
  3. package/dist/dashboard/dashboard/dashboard.controller.d.ts +3 -0
  4. package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
  5. package/dist/dashboard/dashboard/dashboard.service.d.ts +3 -0
  6. package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +12 -0
  8. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts.map +1 -1
  9. package/dist/dashboard/dashboard-component/dashboard-component.controller.js +22 -0
  10. package/dist/dashboard/dashboard-component/dashboard-component.controller.js.map +1 -1
  11. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +15 -0
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts.map +1 -1
  13. package/dist/dashboard/dashboard-component/dashboard-component.service.js +110 -3
  14. package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
  15. package/dist/dashboard/dashboard-component/dto/create.dto.d.ts +1 -0
  16. package/dist/dashboard/dashboard-component/dto/create.dto.d.ts.map +1 -1
  17. package/dist/dashboard/dashboard-component/dto/create.dto.js +5 -0
  18. package/dist/dashboard/dashboard-component/dto/create.dto.js.map +1 -1
  19. package/dist/dashboard/dashboard-component/dto/update.dto.d.ts +1 -0
  20. package/dist/dashboard/dashboard-component/dto/update.dto.d.ts.map +1 -1
  21. package/dist/dashboard/dashboard-component/dto/update.dto.js +5 -0
  22. package/dist/dashboard/dashboard-component/dto/update.dto.js.map +1 -1
  23. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +1 -0
  24. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts.map +1 -1
  25. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +1 -0
  26. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts.map +1 -1
  27. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +21 -1
  28. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  29. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
  30. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  31. package/dist/dashboard/dashboard-core/dashboard-core.module.d.ts.map +1 -1
  32. package/dist/dashboard/dashboard-core/dashboard-core.module.js +6 -1
  33. package/dist/dashboard/dashboard-core/dashboard-core.module.js.map +1 -1
  34. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +180 -2
  35. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  36. package/dist/dashboard/dashboard-core/dashboard-core.service.js +619 -9
  37. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  38. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +1 -0
  39. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
  40. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +1 -0
  41. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
  42. package/dist/file/file.controller.d.ts.map +1 -1
  43. package/dist/file/file.controller.js +16 -0
  44. package/dist/file/file.controller.js.map +1 -1
  45. package/dist/file/file.service.d.ts +7 -1
  46. package/dist/file/file.service.d.ts.map +1 -1
  47. package/dist/file/file.service.js +38 -1
  48. package/dist/file/file.service.js.map +1 -1
  49. package/dist/file/provider/s3.provider.d.ts +1 -0
  50. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  51. package/dist/file/provider/s3.provider.js +38 -29
  52. package/dist/file/provider/s3.provider.js.map +1 -1
  53. package/dist/oauth/oauth.service.d.ts.map +1 -1
  54. package/dist/oauth/oauth.service.js +2 -1
  55. package/dist/oauth/oauth.service.js.map +1 -1
  56. package/dist/user/constants/user.constants.d.ts +1 -0
  57. package/dist/user/constants/user.constants.d.ts.map +1 -1
  58. package/dist/user/constants/user.constants.js +2 -1
  59. package/dist/user/constants/user.constants.js.map +1 -1
  60. package/dist/user/user.controller.d.ts +10 -10
  61. package/dist/user/user.service.d.ts +30 -30
  62. package/dist/user/user.service.d.ts.map +1 -1
  63. package/dist/user/user.service.js +2 -1
  64. package/dist/user/user.service.js.map +1 -1
  65. package/hedhog/data/dashboard_item.yaml +10 -10
  66. package/hedhog/data/route.yaml +20 -0
  67. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +212 -34
  68. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +3 -0
  69. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +136 -23
  70. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +266 -85
  71. package/hedhog/frontend/app/dashboard/components/widgets/core..gitkeep.ejs +11 -0
  72. package/hedhog/frontend/app/dashboard/components/widgets/core.account-security.tsx.ejs +192 -0
  73. package/hedhog/frontend/app/dashboard/components/widgets/core.email-notifications.tsx.ejs +226 -0
  74. package/hedhog/frontend/app/dashboard/components/widgets/core.locale-config.tsx.ejs +168 -0
  75. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-config.tsx.ejs +199 -0
  76. package/hedhog/frontend/app/dashboard/components/widgets/core.oauth-config.tsx.ejs +175 -0
  77. package/hedhog/frontend/app/dashboard/components/widgets/core.profile-card.tsx.ejs +186 -0
  78. package/hedhog/frontend/app/dashboard/components/widgets/core.storage-config.tsx.ejs +196 -0
  79. package/hedhog/frontend/app/dashboard/components/widgets/core.theme-config.tsx.ejs +213 -0
  80. package/hedhog/frontend/app/dashboard/components/widgets/core.user-roles.tsx.ejs +132 -0
  81. package/hedhog/frontend/app/dashboard/components/widgets/core.user-sessions.tsx.ejs +236 -0
  82. package/hedhog/frontend/app/dashboard/components/widgets/finance.alerts.tsx.ejs +108 -0
  83. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-balance-kpi.tsx.ejs +66 -0
  84. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-flow-chart.tsx.ejs +122 -0
  85. package/hedhog/frontend/app/dashboard/components/widgets/finance.default-kpi.tsx.ejs +63 -0
  86. package/hedhog/frontend/app/dashboard/components/widgets/finance.payable-30d-kpi.tsx.ejs +73 -0
  87. package/hedhog/frontend/app/dashboard/components/widgets/finance.receivable-30d-kpi.tsx.ejs +73 -0
  88. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-payable.tsx.ejs +123 -0
  89. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-receivable.tsx.ejs +118 -0
  90. package/hedhog/frontend/messages/en.json +93 -0
  91. package/hedhog/frontend/messages/pt.json +93 -0
  92. package/hedhog/frontend/public/dashboard-previews/.gitkeep +12 -0
  93. package/hedhog/frontend/public/dashboard-previews/account-security.png +0 -0
  94. package/hedhog/frontend/public/dashboard-previews/active-users-card.png +0 -0
  95. package/hedhog/frontend/public/dashboard-previews/activity-timeline.png +0 -0
  96. package/hedhog/frontend/public/dashboard-previews/cash-balance-kpi.png +0 -0
  97. package/hedhog/frontend/public/dashboard-previews/cash-flow-chart.png +0 -0
  98. package/hedhog/frontend/public/dashboard-previews/default-kpi.png +0 -0
  99. package/hedhog/frontend/public/dashboard-previews/email-notifications.png +0 -0
  100. package/hedhog/frontend/public/dashboard-previews/financial-alerts.png +0 -0
  101. package/hedhog/frontend/public/dashboard-previews/login-history-chart.png +0 -0
  102. package/hedhog/frontend/public/dashboard-previews/mail-sent-card.png +0 -0
  103. package/hedhog/frontend/public/dashboard-previews/mail-sent-chart.png +0 -0
  104. package/hedhog/frontend/public/dashboard-previews/menus-card.png +0 -0
  105. package/hedhog/frontend/public/dashboard-previews/payable-30d-kpi.png +0 -0
  106. package/hedhog/frontend/public/dashboard-previews/permissions-card.png +0 -0
  107. package/hedhog/frontend/public/dashboard-previews/permissions-chart.png +0 -0
  108. package/hedhog/frontend/public/dashboard-previews/profile-card.png +0 -0
  109. package/hedhog/frontend/public/dashboard-previews/receivable-30d-kpi.png +0 -0
  110. package/hedhog/frontend/public/dashboard-previews/routes-card.png +0 -0
  111. package/hedhog/frontend/public/dashboard-previews/session-activity-chart.png +0 -0
  112. package/hedhog/frontend/public/dashboard-previews/sessions-today-card.png +0 -0
  113. package/hedhog/frontend/public/dashboard-previews/stat-access-level.png +0 -0
  114. package/hedhog/frontend/public/dashboard-previews/stat-actions-today.png +0 -0
  115. package/hedhog/frontend/public/dashboard-previews/stat-consecutive-days.png +0 -0
  116. package/hedhog/frontend/public/dashboard-previews/stat-online-time.png +0 -0
  117. package/hedhog/frontend/public/dashboard-previews/upcoming-payable.png +0 -0
  118. package/hedhog/frontend/public/dashboard-previews/upcoming-receivable.png +0 -0
  119. package/hedhog/frontend/public/dashboard-previews/user-growth-chart.png +0 -0
  120. package/hedhog/frontend/public/dashboard-previews/user-roles.png +0 -0
  121. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/account-security.tsx.ejs +33 -29
  122. package/hedhog/frontend/widgets/active-users-card.tsx.ejs +58 -0
  123. package/hedhog/frontend/widgets/activity-timeline.tsx.ejs +223 -0
  124. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/email-notifications.tsx.ejs +85 -61
  125. package/hedhog/frontend/widgets/locale-config.tsx.ejs +168 -0
  126. package/hedhog/frontend/widgets/login-history-chart.tsx.ejs +115 -0
  127. package/hedhog/frontend/widgets/mail-config.tsx.ejs +199 -0
  128. package/hedhog/frontend/widgets/mail-sent-card.tsx.ejs +58 -0
  129. package/hedhog/frontend/widgets/mail-sent-chart.tsx.ejs +149 -0
  130. package/hedhog/frontend/widgets/menus-card.tsx.ejs +58 -0
  131. package/hedhog/frontend/widgets/oauth-config.tsx.ejs +175 -0
  132. package/hedhog/frontend/widgets/permissions-card.tsx.ejs +61 -0
  133. package/hedhog/frontend/widgets/permissions-chart.tsx.ejs +156 -0
  134. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/profile-card.tsx.ejs +3 -3
  135. package/hedhog/frontend/widgets/routes-card.tsx.ejs +58 -0
  136. package/hedhog/frontend/widgets/session-activity-chart.tsx.ejs +183 -0
  137. package/hedhog/frontend/widgets/sessions-today-card.tsx.ejs +62 -0
  138. package/hedhog/frontend/widgets/stat-access-level.tsx.ejs +57 -0
  139. package/hedhog/frontend/widgets/stat-actions-today.tsx.ejs +57 -0
  140. package/hedhog/frontend/widgets/stat-consecutive-days.tsx.ejs +57 -0
  141. package/hedhog/frontend/widgets/stat-online-time.tsx.ejs +57 -0
  142. package/hedhog/frontend/widgets/storage-config.tsx.ejs +196 -0
  143. package/hedhog/frontend/widgets/theme-config.tsx.ejs +213 -0
  144. package/hedhog/frontend/widgets/user-growth-chart.tsx.ejs +210 -0
  145. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-roles.tsx.ejs +12 -14
  146. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-sessions.tsx.ejs +1 -1
  147. package/hedhog/table/dashboard_component.yaml +7 -0
  148. package/hedhog/table/mail_sent_user.yaml +75 -0
  149. package/package.json +4 -4
  150. package/src/dashboard/dashboard-component/dashboard-component.controller.ts +36 -12
  151. package/src/dashboard/dashboard-component/dashboard-component.service.ts +150 -3
  152. package/src/dashboard/dashboard-component/dto/create.dto.ts +4 -0
  153. package/src/dashboard/dashboard-component/dto/update.dto.ts +4 -0
  154. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
  155. package/src/dashboard/dashboard-core/dashboard-core.module.ts +6 -1
  156. package/src/dashboard/dashboard-core/dashboard-core.service.ts +874 -8
  157. package/src/file/file.controller.ts +37 -13
  158. package/src/file/file.service.ts +47 -5
  159. package/src/file/provider/s3.provider.ts +39 -29
  160. package/src/oauth/oauth.service.ts +8 -7
  161. package/src/user/constants/user.constants.ts +1 -0
  162. package/src/user/user.service.ts +2 -1
  163. package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +0 -309
  164. package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +0 -445
  165. package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +0 -296
  166. package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +0 -340
  167. package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +0 -275
  168. /package/hedhog/frontend/app/dashboard/components/widgets/{active-users-card.tsx.ejs → core.active-users-card.tsx.ejs} +0 -0
  169. /package/hedhog/frontend/app/dashboard/components/widgets/{activity-timeline.tsx.ejs → core.activity-timeline.tsx.ejs} +0 -0
  170. /package/hedhog/frontend/app/dashboard/components/widgets/{login-history-chart.tsx.ejs → core.login-history-chart.tsx.ejs} +0 -0
  171. /package/hedhog/frontend/app/dashboard/components/widgets/{mail-sent-card.tsx.ejs → core.mail-sent-card.tsx.ejs} +0 -0
  172. /package/hedhog/frontend/app/dashboard/components/widgets/{mail-sent-chart.tsx.ejs → core.mail-sent-chart.tsx.ejs} +0 -0
  173. /package/hedhog/frontend/app/dashboard/components/widgets/{menus-card.tsx.ejs → core.menus-card.tsx.ejs} +0 -0
  174. /package/hedhog/frontend/app/dashboard/components/widgets/{permissions-card.tsx.ejs → core.permissions-card.tsx.ejs} +0 -0
  175. /package/hedhog/frontend/app/dashboard/components/widgets/{permissions-chart.tsx.ejs → core.permissions-chart.tsx.ejs} +0 -0
  176. /package/hedhog/frontend/app/dashboard/components/widgets/{routes-card.tsx.ejs → core.routes-card.tsx.ejs} +0 -0
  177. /package/hedhog/frontend/app/dashboard/components/widgets/{session-activity-chart.tsx.ejs → core.session-activity-chart.tsx.ejs} +0 -0
  178. /package/hedhog/frontend/app/dashboard/components/widgets/{sessions-today-card.tsx.ejs → core.sessions-today-card.tsx.ejs} +0 -0
  179. /package/hedhog/frontend/app/dashboard/components/widgets/{stat-access-level.tsx.ejs → core.stat-access-level.tsx.ejs} +0 -0
  180. /package/hedhog/frontend/app/dashboard/components/widgets/{stat-actions-today.tsx.ejs → core.stat-actions-today.tsx.ejs} +0 -0
  181. /package/hedhog/frontend/app/dashboard/components/widgets/{stat-consecutive-days.tsx.ejs → core.stat-consecutive-days.tsx.ejs} +0 -0
  182. /package/hedhog/frontend/app/dashboard/components/widgets/{stat-online-time.tsx.ejs → core.stat-online-time.tsx.ejs} +0 -0
  183. /package/hedhog/frontend/app/dashboard/components/widgets/{user-growth-chart.tsx.ejs → core.user-growth-chart.tsx.ejs} +0 -0
@@ -2,19 +2,19 @@ import { Public, Role } from '@hed-hog/api';
2
2
  import { getLocaleText, Locale } from '@hed-hog/api-locale';
3
3
  import { Pagination } from '@hed-hog/api-pagination';
4
4
  import {
5
- Body,
6
- Controller,
7
- Delete,
8
- forwardRef,
9
- Get,
10
- Inject,
11
- Param,
12
- ParseIntPipe,
13
- Post,
14
- Put,
15
- Res,
16
- UploadedFile,
17
- UseInterceptors,
5
+ Body,
6
+ Controller,
7
+ Delete,
8
+ forwardRef,
9
+ Get,
10
+ Inject,
11
+ Param,
12
+ ParseIntPipe,
13
+ Post,
14
+ Put,
15
+ Res,
16
+ UploadedFile,
17
+ UseInterceptors,
18
18
  } from '@nestjs/common';
19
19
  import { FileInterceptor } from '@nestjs/platform-express';
20
20
  import { Response } from 'express';
@@ -127,6 +127,30 @@ export class FileController {
127
127
 
128
128
  try {
129
129
 
130
+ const numericId = Number(token);
131
+ const isNumericId = Number.isInteger(numericId) && String(numericId) === token;
132
+
133
+ if (isNumericId && numericId > 0) {
134
+ const { stream, filename, mimetype, size } =
135
+ await this.fileService.openById(locale, numericId);
136
+
137
+ const encodedFilename = encodeURIComponent(filename);
138
+
139
+ res.set({
140
+ 'Content-Type': mimetype || 'application/octet-stream',
141
+ 'Content-Disposition':
142
+ `inline; filename="${filename}"; ` +
143
+ `filename*=UTF-8''${encodedFilename}`,
144
+ });
145
+
146
+ if (typeof size === 'number') {
147
+ res.set('Content-Length', `${size}`);
148
+ }
149
+
150
+ stream.pipe(res);
151
+ return;
152
+ }
153
+
130
154
  const { stream, filename, mimetype, size } =
131
155
  await this.fileService.download(locale, token);
132
156
 
@@ -258,11 +258,26 @@ export class FileService implements OnModuleInit {
258
258
 
259
259
  const provider = await this.getProvider();
260
260
 
261
- const buffer = Buffer.from(
262
- await provider.buffer(
263
- storage === EnumProvider.LOCAL ? file.path : (new URL(file.path)).pathname
264
- )
265
- );
261
+ const providerPath =
262
+ storage === EnumProvider.LOCAL ? file.path : new URL(file.path).pathname;
263
+
264
+ let providerBuffer: Buffer;
265
+ try {
266
+ providerBuffer = await provider.buffer(providerPath);
267
+ } catch (error: any) {
268
+ const missingInStorage =
269
+ error?.name === 'NoSuchKey' ||
270
+ error?.Code === 'NoSuchKey' ||
271
+ error?.$metadata?.httpStatusCode === 404;
272
+
273
+ if (missingInStorage) {
274
+ throw new NotFoundException(`File not found in storage: ${fileId}`);
275
+ }
276
+
277
+ throw error;
278
+ }
279
+
280
+ const buffer = Buffer.from(providerBuffer);
266
281
 
267
282
  return {
268
283
  file,
@@ -453,6 +468,33 @@ export class FileService implements OnModuleInit {
453
468
  }
454
469
  }
455
470
 
471
+ async openById(locale: string, fileId: number) {
472
+ const file = await this.prismaService.file.findFirst({
473
+ where: { id: fileId },
474
+ include: {
475
+ file_mimetype: true,
476
+ },
477
+ });
478
+
479
+ if (!file) {
480
+ throw new NotFoundException(
481
+ getLocaleText('file.download.not_found', locale, `Not found: ${fileId}`),
482
+ );
483
+ }
484
+
485
+ const provider = await this.getProvider();
486
+ const metadata = await provider.metaData(file.path);
487
+ const stream = await provider.readStream(file.path);
488
+ const size = this.getFileSizeFromMetadata(metadata);
489
+
490
+ return {
491
+ stream,
492
+ filename: file?.filename || 'download',
493
+ mimetype: file?.file_mimetype?.name || 'application/octet-stream',
494
+ size,
495
+ };
496
+ }
497
+
456
498
  async tempURL(filepath: string, expires = 3600) {
457
499
  return (await this.getProvider()).tempURL(filepath, expires);
458
500
  }
@@ -21,15 +21,33 @@ export class S3Provider extends AbstractProvider {
21
21
  this.initValidation();
22
22
  }
23
23
 
24
+ private resolveS3Key(filepath: string): string {
25
+ if (!filepath) {
26
+ throw new Error('Invalid filepath for S3');
27
+ }
28
+
29
+ try {
30
+ const url = new URL(filepath);
31
+ const keyFromUrl = url.pathname.split('/').filter(Boolean).join('/');
32
+ if (!keyFromUrl) {
33
+ throw new Error(`Invalid filepath "${filepath}" for S3`);
34
+ }
35
+ return keyFromUrl;
36
+ } catch {
37
+ const keyFromPath = filepath.split('/').filter(Boolean).join('/');
38
+ if (!keyFromPath) {
39
+ throw new Error(`Invalid filepath "${filepath}" for S3`);
40
+ }
41
+ return keyFromPath;
42
+ }
43
+ }
44
+
24
45
  async buffer(filepath: string): Promise<Buffer> {
25
46
  const s3 = await this.getClient();
26
- const url = new URL(filepath);
27
- if (!url.pathname) {
28
- throw new Error(`Invalid filepath "${filepath}" for S3`);
29
- }
47
+ const key = this.resolveS3Key(filepath);
30
48
  const command = new GetObjectCommand({
31
49
  Bucket: this.setting['storage-s3-bucket'],
32
- Key: url.pathname.split('/').slice(1).join('/'),
50
+ Key: key,
33
51
  });
34
52
  const result = await s3.send(command);
35
53
  // @ts-ignore
@@ -123,37 +141,38 @@ export class S3Provider extends AbstractProvider {
123
141
  fileContent: string,
124
142
  ): Promise<any> {
125
143
  const s3 = await this.getClient();
144
+ const storedFilename = this.getFilename(filename);
145
+ const key = [destination, storedFilename].join('/');
126
146
  const command = new PutObjectCommand({
127
147
  Bucket: this.setting['storage-s3-bucket'],
128
- Key: [destination, this.getFilename(filename)].join('/'),
148
+ Key: key,
129
149
  Body: fileContent,
130
150
  });
131
151
  await s3.send(command);
132
152
  // S3 v3 não retorna Location diretamente, construa a URL:
133
- return `https://${this.setting['storage-s3-bucket']}.s3.${this.setting['storage-s3-region']}.amazonaws.com/${destination}/${this.getFilename(filename)}`;
153
+ return `https://${this.setting['storage-s3-bucket']}.s3.${this.setting['storage-s3-region']}.amazonaws.com/${key}`;
134
154
  }
135
155
 
136
156
  async upload(destination: string, file: MulterFile): Promise<any> {
137
157
  const s3 = await this.getClient();
158
+ const storedFilename = this.getFilename(file.originalname);
159
+ const key = [destination, storedFilename].join('/');
138
160
  const command = new PutObjectCommand({
139
161
  Bucket: this.setting['storage-s3-bucket'],
140
- Key: [destination, this.getFilename(file.originalname)].join('/'),
162
+ Key: key,
141
163
  Body: file.buffer,
142
164
  });
143
165
  await s3.send(command);
144
- return `https://${this.setting['storage-s3-bucket']}.s3.${this.setting['storage-s3-region']}.amazonaws.com/${destination}/${this.getFilename(file.originalname)}`;
166
+ return `https://${this.setting['storage-s3-bucket']}.s3.${this.setting['storage-s3-region']}.amazonaws.com/${key}`;
145
167
  }
146
168
 
147
169
  async delete(filepath: string): Promise<any> {
148
170
  const s3 = await this.getClient();
149
- const url = new URL(filepath);
150
- if (!url.pathname) {
151
- throw new Error(`Invalid filepath "${filepath}" for S3`);
152
- }
171
+ const key = this.resolveS3Key(filepath);
153
172
  await s3.send(
154
173
  new DeleteObjectCommand({
155
174
  Bucket: this.setting['storage-s3-bucket'],
156
- Key: url.pathname.split('/').slice(1).join('/'),
175
+ Key: key,
157
176
  }),
158
177
  );
159
178
  return true;
@@ -161,13 +180,10 @@ export class S3Provider extends AbstractProvider {
161
180
 
162
181
  async readStream(filepath: string): Promise<Readable> {
163
182
  const s3 = await this.getClient();
164
- const url = new URL(filepath);
165
- if (!url.pathname) {
166
- throw new Error(`Invalid filepath "${filepath}" for S3`);
167
- }
183
+ const key = this.resolveS3Key(filepath);
168
184
  const command = new GetObjectCommand({
169
185
  Bucket: this.setting['storage-s3-bucket'],
170
- Key: url.pathname.split('/').slice(1).join('/'),
186
+ Key: key,
171
187
  });
172
188
  const result = await s3.send(command);
173
189
  // @ts-ignore
@@ -176,13 +192,10 @@ export class S3Provider extends AbstractProvider {
176
192
 
177
193
  async metaData(filepath: string): Promise<any> {
178
194
  const s3 = await this.getClient();
179
- const url = new URL(filepath);
180
- if (!url.pathname) {
181
- throw new Error(`Invalid filepath "${filepath}" for S3`);
182
- }
195
+ const key = this.resolveS3Key(filepath);
183
196
  const command = new HeadObjectCommand({
184
197
  Bucket: this.setting['storage-s3-bucket'],
185
- Key: url.pathname.split('/').slice(1).join('/'),
198
+ Key: key,
186
199
  });
187
200
  const result = await s3.send(command);
188
201
  return result; // returns metadata
@@ -190,13 +203,10 @@ export class S3Provider extends AbstractProvider {
190
203
 
191
204
  async tempURL(filepath: string, expires = 3600): Promise<string> {
192
205
  const s3Client = await this.getS3Client();
193
- const url = new URL(filepath);
194
- if (!url.pathname) {
195
- throw new Error(`Invalid filepath "${filepath}" for S3`);
196
- }
206
+ const key = this.resolveS3Key(filepath);
197
207
  const command = new GetObjectCommand({
198
208
  Bucket: this.setting['storage-s3-bucket'],
199
- Key: url.pathname.split('/').slice(1).join('/'),
209
+ Key: key,
200
210
  });
201
211
  // Use S3Client instance for getSignedUrl
202
212
  return await getSignedUrl(s3Client as any, command, { expiresIn: expires });
@@ -1,11 +1,11 @@
1
1
  import { PrismaService, user_account_provider_52222e2ecb_enum } from '@hed-hog/api-prisma';
2
2
  import {
3
- BadRequestException,
4
- ConflictException,
5
- Inject,
6
- Injectable,
7
- NotFoundException,
8
- forwardRef
3
+ BadRequestException,
4
+ ConflictException,
5
+ Inject,
6
+ Injectable,
7
+ NotFoundException,
8
+ forwardRef
9
9
  } from '@nestjs/common';
10
10
  import { AuthService } from '../auth/auth.service';
11
11
  import { FileService } from '../file/file.service';
@@ -13,6 +13,7 @@ import { MailService } from '../mail/mail.service';
13
13
  import { SecurityService } from '../security/security.service';
14
14
  import { SettingService } from '../setting/setting.service';
15
15
  import { TokenService } from '../token/token.service';
16
+ import { USER_AVATAR_UPLOAD_DESTINATION } from '../user/constants/user.constants';
16
17
  import { UserService } from '../user/user.service';
17
18
  import { OAuthProvider } from './interfaces/OAuthProvider';
18
19
  import { FacebookProvider } from './providers/facebook.provider';
@@ -385,7 +386,7 @@ export class OAuthService {
385
386
  size: buffer.length,
386
387
  } as any;
387
388
 
388
- const savedFile = await this.file.upload('profile-pictures', file);
389
+ const savedFile = await this.file.upload(USER_AVATAR_UPLOAD_DESTINATION, file);
389
390
  return savedFile.id;
390
391
  }
391
392
  }
@@ -1 +1,2 @@
1
1
  export const SALT_ROUNDS = 10;
2
+ export const USER_AVATAR_UPLOAD_DESTINATION = 'profile-pictures';
@@ -12,6 +12,7 @@ import { ChallengeService } from '../challenge/challenge.service';
12
12
  import { DeleteDTO } from '../dto/delete.dto';
13
13
  import { FileService } from '../file/file.service';
14
14
  import { SecurityService } from '../security/security.service';
15
+ import { USER_AVATAR_UPLOAD_DESTINATION } from './constants/user.constants';
15
16
  import { CreateWithEmailAndPasswordDTO } from './dto/create-with-email-and-password.dto';
16
17
  import { ResetPasswordDTO } from './dto/reset-password.dto';
17
18
  import { UpdateDTO } from './dto/update.dto';
@@ -272,7 +273,7 @@ export class UserService {
272
273
  async changeAvatar(locale: string, userId: number, avatar: MulterFile) {
273
274
  const user = await this.validateUserExists(locale, userId);
274
275
 
275
- const newFile = await this.file.upload('user/avatar', avatar);
276
+ const newFile = await this.file.upload(USER_AVATAR_UPLOAD_DESTINATION, avatar);
276
277
 
277
278
  await this.deleteOldAvatar(locale, user.photo_id);
278
279
 
@@ -1,309 +0,0 @@
1
- 'use client';
2
-
3
- import { Badge } from '@/components/ui/badge';
4
- import { Button } from '@/components/ui/button';
5
- import {
6
- Card,
7
- CardContent,
8
- CardDescription,
9
- CardHeader,
10
- CardTitle,
11
- } from '@/components/ui/card';
12
- import { Checkbox } from '@/components/ui/checkbox';
13
- import { Label } from '@/components/ui/label';
14
- import {
15
- Select,
16
- SelectContent,
17
- SelectItem,
18
- SelectTrigger,
19
- SelectValue,
20
- } from '@/components/ui/select';
21
- import { Switch } from '@/components/ui/switch';
22
- import { Calendar, CheckCircle2, Clock, Globe, Type } from 'lucide-react';
23
- import { useState } from 'react';
24
-
25
- interface Language {
26
- code: string;
27
- label: string;
28
- flag: string;
29
- enabled: boolean;
30
- }
31
-
32
- const availableLanguages: Language[] = [
33
- { code: 'pt-BR', label: 'Portugues (Brasil)', flag: '🇧🇷', enabled: true },
34
- { code: 'en-US', label: 'Ingles (EUA)', flag: '🇺🇸', enabled: true },
35
- { code: 'es-ES', label: 'Espanhol (Espanha)', flag: '🇪🇸', enabled: false },
36
- { code: 'fr-FR', label: 'Frances (Franca)', flag: '🇫🇷', enabled: false },
37
- { code: 'de-DE', label: 'Alemao (Alemanha)', flag: '🇩🇪', enabled: false },
38
- { code: 'it-IT', label: 'Italiano (Italia)', flag: '🇮🇹', enabled: false },
39
- { code: 'ja-JP', label: 'Japones (Japao)', flag: '🇯🇵', enabled: false },
40
- { code: 'zh-CN', label: 'Chines (Simplificado)', flag: '🇨🇳', enabled: false },
41
- ];
42
-
43
- const timezones = [
44
- { value: 'America/Sao_Paulo', label: 'America/Sao_Paulo (GMT-3)' },
45
- { value: 'America/New_York', label: 'America/New_York (GMT-5)' },
46
- { value: 'America/Chicago', label: 'America/Chicago (GMT-6)' },
47
- { value: 'America/Denver', label: 'America/Denver (GMT-7)' },
48
- { value: 'America/Los_Angeles', label: 'America/Los_Angeles (GMT-8)' },
49
- { value: 'Europe/London', label: 'Europe/London (GMT+0)' },
50
- { value: 'Europe/Paris', label: 'Europe/Paris (GMT+1)' },
51
- { value: 'Europe/Berlin', label: 'Europe/Berlin (GMT+1)' },
52
- { value: 'Asia/Tokyo', label: 'Asia/Tokyo (GMT+9)' },
53
- { value: 'Asia/Shanghai', label: 'Asia/Shanghai (GMT+8)' },
54
- { value: 'Australia/Sydney', label: 'Australia/Sydney (GMT+11)' },
55
- ];
56
-
57
- const dateFormats = [
58
- { value: 'DD/MM/YYYY', label: 'DD/MM/YYYY', example: '14/02/2026' },
59
- { value: 'MM/DD/YYYY', label: 'MM/DD/YYYY', example: '02/14/2026' },
60
- { value: 'YYYY-MM-DD', label: 'YYYY-MM-DD', example: '2026-02-14' },
61
- { value: 'DD.MM.YYYY', label: 'DD.MM.YYYY', example: '14.02.2026' },
62
- { value: 'DD-MM-YYYY', label: 'DD-MM-YYYY', example: '14-02-2026' },
63
- ];
64
-
65
- const timeFormats = [
66
- { value: 'HH:mm', label: '24 horas', example: '14:30' },
67
- { value: 'hh:mm A', label: '12 horas', example: '02:30 PM' },
68
- { value: 'HH:mm:ss', label: '24 horas com segundos', example: '14:30:45' },
69
- {
70
- value: 'hh:mm:ss A',
71
- label: '12 horas com segundos',
72
- example: '02:30:45 PM',
73
- },
74
- ];
75
-
76
- export default function LocaleConfig() {
77
- const [languages, setLanguages] = useState<Language[]>(availableLanguages);
78
- const [defaultLang, setDefaultLang] = useState('pt-BR');
79
- const [timezone, setTimezone] = useState('America/Sao_Paulo');
80
- const [dateFormat, setDateFormat] = useState('DD/MM/YYYY');
81
- const [timeFormat, setTimeFormat] = useState('HH:mm');
82
- const [autoDetect, setAutoDetect] = useState(true);
83
-
84
- const enabledLanguages = languages.filter((l) => l.enabled);
85
-
86
- function toggleLanguage(code: string) {
87
- setLanguages((prev) =>
88
- prev.map((l) => {
89
- if (l.code === code) {
90
- if (l.enabled && l.code === defaultLang) return l;
91
- return { ...l, enabled: !l.enabled };
92
- }
93
- return l;
94
- })
95
- );
96
- }
97
-
98
- return (
99
- <Card className="h-full">
100
- <CardHeader>
101
- <div className="flex items-center justify-between">
102
- <div className="flex items-center gap-3">
103
- <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-50">
104
- <Globe className="h-5 w-5 text-indigo-600" />
105
- </div>
106
- <div>
107
- <CardTitle className="text-base">Localizacao</CardTitle>
108
- <CardDescription>
109
- Idiomas, fuso horario e formatos regionais
110
- </CardDescription>
111
- </div>
112
- </div>
113
- <Badge variant="secondary" className="bg-indigo-50 text-indigo-700">
114
- {enabledLanguages.length} idioma
115
- {enabledLanguages.length !== 1 ? 's' : ''}
116
- </Badge>
117
- </div>
118
- </CardHeader>
119
- <CardContent className="space-y-6">
120
- {/* Languages */}
121
- <div className="space-y-3">
122
- <div className="flex items-center gap-2">
123
- <Type className="h-4 w-4 text-muted-foreground" />
124
- <Label className="text-sm font-medium">Idiomas habilitados</Label>
125
- </div>
126
- <div className="grid grid-cols-2 gap-2">
127
- {languages.map((lang) => (
128
- <label
129
- key={lang.code}
130
- className={`flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors ${
131
- lang.enabled
132
- ? 'border-foreground/20 bg-foreground/2'
133
- : 'border-border hover:border-foreground/10'
134
- }`}
135
- >
136
- <Checkbox
137
- checked={lang.enabled}
138
- onCheckedChange={() => toggleLanguage(lang.code)}
139
- disabled={lang.enabled && lang.code === defaultLang}
140
- />
141
- <span className="text-base leading-none">{lang.flag}</span>
142
- <div className="flex flex-1 flex-col">
143
- <span className="text-sm font-medium">{lang.label}</span>
144
- <span className="text-xs text-muted-foreground">
145
- {lang.code}
146
- </span>
147
- </div>
148
- {lang.code === defaultLang && (
149
- <Badge variant="outline" className="text-[10px] h-5">
150
- Padrao
151
- </Badge>
152
- )}
153
- </label>
154
- ))}
155
- </div>
156
- </div>
157
-
158
- {/* Default Language */}
159
- <div className="space-y-2">
160
- <Label>Idioma padrao</Label>
161
- <Select value={defaultLang} onValueChange={setDefaultLang}>
162
- <SelectTrigger>
163
- <SelectValue />
164
- </SelectTrigger>
165
- <SelectContent>
166
- {enabledLanguages.map((lang) => (
167
- <SelectItem key={lang.code} value={lang.code}>
168
- <span className="flex items-center gap-2">
169
- <span>{lang.flag}</span>
170
- <span>{lang.label}</span>
171
- </span>
172
- </SelectItem>
173
- ))}
174
- </SelectContent>
175
- </Select>
176
- <p className="text-xs text-muted-foreground">
177
- Idioma exibido quando o idioma do usuario nao esta disponivel.
178
- </p>
179
- </div>
180
-
181
- {/* Timezone */}
182
- <div className="space-y-2">
183
- <div className="flex items-center gap-2">
184
- <Clock className="h-4 w-4 text-muted-foreground" />
185
- <Label>Fuso horario do sistema</Label>
186
- </div>
187
- <Select value={timezone} onValueChange={setTimezone}>
188
- <SelectTrigger>
189
- <SelectValue />
190
- </SelectTrigger>
191
- <SelectContent>
192
- {timezones.map((tz) => (
193
- <SelectItem key={tz.value} value={tz.value}>
194
- {tz.label}
195
- </SelectItem>
196
- ))}
197
- </SelectContent>
198
- </Select>
199
- </div>
200
-
201
- {/* Date & Time Formats */}
202
- <div className="grid gap-4 sm:grid-cols-2">
203
- <div className="space-y-2">
204
- <div className="flex items-center gap-2">
205
- <Calendar className="h-4 w-4 text-muted-foreground" />
206
- <Label>Formato de data</Label>
207
- </div>
208
- <Select value={dateFormat} onValueChange={setDateFormat}>
209
- <SelectTrigger>
210
- <SelectValue />
211
- </SelectTrigger>
212
- <SelectContent>
213
- {dateFormats.map((df) => (
214
- <SelectItem key={df.value} value={df.value}>
215
- <span className="flex items-center gap-2">
216
- <span className="font-mono text-xs">{df.value}</span>
217
- <span className="text-muted-foreground">
218
- {'('}
219
- {df.example}
220
- {')'}
221
- </span>
222
- </span>
223
- </SelectItem>
224
- ))}
225
- </SelectContent>
226
- </Select>
227
- </div>
228
- <div className="space-y-2">
229
- <div className="flex items-center gap-2">
230
- <Clock className="h-4 w-4 text-muted-foreground" />
231
- <Label>Formato de hora</Label>
232
- </div>
233
- <Select value={timeFormat} onValueChange={setTimeFormat}>
234
- <SelectTrigger>
235
- <SelectValue />
236
- </SelectTrigger>
237
- <SelectContent>
238
- {timeFormats.map((tf) => (
239
- <SelectItem key={tf.value} value={tf.value}>
240
- <span className="flex items-center gap-2">
241
- <span>{tf.label}</span>
242
- <span className="text-muted-foreground font-mono text-xs">
243
- {'('}
244
- {tf.example}
245
- {')'}
246
- </span>
247
- </span>
248
- </SelectItem>
249
- ))}
250
- </SelectContent>
251
- </Select>
252
- </div>
253
- </div>
254
-
255
- {/* Preview */}
256
- <div className="rounded-lg border bg-muted/50 p-4">
257
- <p className="mb-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
258
- Pre-visualizacao
259
- </p>
260
- <div className="flex flex-wrap gap-x-6 gap-y-1 text-sm">
261
- <span>
262
- <span className="text-muted-foreground">Data: </span>
263
- <span className="font-medium font-mono">
264
- {dateFormats.find((d) => d.value === dateFormat)?.example}
265
- </span>
266
- </span>
267
- <span>
268
- <span className="text-muted-foreground">Hora: </span>
269
- <span className="font-medium font-mono">
270
- {timeFormats.find((t) => t.value === timeFormat)?.example}
271
- </span>
272
- </span>
273
- <span>
274
- <span className="text-muted-foreground">Fuso: </span>
275
- <span className="font-medium font-mono">
276
- {
277
- timezones
278
- .find((t) => t.value === timezone)
279
- ?.label.split(' ')[0]
280
- }
281
- </span>
282
- </span>
283
- </div>
284
- </div>
285
-
286
- {/* Actions */}
287
- <div className="flex items-center justify-between border-t pt-4">
288
- <div className="flex items-center gap-2">
289
- <Switch
290
- id="auto-detect-lang"
291
- checked={autoDetect}
292
- onCheckedChange={setAutoDetect}
293
- />
294
- <Label
295
- htmlFor="auto-detect-lang"
296
- className="text-sm text-muted-foreground"
297
- >
298
- Detectar idioma do navegador
299
- </Label>
300
- </div>
301
- <Button size="sm">
302
- <CheckCircle2 className="mr-1.5 h-3.5 w-3.5" />
303
- Salvar
304
- </Button>
305
- </div>
306
- </CardContent>
307
- </Card>
308
- );
309
- }