@solidxai/core 0.1.12-beta.1 → 0.1.12-beta.3

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 (169) hide show
  1. package/dist/constants/media-file-types.d.ts +5 -0
  2. package/dist/constants/media-file-types.d.ts.map +1 -0
  3. package/dist/constants/media-file-types.js +27 -0
  4. package/dist/constants/media-file-types.js.map +1 -0
  5. package/dist/controllers/media.controller.d.ts +14 -11
  6. package/dist/controllers/media.controller.d.ts.map +1 -1
  7. package/dist/controllers/media.controller.js +72 -33
  8. package/dist/controllers/media.controller.js.map +1 -1
  9. package/dist/dtos/create-chatter-message.dto.d.ts +1 -0
  10. package/dist/dtos/create-chatter-message.dto.d.ts.map +1 -1
  11. package/dist/dtos/create-chatter-message.dto.js +7 -1
  12. package/dist/dtos/create-chatter-message.dto.js.map +1 -1
  13. package/dist/dtos/create-media.dto.d.ts +1 -0
  14. package/dist/dtos/create-media.dto.d.ts.map +1 -1
  15. package/dist/dtos/create-media.dto.js +7 -1
  16. package/dist/dtos/create-media.dto.js.map +1 -1
  17. package/dist/dtos/post-chatter-message.dto.d.ts +1 -0
  18. package/dist/dtos/post-chatter-message.dto.d.ts.map +1 -1
  19. package/dist/dtos/post-chatter-message.dto.js +6 -1
  20. package/dist/dtos/post-chatter-message.dto.js.map +1 -1
  21. package/dist/dtos/update-chatter-message.dto.d.ts +1 -0
  22. package/dist/dtos/update-chatter-message.dto.d.ts.map +1 -1
  23. package/dist/dtos/update-chatter-message.dto.js +8 -1
  24. package/dist/dtos/update-chatter-message.dto.js.map +1 -1
  25. package/dist/dtos/update-chatter-note-message.dto.d.ts +1 -0
  26. package/dist/dtos/update-chatter-note-message.dto.d.ts.map +1 -1
  27. package/dist/dtos/update-chatter-note-message.dto.js +7 -1
  28. package/dist/dtos/update-chatter-note-message.dto.js.map +1 -1
  29. package/dist/dtos/update-media.dto.d.ts +1 -0
  30. package/dist/dtos/update-media.dto.d.ts.map +1 -1
  31. package/dist/dtos/update-media.dto.js +7 -1
  32. package/dist/dtos/update-media.dto.js.map +1 -1
  33. package/dist/entities/chatter-message.entity.d.ts +1 -0
  34. package/dist/entities/chatter-message.entity.d.ts.map +1 -1
  35. package/dist/entities/chatter-message.entity.js +5 -1
  36. package/dist/entities/chatter-message.entity.js.map +1 -1
  37. package/dist/entities/media.entity.d.ts +1 -0
  38. package/dist/entities/media.entity.d.ts.map +1 -1
  39. package/dist/entities/media.entity.js +9 -1
  40. package/dist/entities/media.entity.js.map +1 -1
  41. package/dist/enums/auth-type.enum.d.ts +2 -1
  42. package/dist/enums/auth-type.enum.d.ts.map +1 -1
  43. package/dist/enums/auth-type.enum.js +1 -0
  44. package/dist/enums/auth-type.enum.js.map +1 -1
  45. package/dist/guards/authentication.guard.d.ts +3 -1
  46. package/dist/guards/authentication.guard.d.ts.map +1 -1
  47. package/dist/guards/authentication.guard.js +5 -1
  48. package/dist/guards/authentication.guard.js.map +1 -1
  49. package/dist/guards/media-signed-url.guard.d.ts +9 -0
  50. package/dist/guards/media-signed-url.guard.d.ts.map +1 -0
  51. package/dist/guards/media-signed-url.guard.js +49 -0
  52. package/dist/guards/media-signed-url.guard.js.map +1 -0
  53. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
  54. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +10 -13
  55. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
  56. package/dist/interfaces/chatter-mention-notification.interface.d.ts +27 -0
  57. package/dist/interfaces/chatter-mention-notification.interface.d.ts.map +1 -0
  58. package/dist/interfaces/chatter-mention-notification.interface.js +3 -0
  59. package/dist/interfaces/chatter-mention-notification.interface.js.map +1 -0
  60. package/dist/jobs/chatter-mention-notification-email-queue-handler.service.d.ts +17 -0
  61. package/dist/jobs/chatter-mention-notification-email-queue-handler.service.d.ts.map +1 -0
  62. package/dist/jobs/chatter-mention-notification-email-queue-handler.service.js +106 -0
  63. package/dist/jobs/chatter-mention-notification-email-queue-handler.service.js.map +1 -0
  64. package/dist/jobs/database/chatter-mention-notification-email-publisher-database.service.d.ts +12 -0
  65. package/dist/jobs/database/chatter-mention-notification-email-publisher-database.service.d.ts.map +1 -0
  66. package/dist/jobs/database/chatter-mention-notification-email-publisher-database.service.js +39 -0
  67. package/dist/jobs/database/chatter-mention-notification-email-publisher-database.service.js.map +1 -0
  68. package/dist/jobs/database/chatter-mention-notification-email-queue-options-database.d.ts +8 -0
  69. package/dist/jobs/database/chatter-mention-notification-email-queue-options-database.d.ts.map +1 -0
  70. package/dist/jobs/database/chatter-mention-notification-email-queue-options-database.js +10 -0
  71. package/dist/jobs/database/chatter-mention-notification-email-queue-options-database.js.map +1 -0
  72. package/dist/jobs/database/chatter-mention-notification-email-subscriber-database.service.d.ts +20 -0
  73. package/dist/jobs/database/chatter-mention-notification-email-subscriber-database.service.d.ts.map +1 -0
  74. package/dist/jobs/database/chatter-mention-notification-email-subscriber-database.service.js +48 -0
  75. package/dist/jobs/database/chatter-mention-notification-email-subscriber-database.service.js.map +1 -0
  76. package/dist/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.d.ts +12 -0
  77. package/dist/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.d.ts.map +1 -0
  78. package/dist/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.js +39 -0
  79. package/dist/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.js.map +1 -0
  80. package/dist/jobs/rabbitmq/chatter-mention-notification-email-queue-options.d.ts +8 -0
  81. package/dist/jobs/rabbitmq/chatter-mention-notification-email-queue-options.d.ts.map +1 -0
  82. package/dist/jobs/rabbitmq/chatter-mention-notification-email-queue-options.js +10 -0
  83. package/dist/jobs/rabbitmq/chatter-mention-notification-email-queue-options.js.map +1 -0
  84. package/dist/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.d.ts +18 -0
  85. package/dist/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.d.ts.map +1 -0
  86. package/dist/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.js +45 -0
  87. package/dist/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.js.map +1 -0
  88. package/dist/jobs/redis/chatter-mention-notification-email-publisher-redis.service.d.ts +12 -0
  89. package/dist/jobs/redis/chatter-mention-notification-email-publisher-redis.service.d.ts.map +1 -0
  90. package/dist/jobs/redis/chatter-mention-notification-email-publisher-redis.service.js +39 -0
  91. package/dist/jobs/redis/chatter-mention-notification-email-publisher-redis.service.js.map +1 -0
  92. package/dist/jobs/redis/chatter-mention-notification-email-queue-options-redis.d.ts +8 -0
  93. package/dist/jobs/redis/chatter-mention-notification-email-queue-options-redis.d.ts.map +1 -0
  94. package/dist/jobs/redis/chatter-mention-notification-email-queue-options-redis.js +10 -0
  95. package/dist/jobs/redis/chatter-mention-notification-email-queue-options-redis.js.map +1 -0
  96. package/dist/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.d.ts +18 -0
  97. package/dist/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.d.ts.map +1 -0
  98. package/dist/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.js +45 -0
  99. package/dist/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.js.map +1 -0
  100. package/dist/repository/media.repository.d.ts.map +1 -1
  101. package/dist/repository/media.repository.js +2 -0
  102. package/dist/repository/media.repository.js.map +1 -1
  103. package/dist/seeders/seed-data/solid-core-metadata.json +109 -3
  104. package/dist/services/chatter-message.service.d.ts +7 -1
  105. package/dist/services/chatter-message.service.d.ts.map +1 -1
  106. package/dist/services/chatter-message.service.js +85 -4
  107. package/dist/services/chatter-message.service.js.map +1 -1
  108. package/dist/services/media-download-url.service.d.ts +21 -0
  109. package/dist/services/media-download-url.service.d.ts.map +1 -0
  110. package/dist/services/media-download-url.service.js +67 -0
  111. package/dist/services/media-download-url.service.js.map +1 -0
  112. package/dist/services/media-storage.utils.d.ts +27 -0
  113. package/dist/services/media-storage.utils.d.ts.map +1 -0
  114. package/dist/services/media-storage.utils.js +82 -0
  115. package/dist/services/media-storage.utils.js.map +1 -0
  116. package/dist/services/media.service.d.ts +20 -4
  117. package/dist/services/media.service.d.ts.map +1 -1
  118. package/dist/services/media.service.js +157 -55
  119. package/dist/services/media.service.js.map +1 -1
  120. package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts +4 -4
  121. package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
  122. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +37 -63
  123. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
  124. package/dist/services/mediaStorageProviders/file-storage-provider.d.ts +5 -5
  125. package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
  126. package/dist/services/mediaStorageProviders/file-storage-provider.js +37 -38
  127. package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
  128. package/dist/solid-core.module.d.ts.map +1 -1
  129. package/dist/solid-core.module.js +25 -1
  130. package/dist/solid-core.module.js.map +1 -1
  131. package/dist/subscribers/audit.subscriber.js +1 -1
  132. package/dist/subscribers/audit.subscriber.js.map +1 -1
  133. package/package.json +1 -1
  134. package/src/constants/media-file-types.ts +44 -0
  135. package/src/controllers/media.controller.ts +44 -24
  136. package/src/dtos/create-chatter-message.dto.ts +5 -0
  137. package/src/dtos/create-media.dto.ts +8 -3
  138. package/src/dtos/post-chatter-message.dto.ts +4 -0
  139. package/src/dtos/update-chatter-message.dto.ts +6 -0
  140. package/src/dtos/update-chatter-note-message.dto.ts +5 -0
  141. package/src/dtos/update-media.dto.ts +8 -3
  142. package/src/entities/chatter-message.entity.ts +3 -0
  143. package/src/entities/media.entity.ts +4 -1
  144. package/src/enums/auth-type.enum.ts +1 -0
  145. package/src/guards/authentication.guard.ts +3 -0
  146. package/src/guards/media-signed-url.guard.ts +41 -0
  147. package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +12 -24
  148. package/src/interfaces/chatter-mention-notification.interface.ts +29 -0
  149. package/src/jobs/chatter-mention-notification-email-queue-handler.service.ts +114 -0
  150. package/src/jobs/database/chatter-mention-notification-email-publisher-database.service.ts +23 -0
  151. package/src/jobs/database/chatter-mention-notification-email-queue-options-database.ts +9 -0
  152. package/src/jobs/database/chatter-mention-notification-email-subscriber-database.service.ts +32 -0
  153. package/src/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.ts +23 -0
  154. package/src/jobs/rabbitmq/chatter-mention-notification-email-queue-options.ts +9 -0
  155. package/src/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.ts +30 -0
  156. package/src/jobs/redis/chatter-mention-notification-email-publisher-redis.service.ts +23 -0
  157. package/src/jobs/redis/chatter-mention-notification-email-queue-options-redis.ts +9 -0
  158. package/src/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.ts +30 -0
  159. package/src/repository/media.repository.ts +2 -0
  160. package/src/seeders/seed-data/email-templates/chatter-mention-notification.handlebars.html +200 -0
  161. package/src/seeders/seed-data/solid-core-metadata.json +109 -3
  162. package/src/services/chatter-message.service.ts +97 -2
  163. package/src/services/media-download-url.service.ts +67 -0
  164. package/src/services/media-storage.utils.ts +79 -0
  165. package/src/services/media.service.ts +189 -59
  166. package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +52 -76
  167. package/src/services/mediaStorageProviders/file-storage-provider.ts +44 -48
  168. package/src/solid-core.module.ts +35 -2
  169. package/src/subscribers/audit.subscriber.ts +1 -1
@@ -1,5 +1,5 @@
1
- import { ApiProperty } from '@nestjs/swagger';
2
- import { IsInt } from 'class-validator';
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsBoolean, IsInt } from 'class-validator';
3
3
  import { IsNotEmpty, IsString, IsOptional } from 'class-validator';
4
4
  export class CreateMediaDto {
5
5
  @IsNotEmpty()
@@ -7,6 +7,11 @@ export class CreateMediaDto {
7
7
  @ApiProperty()
8
8
  entityId: number;
9
9
 
10
+ @IsOptional()
11
+ @IsBoolean()
12
+ @ApiPropertyOptional({ default: true })
13
+ isPublic?: boolean;
14
+
10
15
  @IsOptional()
11
16
  @IsString()
12
17
  @ApiProperty()
@@ -56,4 +61,4 @@ fieldMetadataId: number;
56
61
  @IsOptional()
57
62
  @ApiProperty()
58
63
  fieldMetadataUserKey: string;
59
- }
64
+ }
@@ -13,6 +13,10 @@ export class PostChatterMessageDto {
13
13
  @IsNotEmpty()
14
14
  messageBody: string;
15
15
 
16
+ @IsString()
17
+ @IsOptional()
18
+ messageBodyMentions?: string;
19
+
16
20
  @IsString()
17
21
  @IsOptional()
18
22
  messageSubType?: string;
@@ -24,6 +24,12 @@ export class UpdateChatterMessageDto {
24
24
  @ApiProperty()
25
25
  messageBody: string;
26
26
 
27
+ @IsNotEmpty()
28
+ @IsOptional()
29
+ @IsString()
30
+ @ApiProperty()
31
+ messageBodyMentions: string;
32
+
27
33
  @IsNotEmpty()
28
34
  @IsOptional()
29
35
  @IsInt()
@@ -7,6 +7,11 @@ export class UpdateChatterNoteMessageDto {
7
7
  @ApiProperty()
8
8
  messageBody: string;
9
9
 
10
+ @IsString()
11
+ @IsOptional()
12
+ @ApiProperty()
13
+ messageBodyMentions?: string;
14
+
10
15
  @IsString()
11
16
  @IsOptional()
12
17
  @ApiProperty({ required: false, description: 'Comma-separated media IDs to remove from this note.' })
@@ -1,5 +1,5 @@
1
- import { IsInt,IsOptional, IsNotEmpty, IsString } from 'class-validator';
2
- import { ApiProperty } from '@nestjs/swagger';
1
+ import { IsBoolean, IsInt,IsOptional, IsNotEmpty, IsString } from 'class-validator';
2
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
3
3
  export class UpdateMediaDto {
4
4
  @IsOptional()
5
5
  @IsInt()
@@ -11,6 +11,11 @@ export class UpdateMediaDto {
11
11
  @ApiProperty()
12
12
  entityId: number;
13
13
 
14
+ @IsOptional()
15
+ @IsBoolean()
16
+ @ApiPropertyOptional({ default: true })
17
+ isPublic?: boolean;
18
+
14
19
  @IsOptional()
15
20
  @IsString()
16
21
  @ApiProperty()
@@ -60,4 +65,4 @@ fieldMetadataId: number;
60
65
  @IsOptional()
61
66
  @ApiProperty()
62
67
  fieldMetadataUserKey: string;
63
- }
68
+ }
@@ -18,6 +18,9 @@ export class ChatterMessage extends CommonEntity {
18
18
  @Column({ nullable: true, ...getColumnType('longText') })
19
19
  messageBody: string;
20
20
 
21
+ @Column({ nullable: true, ...getColumnType('longText') })
22
+ messageBodyMentions: string;
23
+
21
24
  @Index()
22
25
  @Column({ type: "integer" })
23
26
  coModelEntityId: number;
@@ -9,6 +9,9 @@ export class Media extends CommonEntity {
9
9
  @Column({ type: "integer" })
10
10
  entityId: number;
11
11
 
12
+ @Column({ name: "is_public", default: true })
13
+ isPublic: boolean = true;
14
+
12
15
  @Column({ type: "varchar", nullable: true })
13
16
  relativeUri: string;
14
17
 
@@ -36,4 +39,4 @@ export class Media extends CommonEntity {
36
39
  @ManyToOne(() => FieldMetadata, { nullable: false })
37
40
  @JoinColumn()
38
41
  fieldMetadata: FieldMetadata;
39
- }
42
+ }
@@ -2,4 +2,5 @@ export enum AuthType {
2
2
  Bearer,
3
3
  ApiKey,
4
4
  None,
5
+ MediaSignedUrl,
5
6
  }
@@ -9,6 +9,7 @@ import { AUTH_TYPE_KEY } from '../decorators/auth.decorator';
9
9
  import { AuthType } from '../enums/auth-type.enum';
10
10
  import { AccessTokenGuard } from './access-token.guard';
11
11
  import { ApiKeyGuard } from './api-key.guard';
12
+ import { MediaSignedUrlGuard } from './media-signed-url.guard';
12
13
  import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
13
14
  import { PermissionMetadataService } from '../services/permission-metadata.service';
14
15
  import { ClsService } from 'nestjs-cls';
@@ -22,12 +23,14 @@ export class AuthenticationGuard implements CanActivate {
22
23
  [AuthType.Bearer]: this.accessTokenGuard,
23
24
  [AuthType.ApiKey]: this.apiKeyGuard,
24
25
  [AuthType.None]: { canActivate: () => true },
26
+ [AuthType.MediaSignedUrl]: this.mediaSignedUrlGuard,
25
27
  };
26
28
 
27
29
  constructor(
28
30
  private readonly reflector: Reflector,
29
31
  private readonly accessTokenGuard: AccessTokenGuard,
30
32
  private readonly apiKeyGuard: ApiKeyGuard,
33
+ private readonly mediaSignedUrlGuard: MediaSignedUrlGuard,
31
34
  private readonly permissionService: PermissionMetadataService,
32
35
  private readonly cls: ClsService,
33
36
  ) { }
@@ -0,0 +1,41 @@
1
+ import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
2
+ import { Request } from 'express';
3
+ import { MediaDownloadUrlService } from 'src/services/media-download-url.service';
4
+
5
+ @Injectable()
6
+ export class MediaSignedUrlGuard implements CanActivate {
7
+ constructor(private readonly mediaDownloadUrlService: MediaDownloadUrlService) { }
8
+
9
+ async canActivate(context: ExecutionContext): Promise<boolean> {
10
+ const request = context.switchToHttp().getRequest<Request>();
11
+ const token = this.extractTokenFromQuery(request);
12
+ if (!token) {
13
+ throw new UnauthorizedException();
14
+ }
15
+
16
+ let payload: { mediaId: number };
17
+ try {
18
+ payload = await this.mediaDownloadUrlService.verifyDownloadToken(token);
19
+ } catch {
20
+ throw new UnauthorizedException();
21
+ }
22
+
23
+ const routeMediaId = Number(request.params?.id);
24
+ if (!Number.isFinite(routeMediaId) || payload.mediaId !== routeMediaId) {
25
+ throw new UnauthorizedException();
26
+ }
27
+
28
+ // No REQUEST_USER_KEY is ever set for token auth (only Bearer/ApiKey guards set it), so
29
+ // the sibling global APP_GUARD PermissionsGuard would otherwise reject this request for
30
+ // lack of a per-user permission grant. Flag it the same way AuthenticationGuard flags
31
+ // Public-role-exempt requests - token possession IS the full authorization decision here.
32
+ request['isListedInPublicRole'] = true;
33
+
34
+ return true;
35
+ }
36
+
37
+ private extractTokenFromQuery(request: Request): string | undefined {
38
+ const token = request.query?.token;
39
+ return typeof token === 'string' ? token : undefined;
40
+ }
41
+ }
@@ -1,4 +1,5 @@
1
1
  import { FieldCrudManager, ValidationError } from "src/interfaces";
2
+ import { DANGEROUS_EXTENSIONS, EXT_TO_MEDIA_TYPE, MediaCategory } from "src/constants/media-file-types";
2
3
 
3
4
  export enum SolidMediaType {
4
5
  mediaSingle = 'mediaSingle',
@@ -14,7 +15,7 @@ export interface MediaFieldOptions {
14
15
  isUpdate: boolean | undefined | null;
15
16
  }
16
17
 
17
- type MediaType = 'image' | 'audio' | 'video' | 'file' | 'pdf';
18
+ type MediaType = MediaCategory;
18
19
 
19
20
  const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
20
21
  // Images
@@ -25,7 +26,6 @@ const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
25
26
  'image/gif': 'image',
26
27
  'image/bmp': 'image',
27
28
  'image/tiff': 'image',
28
- 'image/svg+xml': 'image',
29
29
  'image/heic': 'image',
30
30
  'image/heif': 'image',
31
31
 
@@ -72,35 +72,23 @@ const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
72
72
  'application/x-zip-compressed': 'file',
73
73
  'application/x-rar-compressed': 'file',
74
74
  'application/x-7z-compressed': 'file',
75
-
76
- // Common binary fallback category
77
- 'application/octet-stream': 'file',
78
75
  };
79
76
 
80
- const EXT_TO_MEDIA_TYPE: Record<string, MediaType> = {
81
- // Images
82
- png: 'image', jpg: 'image', jpeg: 'image', webp: 'image', gif: 'image', bmp: 'image', tiff: 'image', svg: 'image', heic: 'image', heif: 'image',
83
-
84
- // Audio
85
- mp3: 'audio', wav: 'audio', ogg: 'audio', aac: 'audio', m4a: 'audio', flac: 'audio',
86
-
87
- // Video
88
- mp4: 'video', mov: 'video', avi: 'video', mkv: 'video', mpeg: 'video', mpg: 'video', '3gp': 'video', '3g2': 'video',
89
-
90
- // Files
91
- pdf: 'file', txt: 'file', md: 'file', csv: 'file', json: 'file',
92
- doc: 'file', docx: 'file', xls: 'file', xlsx: 'file', ppt: 'file', pptx: 'file',
93
- zip: 'file', rar: 'file', '7z': 'file',
94
- };
95
-
96
-
97
77
  export class MediaFieldCrudManager implements FieldCrudManager {
98
78
 
99
79
  constructor(private readonly options: MediaFieldOptions) {
100
80
  }
101
81
 
102
82
  private resolveMediaType(mimetype?: string, filename?: string): MediaType | null {
83
+ const ext = (filename || '').split('.').pop()?.toLowerCase();
84
+ if (ext && DANGEROUS_EXTENSIONS.has(ext)) {
85
+ return null;
86
+ }
87
+
103
88
  const mt = (mimetype || '').toLowerCase().trim();
89
+ if (mt === 'image/svg+xml' || mt === 'text/html' || mt === 'application/xhtml+xml') {
90
+ return null;
91
+ }
104
92
  if (mt && MIME_TO_MEDIA_TYPE[mt]) {
105
93
  return MIME_TO_MEDIA_TYPE[mt];
106
94
  }
@@ -110,8 +98,8 @@ export class MediaFieldCrudManager implements FieldCrudManager {
110
98
  if (mt.startsWith('audio/')) return 'audio';
111
99
  if (mt.startsWith('video/')) return 'video';
112
100
 
113
- // Fallback to extension if provided
114
- const ext = (filename || '').split('.').pop()?.toLowerCase();
101
+ // Fallback to extension if provided (also covers generic/unrecognized
102
+ // mimetypes such as application/octet-stream).
115
103
  if (ext && EXT_TO_MEDIA_TYPE[ext]) {
116
104
  return EXT_TO_MEDIA_TYPE[ext];
117
105
  }
@@ -0,0 +1,29 @@
1
+ export interface ChatterMentionNotificationMention {
2
+ id?: number;
3
+ username: string;
4
+ displayName: string;
5
+ }
6
+
7
+ export interface ChatterMentionNotificationActor {
8
+ id?: number;
9
+ username?: string;
10
+ email?: string;
11
+ }
12
+
13
+ export interface ChatterMentionNotificationEntity {
14
+ id: number;
15
+ modelName: string;
16
+ moduleName?: string;
17
+ displayName: string;
18
+ userKey?: string;
19
+ }
20
+
21
+ export interface ChatterMentionNotificationPayload {
22
+ templateName: string;
23
+ mentions: ChatterMentionNotificationMention[];
24
+ actor: ChatterMentionNotificationActor;
25
+ noteBody: string;
26
+ entity: ChatterMentionNotificationEntity;
27
+ parentEntity: string;
28
+ parentEntityId: number;
29
+ }
@@ -0,0 +1,114 @@
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { MailFactory } from 'src/factories/mail.factory';
3
+ import { ChatterMentionNotificationPayload } from 'src/interfaces/chatter-mention-notification.interface';
4
+ import { QueueMessage } from 'src/interfaces/mq';
5
+ import { UserRepository } from 'src/repository/user.repository';
6
+ import { SettingService } from 'src/services/setting.service';
7
+ import type { SolidCoreSetting } from 'src/services/settings/default-settings-provider.service';
8
+ import { In } from 'typeorm';
9
+
10
+ @Injectable()
11
+ export class ChatterMentionNotificationEmailQueueHandler {
12
+ private readonly logger = new Logger(ChatterMentionNotificationEmailQueueHandler.name);
13
+
14
+ constructor(
15
+ private readonly mailFactory: MailFactory,
16
+ private readonly userRepository: UserRepository,
17
+ private readonly settingService: SettingService,
18
+ ) {}
19
+
20
+ private buildAdminRecordFormUrl(frontendAdminBaseUrl: string, moduleName: string, modelName: string, recordId: number) {
21
+ if (!frontendAdminBaseUrl || !moduleName || !modelName || !recordId) return '';
22
+ const normalizedModelName = modelName
23
+ .trim()
24
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
25
+ .replace(/[_\s]+/g, '-')
26
+ .toLowerCase();
27
+ return `${frontendAdminBaseUrl.replace(/\/+$/, '')}/admin/core/${moduleName.trim()}/${normalizedModelName}/form/${recordId}`;
28
+ }
29
+
30
+ async handle(message: QueueMessage<ChatterMentionNotificationPayload>) {
31
+ const mailService = this.mailFactory.getMailService();
32
+ const payload = message.payload;
33
+
34
+ if (!payload?.mentions?.length) {
35
+ this.logger.debug('Skipping chatter mention notification because no mentions were provided.');
36
+ return { sent: 0 };
37
+ }
38
+
39
+ const mentionUserIds = payload.mentions
40
+ .map(mention => Number(mention.id))
41
+ .filter(id => Number.isInteger(id) && id > 0);
42
+ const userIdsToLoad = Array.from(new Set([
43
+ ...mentionUserIds,
44
+ ...(payload.actor?.id ? [Number(payload.actor.id)] : []),
45
+ ]));
46
+
47
+ if (userIdsToLoad.length === 0) {
48
+ this.logger.debug('Skipping chatter mention notification because mention user ids were not provided.');
49
+ return { sent: 0 };
50
+ }
51
+
52
+ const users = await this.userRepository.find({
53
+ where: { id: In(userIdsToLoad) },
54
+ });
55
+ const usersById = new Map(users.map(user => [Number(user.id), user]));
56
+ const actor = payload.actor?.id ? usersById.get(Number(payload.actor.id)) : null;
57
+ const recipients = Array.from(new Set(mentionUserIds))
58
+ .map(id => {
59
+ const user = usersById.get(id);
60
+ const mention = payload.mentions.find(item => Number(item.id) === id);
61
+ return {
62
+ id,
63
+ username: user?.username || mention?.username || '',
64
+ displayName: user?.fullName || mention?.displayName || user?.username || mention?.username || '',
65
+ email: user?.email || '',
66
+ };
67
+ })
68
+ .filter(recipient => Boolean(recipient.email));
69
+
70
+ if (recipients.length === 0) {
71
+ this.logger.debug('Skipping chatter mention notification because no mentioned users have email addresses.');
72
+ return { sent: 0 };
73
+ }
74
+
75
+ const frontendAdminBaseUrl = this.settingService.getConfigValue<SolidCoreSetting>('frontendAdminBaseUrl');
76
+ const commonTemplateParams = {
77
+ mentionedByDisplayName: actor?.fullName || payload.actor?.username || payload.actor?.email || 'Someone',
78
+ mentionedByEmail: actor?.email || payload.actor?.email,
79
+ noteBody: payload.noteBody,
80
+ entityDisplayName: payload.entity.displayName,
81
+ entityUserKey: payload.entity.userKey,
82
+ entityFormUrl: this.buildAdminRecordFormUrl(
83
+ frontendAdminBaseUrl,
84
+ payload.entity.moduleName,
85
+ payload.entity.modelName,
86
+ payload.entity.id,
87
+ ),
88
+ solidAppName: this.settingService.getConfigValue<SolidCoreSetting>('appTitle'),
89
+ solidAppWebsiteUrl: this.settingService.getConfigValue<SolidCoreSetting>('solidAppWebsiteUrl'),
90
+ companyLogoUrl: this.settingService.getConfigValue<SolidCoreSetting>('companylogo'),
91
+ };
92
+
93
+ let sent = 0;
94
+ for (const recipient of recipients) {
95
+ await mailService.sendEmailUsingTemplate(
96
+ recipient.email,
97
+ payload.templateName,
98
+ {
99
+ ...commonTemplateParams,
100
+ mentionedUserDisplayName: recipient.displayName || recipient.username,
101
+ mentionedUserUsername: recipient.username,
102
+ },
103
+ false,
104
+ null,
105
+ null,
106
+ payload.parentEntity,
107
+ payload.parentEntityId,
108
+ );
109
+ sent += 1;
110
+ }
111
+
112
+ return { sent };
113
+ }
114
+ }
@@ -0,0 +1,23 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ChatterMentionNotificationPayload } from 'src/interfaces/chatter-mention-notification.interface';
3
+ import { QueuesModuleOptions } from 'src/interfaces';
4
+ import { MqMessageQueueService } from 'src/services/mq-message-queue.service';
5
+ import { MqMessageService } from 'src/services/mq-message.service';
6
+ import { DatabasePublisher } from 'src/services/queues/database-publisher.service';
7
+ import queueOptions from './chatter-mention-notification-email-queue-options-database';
8
+
9
+ @Injectable()
10
+ export class ChatterMentionNotificationEmailQueuePublisherDatabase extends DatabasePublisher<ChatterMentionNotificationPayload> {
11
+ constructor(
12
+ protected readonly mqMessageService: MqMessageService,
13
+ protected readonly mqMessageQueueService: MqMessageQueueService,
14
+ ) {
15
+ super(mqMessageService, mqMessageQueueService);
16
+ }
17
+
18
+ options(): QueuesModuleOptions {
19
+ return {
20
+ ...queueOptions
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ import { BrokerType } from "src/interfaces";
2
+
3
+ const QUEUE_NAME = 'solid_chatter_mention_notification_email_queue_database';
4
+
5
+ export default {
6
+ name: QUEUE_NAME,
7
+ type: BrokerType.Database,
8
+ queueName: QUEUE_NAME,
9
+ };
@@ -0,0 +1,32 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ChatterMentionNotificationEmailQueueHandler } from 'src/jobs/chatter-mention-notification-email-queue-handler.service';
3
+ import { ChatterMentionNotificationPayload } from 'src/interfaces/chatter-mention-notification.interface';
4
+ import { QueuesModuleOptions } from 'src/interfaces';
5
+ import { QueueMessage } from 'src/interfaces/mq';
6
+ import { MqMessageQueueService } from 'src/services/mq-message-queue.service';
7
+ import { MqMessageService } from 'src/services/mq-message.service';
8
+ import { PollerService } from 'src/services/poller.service';
9
+ import { DatabaseSubscriber } from 'src/services/queues/database-subscriber.service';
10
+ import queueOptions from './chatter-mention-notification-email-queue-options-database';
11
+
12
+ @Injectable()
13
+ export class ChatterMentionNotificationEmailQueueSubscriberDatabase extends DatabaseSubscriber<ChatterMentionNotificationPayload> {
14
+ constructor(
15
+ readonly mqMessageService: MqMessageService,
16
+ readonly mqMessageQueueService: MqMessageQueueService,
17
+ readonly poller: PollerService,
18
+ private readonly handler: ChatterMentionNotificationEmailQueueHandler,
19
+ ) {
20
+ super(mqMessageService, mqMessageQueueService, poller);
21
+ }
22
+
23
+ options(): QueuesModuleOptions {
24
+ return {
25
+ ...queueOptions
26
+ };
27
+ }
28
+
29
+ subscribe(message: QueueMessage<ChatterMentionNotificationPayload>) {
30
+ return this.handler.handle(message);
31
+ }
32
+ }
@@ -0,0 +1,23 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ChatterMentionNotificationPayload } from 'src/interfaces/chatter-mention-notification.interface';
3
+ import { QueuesModuleOptions } from 'src/interfaces';
4
+ import { MqMessageQueueService } from 'src/services/mq-message-queue.service';
5
+ import { MqMessageService } from 'src/services/mq-message.service';
6
+ import { RabbitMqPublisher } from 'src/services/queues/rabbitmq-publisher.service';
7
+ import queueOptions from './chatter-mention-notification-email-queue-options';
8
+
9
+ @Injectable()
10
+ export class ChatterMentionNotificationEmailQueuePublisherRabbitmq extends RabbitMqPublisher<ChatterMentionNotificationPayload> {
11
+ constructor(
12
+ protected readonly mqMessageService: MqMessageService,
13
+ protected readonly mqMessageQueueService: MqMessageQueueService,
14
+ ) {
15
+ super(mqMessageService, mqMessageQueueService);
16
+ }
17
+
18
+ options(): QueuesModuleOptions {
19
+ return {
20
+ ...queueOptions
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ import { BrokerType } from "src/interfaces";
2
+
3
+ const QUEUE_NAME = 'solid_chatter_mention_notification_email_queue';
4
+
5
+ export default {
6
+ name: QUEUE_NAME,
7
+ type: BrokerType.RabbitMQ,
8
+ queueName: QUEUE_NAME,
9
+ };
@@ -0,0 +1,30 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ChatterMentionNotificationEmailQueueHandler } from 'src/jobs/chatter-mention-notification-email-queue-handler.service';
3
+ import { ChatterMentionNotificationPayload } from 'src/interfaces/chatter-mention-notification.interface';
4
+ import { QueuesModuleOptions } from 'src/interfaces';
5
+ import { QueueMessage } from 'src/interfaces/mq';
6
+ import { MqMessageQueueService } from 'src/services/mq-message-queue.service';
7
+ import { MqMessageService } from 'src/services/mq-message.service';
8
+ import { RabbitMqSubscriber } from 'src/services/queues/rabbitmq-subscriber.service';
9
+ import queueOptions from './chatter-mention-notification-email-queue-options';
10
+
11
+ @Injectable()
12
+ export class ChatterMentionNotificationEmailQueueSubscriberRabbitmq extends RabbitMqSubscriber<ChatterMentionNotificationPayload> {
13
+ constructor(
14
+ readonly mqMessageService: MqMessageService,
15
+ readonly mqMessageQueueService: MqMessageQueueService,
16
+ private readonly handler: ChatterMentionNotificationEmailQueueHandler,
17
+ ) {
18
+ super(mqMessageService, mqMessageQueueService);
19
+ }
20
+
21
+ options(): QueuesModuleOptions {
22
+ return {
23
+ ...queueOptions
24
+ };
25
+ }
26
+
27
+ subscribe(message: QueueMessage<ChatterMentionNotificationPayload>) {
28
+ return this.handler.handle(message);
29
+ }
30
+ }
@@ -0,0 +1,23 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ChatterMentionNotificationPayload } from 'src/interfaces/chatter-mention-notification.interface';
3
+ import { QueuesModuleOptions } from 'src/interfaces';
4
+ import { MqMessageQueueService } from 'src/services/mq-message-queue.service';
5
+ import { MqMessageService } from 'src/services/mq-message.service';
6
+ import { RedisPublisher } from 'src/services/queues/redis-publisher.service';
7
+ import queueOptions from './chatter-mention-notification-email-queue-options-redis';
8
+
9
+ @Injectable()
10
+ export class ChatterMentionNotificationEmailQueuePublisherRedis extends RedisPublisher<ChatterMentionNotificationPayload> {
11
+ constructor(
12
+ protected readonly mqMessageService: MqMessageService,
13
+ protected readonly mqMessageQueueService: MqMessageQueueService,
14
+ ) {
15
+ super(mqMessageService, mqMessageQueueService);
16
+ }
17
+
18
+ options(): QueuesModuleOptions {
19
+ return {
20
+ ...queueOptions
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ import { BrokerType } from "src/interfaces";
2
+
3
+ const QUEUE_NAME = 'solid_chatter_mention_notification_email_queue_redis';
4
+
5
+ export default {
6
+ name: QUEUE_NAME,
7
+ type: BrokerType.Redis,
8
+ queueName: QUEUE_NAME,
9
+ };
@@ -0,0 +1,30 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ChatterMentionNotificationEmailQueueHandler } from 'src/jobs/chatter-mention-notification-email-queue-handler.service';
3
+ import { ChatterMentionNotificationPayload } from 'src/interfaces/chatter-mention-notification.interface';
4
+ import { QueuesModuleOptions } from 'src/interfaces';
5
+ import { QueueMessage } from 'src/interfaces/mq';
6
+ import { MqMessageQueueService } from 'src/services/mq-message-queue.service';
7
+ import { MqMessageService } from 'src/services/mq-message.service';
8
+ import { RedisSubscriber } from 'src/services/queues/redis-subscriber.service';
9
+ import queueOptions from './chatter-mention-notification-email-queue-options-redis';
10
+
11
+ @Injectable()
12
+ export class ChatterMentionNotificationEmailQueueSubscriberRedis extends RedisSubscriber<ChatterMentionNotificationPayload> {
13
+ constructor(
14
+ readonly mqMessageService: MqMessageService,
15
+ readonly mqMessageQueueService: MqMessageQueueService,
16
+ private readonly handler: ChatterMentionNotificationEmailQueueHandler,
17
+ ) {
18
+ super(mqMessageService, mqMessageQueueService);
19
+ }
20
+
21
+ options(): QueuesModuleOptions {
22
+ return {
23
+ ...queueOptions
24
+ };
25
+ }
26
+
27
+ subscribe(message: QueueMessage<ChatterMentionNotificationPayload>) {
28
+ return this.handler.handle(message);
29
+ }
30
+ }
@@ -7,6 +7,7 @@ import { MediaStorageProviderMetadataRepository } from './media-storage-provider
7
7
  import { ModelMetadataRepository } from './model-metadata.repository';
8
8
  import { SecurityRuleRepository } from './security-rule.repository';
9
9
  import { SolidBaseRepository } from './solid-base.repository';
10
+ import { resolveStoredMediaIsPublic } from 'src/services/media-storage.utils';
10
11
 
11
12
  @Injectable()
12
13
  export class MediaRepository extends SolidBaseRepository<Media> {
@@ -44,6 +45,7 @@ export class MediaRepository extends SolidBaseRepository<Media> {
44
45
  id: createDto['mediaStorageProviderMetadataId']
45
46
  },
46
47
  });
48
+ createDto['isPublic'] = resolveStoredMediaIsPublic(createDto['isPublic'], createDto['mediaStorageProviderMetadata']);
47
49
  const media = this.create(createDto);
48
50
  return this.save(media);
49
51
  }