@hed-hog/core 0.0.297 → 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 (60) 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-core/dashboard-core.controller.d.ts +14 -0
  4. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  5. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
  6. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  7. package/dist/dashboard/dashboard-core/dashboard-core.module.d.ts.map +1 -1
  8. package/dist/dashboard/dashboard-core/dashboard-core.module.js +6 -1
  9. package/dist/dashboard/dashboard-core/dashboard-core.module.js.map +1 -1
  10. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +173 -1
  11. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  12. package/dist/dashboard/dashboard-core/dashboard-core.service.js +531 -5
  13. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  14. package/dist/file/file.controller.d.ts.map +1 -1
  15. package/dist/file/file.controller.js +16 -0
  16. package/dist/file/file.controller.js.map +1 -1
  17. package/dist/file/file.service.d.ts +7 -1
  18. package/dist/file/file.service.d.ts.map +1 -1
  19. package/dist/file/file.service.js +38 -1
  20. package/dist/file/file.service.js.map +1 -1
  21. package/dist/file/provider/s3.provider.d.ts +1 -0
  22. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  23. package/dist/file/provider/s3.provider.js +38 -29
  24. package/dist/file/provider/s3.provider.js.map +1 -1
  25. package/dist/oauth/oauth.service.d.ts.map +1 -1
  26. package/dist/oauth/oauth.service.js +2 -1
  27. package/dist/oauth/oauth.service.js.map +1 -1
  28. package/dist/user/constants/user.constants.d.ts +1 -0
  29. package/dist/user/constants/user.constants.d.ts.map +1 -1
  30. package/dist/user/constants/user.constants.js +2 -1
  31. package/dist/user/constants/user.constants.js.map +1 -1
  32. package/dist/user/user.controller.d.ts +10 -10
  33. package/dist/user/user.service.d.ts +30 -30
  34. package/dist/user/user.service.d.ts.map +1 -1
  35. package/dist/user/user.service.js +2 -1
  36. package/dist/user/user.service.js.map +1 -1
  37. package/hedhog/data/dashboard_item.yaml +11 -11
  38. package/hedhog/data/route.yaml +8 -0
  39. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +76 -15
  40. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +85 -61
  41. package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +139 -280
  42. package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +161 -407
  43. package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +150 -271
  44. package/hedhog/frontend/app/dashboard/components/widgets/profile-card.tsx.ejs +3 -3
  45. package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +161 -305
  46. package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +184 -246
  47. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +12 -14
  48. package/hedhog/frontend/messages/en.json +90 -0
  49. package/hedhog/frontend/messages/pt.json +90 -0
  50. package/hedhog/table/mail_sent_user.yaml +75 -0
  51. package/package.json +5 -5
  52. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
  53. package/src/dashboard/dashboard-core/dashboard-core.module.ts +6 -1
  54. package/src/dashboard/dashboard-core/dashboard-core.service.ts +766 -3
  55. package/src/file/file.controller.ts +37 -13
  56. package/src/file/file.service.ts +47 -5
  57. package/src/file/provider/s3.provider.ts +39 -29
  58. package/src/oauth/oauth.service.ts +8 -7
  59. package/src/user/constants/user.constants.ts +1 -0
  60. package/src/user/user.service.ts +2 -1
@@ -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