@hed-hog/core 0.0.106 → 0.0.108

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 (34) hide show
  1. package/dist/auth/auth.service.js +2 -2
  2. package/dist/auth/auth.service.js.map +1 -1
  3. package/dist/challenge/challenge.service.js +1 -1
  4. package/dist/challenge/challenge.service.js.map +1 -1
  5. package/dist/mail/dto/import.dto.d.ts +16 -0
  6. package/dist/mail/dto/import.dto.d.ts.map +1 -0
  7. package/dist/mail/dto/import.dto.js +78 -0
  8. package/dist/mail/dto/import.dto.js.map +1 -0
  9. package/dist/mail/mail.controller.d.ts +12 -0
  10. package/dist/mail/mail.controller.d.ts.map +1 -1
  11. package/dist/mail/mail.controller.js +11 -0
  12. package/dist/mail/mail.controller.js.map +1 -1
  13. package/dist/mail/mail.service.d.ts +12 -0
  14. package/dist/mail/mail.service.d.ts.map +1 -1
  15. package/dist/mail/mail.service.js +89 -0
  16. package/dist/mail/mail.service.js.map +1 -1
  17. package/dist/profile/profile.controller.d.ts +1 -1
  18. package/dist/profile/profile.controller.d.ts.map +1 -1
  19. package/dist/profile/profile.controller.js +5 -3
  20. package/dist/profile/profile.controller.js.map +1 -1
  21. package/dist/profile/profile.service.d.ts +4 -2
  22. package/dist/profile/profile.service.d.ts.map +1 -1
  23. package/dist/profile/profile.service.js +32 -3
  24. package/dist/profile/profile.service.js.map +1 -1
  25. package/hedhog/data/mail.yaml +162 -0
  26. package/hedhog/data/route.yaml +6 -0
  27. package/package.json +4 -4
  28. package/src/auth/auth.service.ts +2 -2
  29. package/src/challenge/challenge.service.ts +1 -1
  30. package/src/mail/dto/import.dto.ts +62 -0
  31. package/src/mail/mail.controller.ts +19 -13
  32. package/src/mail/mail.service.ts +106 -0
  33. package/src/profile/profile.controller.ts +9 -3
  34. package/src/profile/profile.service.ts +36 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.106",
3
+ "version": "0.0.108",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -31,10 +31,10 @@
31
31
  "uuid": "^11.1.0",
32
32
  "@hed-hog/types": "0.0.1",
33
33
  "@hed-hog/api-pagination": "0.0.4",
34
- "@hed-hog/api": "0.0.3",
35
34
  "@hed-hog/api-mail": "0.0.7",
36
- "@hed-hog/api-locale": "0.0.10",
37
- "@hed-hog/api-prisma": "0.0.4"
35
+ "@hed-hog/api": "0.0.3",
36
+ "@hed-hog/api-prisma": "0.0.4",
37
+ "@hed-hog/api-locale": "0.0.10"
38
38
  },
39
39
  "exports": {
40
40
  ".": {
@@ -122,7 +122,7 @@ export class AuthService {
122
122
  locale,
123
123
  {
124
124
  email,
125
- slug: 'auth-sign-up-confirm-email',
125
+ slug: 'auth-email-verification-code',
126
126
  variables: {
127
127
  code,
128
128
  name: user.name,
@@ -179,7 +179,7 @@ export class AuthService {
179
179
  locale,
180
180
  {
181
181
  email,
182
- slug: 'auth-sign-up-confirm-email',
182
+ slug: 'auth-email-verification-code',
183
183
  variables: {
184
184
  code,
185
185
  name: user.name,
@@ -71,7 +71,7 @@ export class ChallengeService {
71
71
  locale,
72
72
  {
73
73
  email,
74
- slug: 'auth-sign-up-confirm-email',
74
+ slug: 'auth-email-verification-code',
75
75
  variables: {
76
76
  code,
77
77
  name: user.name,
@@ -0,0 +1,62 @@
1
+ import { getLocaleText } from '@hed-hog/api-locale';
2
+ import { Type } from 'class-transformer';
3
+ import {
4
+ IsArray,
5
+ IsBoolean,
6
+ IsOptional,
7
+ IsString,
8
+ ValidateNested,
9
+ } from 'class-validator';
10
+
11
+ class ImportTranslationDTO {
12
+ @IsString({
13
+ message: (args) => getLocaleText('validation.stringRequired', args.value),
14
+ })
15
+ code: string;
16
+
17
+ @IsString({
18
+ message: (args) => getLocaleText('validation.stringRequired', args.value),
19
+ })
20
+ subject: string;
21
+
22
+ @IsString({
23
+ message: (args) => getLocaleText('validation.stringRequired', args.value),
24
+ })
25
+ body: string;
26
+ }
27
+
28
+ class ImportTemplateDTO {
29
+ @IsString({
30
+ message: (args) => getLocaleText('validation.stringRequired', args.value),
31
+ })
32
+ slug: string;
33
+
34
+ @IsArray({
35
+ message: (args) => getLocaleText('validation.arrayRequired', args.value),
36
+ })
37
+ @ValidateNested({ each: true })
38
+ @Type(() => ImportTranslationDTO)
39
+ translations: ImportTranslationDTO[];
40
+
41
+ @IsArray({
42
+ message: (args) => getLocaleText('validation.arrayRequired', args.value),
43
+ })
44
+ @IsString({ each: true })
45
+ @IsOptional()
46
+ variables?: string[];
47
+ }
48
+
49
+ export class ImportDTO {
50
+ @IsArray({
51
+ message: (args) => getLocaleText('validation.arrayRequired', args.value),
52
+ })
53
+ @ValidateNested({ each: true })
54
+ @Type(() => ImportTemplateDTO)
55
+ data: ImportTemplateDTO[];
56
+
57
+ @IsBoolean({
58
+ message: (args) => getLocaleText('validation.booleanRequired', args.value),
59
+ })
60
+ @IsOptional()
61
+ overwrite?: boolean;
62
+ }
@@ -2,22 +2,23 @@ import { DeleteDTO, Role } from '@hed-hog/api';
2
2
  import { 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
- Get,
9
- Header,
10
- Inject,
11
- Param,
12
- ParseIntPipe,
13
- Patch,
14
- Post,
15
- Query,
16
- Res,
17
- forwardRef,
5
+ Body,
6
+ Controller,
7
+ Delete,
8
+ Get,
9
+ Header,
10
+ Inject,
11
+ Param,
12
+ ParseIntPipe,
13
+ Patch,
14
+ Post,
15
+ Query,
16
+ Res,
17
+ forwardRef,
18
18
  } from '@nestjs/common';
19
19
  import { Response } from 'express';
20
20
  import { CreateDTO } from './dto/create.dto';
21
+ import { ImportDTO } from './dto/import.dto';
21
22
  import { TestMailDTO } from './dto/test-mail.dto';
22
23
  import { UpdateDTO } from './dto/update.dto';
23
24
  import { MailService } from './mail.service';
@@ -116,4 +117,9 @@ export class MailController {
116
117
  async sendTestMail(@Locale() locale: string, @Body() data: TestMailDTO) {
117
118
  return this.mailService.sendTestMail(locale, data);
118
119
  }
120
+
121
+ @Post('import')
122
+ async import(@Body() data: ImportDTO) {
123
+ return this.mailService.importTemplates(data);
124
+ }
119
125
  }
@@ -13,6 +13,7 @@ import {
13
13
  import * as Handlebars from 'handlebars';
14
14
  import { SettingService } from '../setting/setting.service';
15
15
  import { CreateDTO } from './dto/create.dto';
16
+ import { ImportDTO } from './dto/import.dto';
16
17
  import { SendTemplatedMailDTO } from './dto/send.dto';
17
18
  import { TestMailDTO } from './dto/test-mail.dto';
18
19
  import { UpdateDTO } from './dto/update.dto';
@@ -461,4 +462,109 @@ export class MailService {
461
462
  throw new BadRequestException('Failed to send test email');
462
463
  }
463
464
  }
465
+
466
+ async importTemplates({ data, overwrite }: ImportDTO) {
467
+ const conflicts: string[] = [];
468
+
469
+ if (!overwrite) {
470
+ for (const template of data) {
471
+ const existing = await this.prismaService.mail.findUnique({
472
+ where: { slug: template.slug },
473
+ });
474
+
475
+ if (existing) {
476
+ conflicts.push(template.slug);
477
+ }
478
+ }
479
+
480
+ if (conflicts.length > 0) {
481
+ return { conflicts };
482
+ }
483
+ }
484
+
485
+ const imported: any[] = [];
486
+ for (const template of data) {
487
+ try {
488
+ const locales = await this.prismaService.locale.findMany({
489
+ where: {
490
+ code: {
491
+ in: template.translations.map((t) => t.code),
492
+ },
493
+ },
494
+ });
495
+
496
+ const localeMap = new Map(
497
+ locales.map((locale) => [locale.code, locale.id])
498
+ );
499
+
500
+ const existing = await this.prismaService.mail.findUnique({
501
+ where: { slug: template.slug },
502
+ });
503
+
504
+ if (existing && overwrite) {
505
+ await this.prismaService.mail_locale.deleteMany({
506
+ where: { mail_id: existing.id },
507
+ });
508
+
509
+ await this.prismaService.mail_var.deleteMany({
510
+ where: { mail_id: existing.id },
511
+ });
512
+
513
+ await this.prismaService.mail_locale.createMany({
514
+ data: template.translations.map((translation) => ({
515
+ mail_id: existing.id,
516
+ locale_id: localeMap.get(translation.code)!,
517
+ subject: translation.subject,
518
+ body: translation.body,
519
+ })),
520
+ });
521
+
522
+ if (template.variables && template.variables.length > 0) {
523
+ await this.prismaService.mail_var.createMany({
524
+ data: template.variables.map((variable) => ({
525
+ mail_id: existing.id,
526
+ name: variable,
527
+ })),
528
+ });
529
+ }
530
+
531
+ imported.push(existing.id);
532
+ } else if (!existing) {
533
+ const newMail = await this.prismaService.mail.create({
534
+ data: {
535
+ slug: template.slug,
536
+ mail_locale: {
537
+ create: template.translations.map((translation) => ({
538
+ locale_id: localeMap.get(translation.code)!,
539
+ subject: translation.subject,
540
+ body: translation.body,
541
+ })),
542
+ },
543
+ mail_var: template.variables
544
+ ? {
545
+ create: template.variables.map((variable) => ({
546
+ name: variable,
547
+ })),
548
+ }
549
+ : undefined,
550
+ },
551
+ });
552
+
553
+ imported.push(newMail.id);
554
+ }
555
+ } catch (error) {
556
+ this.logger.error(`Error importing template ${template.slug}:`, error);
557
+ const message = error instanceof Error ? error.message : 'Unknown error';
558
+ throw new BadRequestException(
559
+ `Failed to import template "${template.slug}": ${message}`
560
+ );
561
+ }
562
+ }
563
+
564
+ return {
565
+ success: true,
566
+ imported: imported.length,
567
+ templates: imported,
568
+ };
569
+ }
464
570
  }
@@ -1,6 +1,6 @@
1
1
  import { Role, User } from '@hed-hog/api';
2
2
  import { Locale } from '@hed-hog/api-locale';
3
- import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common';
3
+ import { Body, Controller, Delete, Get, Headers, Ip, Param, ParseIntPipe, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common';
4
4
  import { FileInterceptor } from '@nestjs/platform-express';
5
5
  import { ChangeEmailDto } from './dto/change-email.dto';
6
6
  import { ChangePasswordDto } from './dto/change-password.dto';
@@ -196,8 +196,14 @@ export class ProfileController {
196
196
  }
197
197
 
198
198
  @Put('change-password')
199
- changePassword(@User() {id}, @Body() body: ChangePasswordDto, @Locale() locale: string) {
200
- return this.profileService.changePassword(locale, id, body);
199
+ changePassword(
200
+ @User() {id},
201
+ @Body() body: ChangePasswordDto,
202
+ @Locale() locale: string,
203
+ @Ip() ipAddress: string,
204
+ @Headers('user-agent') userAgent: string
205
+ ) {
206
+ return this.profileService.changePassword(locale, id, body, ipAddress, userAgent);
201
207
  }
202
208
 
203
209
  @Put('change-email')
@@ -4,18 +4,19 @@ import { User } from '@hed-hog/types';
4
4
  import { BadRequestException, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common';
5
5
  import { JwtService } from '@nestjs/jwt';
6
6
  import type {
7
- VerifiedAuthenticationResponse,
8
- VerifiedRegistrationResponse,
7
+ VerifiedAuthenticationResponse,
8
+ VerifiedRegistrationResponse,
9
9
  } from '@simplewebauthn/server';
10
10
  import {
11
- generateAuthenticationOptions,
12
- generateRegistrationOptions,
13
- verifyAuthenticationResponse,
14
- verifyRegistrationResponse,
11
+ generateAuthenticationOptions,
12
+ generateRegistrationOptions,
13
+ verifyAuthenticationResponse,
14
+ verifyRegistrationResponse,
15
15
  } from '@simplewebauthn/server';
16
16
  import * as qrcode from 'qrcode';
17
17
  import * as speakeasy from 'speakeasy';
18
18
  import { ChallengeService } from '../challenge/challenge.service';
19
+ import { MailService } from '../mail/mail.service';
19
20
  import { SecurityService } from '../security/security.service';
20
21
  import { SettingService } from '../setting/setting.service';
21
22
  import { TokenService } from '../token/token.service';
@@ -41,6 +42,8 @@ export class ProfileService {
41
42
  private readonly challenge: ChallengeService,
42
43
  @Inject(forwardRef(() => TokenService))
43
44
  private readonly token: TokenService,
45
+ @Inject(forwardRef(() => MailService))
46
+ private readonly mail: MailService,
44
47
  ) {}
45
48
 
46
49
  async changeEmail(locale: string, userId: number, { email, password, pin }: ChangeEmailDto) {
@@ -335,7 +338,7 @@ export class ProfileService {
335
338
  return this.user.changeAvatar(locale, userId, file);
336
339
  }
337
340
 
338
- async changePassword(locale:string, userId: number, {currentPassword, newPassword}: ChangePasswordDto) {
341
+ async changePassword(locale:string, userId: number, {currentPassword, newPassword}: ChangePasswordDto, ipAddress?: string, userAgent?: string) {
339
342
  const credential = await this.prisma.user_credential.findFirst({
340
343
  where: { user_id: userId, type: 'password' },
341
344
  });
@@ -355,6 +358,32 @@ export class ProfileService {
355
358
  data: { hash: passwordHash },
356
359
  });
357
360
 
361
+ // Get user data and email for notification
362
+ const user = await this.prisma.user.findUnique({
363
+ where: { id: userId },
364
+ include: {
365
+ user_identifier: {
366
+ where: { type: 'email', enabled: true, verified_at: { not: null } },
367
+ select: { value: true },
368
+ },
369
+ },
370
+ });
371
+
372
+ if (user?.user_identifier?.[0]?.value) {
373
+ this.mail.sendTemplatedMail(locale, {
374
+ email: user.user_identifier[0].value,
375
+ slug: 'auth-change-password-notification',
376
+ variables: {
377
+ name: user.name,
378
+ userAgent: userAgent || 'Unknown',
379
+ ipAddress: ipAddress || 'Unknown',
380
+ location: 'Unknown',
381
+ },
382
+ }).catch(error => {
383
+ console.error('Failed to send password change notification:', error);
384
+ });
385
+ }
386
+
358
387
  return { success: true };
359
388
  }
360
389