@hed-hog/lms 0.0.357 → 0.0.361

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 (75) hide show
  1. package/dist/course/course-operations-integration.service.d.ts +31 -0
  2. package/dist/course/course-operations-integration.service.d.ts.map +1 -1
  3. package/dist/course/course-operations-integration.service.js +286 -22
  4. package/dist/course/course-operations-integration.service.js.map +1 -1
  5. package/dist/course/course-operations.controller.d.ts +10 -0
  6. package/dist/course/course-operations.controller.d.ts.map +1 -0
  7. package/dist/course/course-operations.controller.js +67 -0
  8. package/dist/course/course-operations.controller.js.map +1 -0
  9. package/dist/course/course-structure.controller.d.ts +3 -1
  10. package/dist/course/course-structure.controller.d.ts.map +1 -1
  11. package/dist/course/course-structure.service.d.ts +3 -1
  12. package/dist/course/course-structure.service.d.ts.map +1 -1
  13. package/dist/course/course-structure.service.js +13 -6
  14. package/dist/course/course-structure.service.js.map +1 -1
  15. package/dist/course/course.module.d.ts.map +1 -1
  16. package/dist/course/course.module.js +15 -2
  17. package/dist/course/course.module.js.map +1 -1
  18. package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
  19. package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
  20. package/dist/course/dto/update-course-operations-config.dto.js +33 -0
  21. package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
  22. package/dist/course/lms-bulk-upload.controller.d.ts +37 -0
  23. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -0
  24. package/dist/course/lms-bulk-upload.controller.js +60 -0
  25. package/dist/course/lms-bulk-upload.controller.js.map +1 -0
  26. package/dist/course/lms-bulk-upload.service.d.ts +42 -0
  27. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -0
  28. package/dist/course/lms-bulk-upload.service.js +169 -0
  29. package/dist/course/lms-bulk-upload.service.js.map +1 -0
  30. package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
  31. package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
  32. package/dist/course/lms-operations-task.subscriber.js +57 -0
  33. package/dist/course/lms-operations-task.subscriber.js.map +1 -0
  34. package/dist/course/lms-setting.controller.d.ts +3 -0
  35. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  36. package/dist/course/lms-setting.controller.js +9 -1
  37. package/dist/course/lms-setting.controller.js.map +1 -1
  38. package/dist/enterprise/enterprise.service.js +1 -1
  39. package/dist/enterprise/enterprise.service.js.map +1 -1
  40. package/dist/instructor/instructor.service.d.ts.map +1 -1
  41. package/dist/instructor/instructor.service.js +12 -3
  42. package/dist/instructor/instructor.service.js.map +1 -1
  43. package/dist/platforma/platforma.controller.d.ts +9 -9
  44. package/hedhog/data/role.yaml +8 -0
  45. package/hedhog/data/route.yaml +62 -0
  46. package/hedhog/data/setting_group.yaml +33 -0
  47. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
  48. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
  49. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
  50. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
  51. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
  52. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
  53. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
  54. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +31 -1
  55. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +91 -20
  56. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -0
  57. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +11 -3
  58. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -2
  59. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
  60. package/hedhog/frontend/app/courses/page.tsx.ejs +21 -88
  61. package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
  62. package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
  63. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
  64. package/package.json +7 -7
  65. package/src/course/course-operations-integration.service.ts +460 -22
  66. package/src/course/course-operations.controller.ts +45 -0
  67. package/src/course/course-structure.service.ts +5 -1
  68. package/src/course/course.module.ts +15 -2
  69. package/src/course/dto/update-course-operations-config.dto.ts +16 -0
  70. package/src/course/lms-bulk-upload.controller.ts +27 -0
  71. package/src/course/lms-bulk-upload.service.ts +204 -0
  72. package/src/course/lms-operations-task.subscriber.ts +44 -0
  73. package/src/course/lms-setting.controller.ts +12 -1
  74. package/src/enterprise/enterprise.service.ts +1 -1
  75. package/src/instructor/instructor.service.ts +12 -3
@@ -0,0 +1,204 @@
1
+ import { FileService } from '@hed-hog/core';
2
+ import { PrismaService } from '@hed-hog/api-prisma';
3
+ import { SettingService } from '@hed-hog/core';
4
+ import {
5
+ BadRequestException,
6
+ Inject,
7
+ Injectable,
8
+ NotFoundException,
9
+ forwardRef,
10
+ } from '@nestjs/common';
11
+
12
+ type IntegrationProfileConfig = {
13
+ access_key_id?: string;
14
+ secret_access_key?: string;
15
+ session_token?: string;
16
+ region?: string;
17
+ bucket?: string;
18
+ role_arn?: string;
19
+ external_id?: string;
20
+ };
21
+
22
+ @Injectable()
23
+ export class LmsBulkUploadService {
24
+ constructor(
25
+ @Inject(forwardRef(() => SettingService))
26
+ private readonly settingService: SettingService,
27
+ @Inject(forwardRef(() => PrismaService))
28
+ private readonly prismaService: PrismaService,
29
+ @Inject(forwardRef(() => FileService))
30
+ private readonly fileService: FileService,
31
+ ) {}
32
+
33
+ async getBulkUploadSettings() {
34
+ const settings = await this.settingService.getSettingValues([
35
+ 'lms-bulk-upload-storage-profile-id',
36
+ 'lms-bulk-upload-s3-bucket',
37
+ 'lms-bulk-upload-sts-duration-seconds',
38
+ ]);
39
+
40
+ return {
41
+ storageProfileId: Number(settings['lms-bulk-upload-storage-profile-id'] || 0) || null,
42
+ bucketName:
43
+ typeof settings['lms-bulk-upload-s3-bucket'] === 'string'
44
+ ? settings['lms-bulk-upload-s3-bucket'].trim()
45
+ : '',
46
+ sessionDurationSeconds:
47
+ Number(settings['lms-bulk-upload-sts-duration-seconds'] || 3600) || 3600,
48
+ };
49
+ }
50
+
51
+ async getTemporaryCredentials(userId: number) {
52
+ const { profile, region, bucket, durationSeconds, prefix } =
53
+ await this.resolveStorageProfileAndBucket(userId);
54
+
55
+ const credentials = profile.config as IntegrationProfileConfig;
56
+ const accessKeyId = String(credentials.access_key_id ?? '').trim();
57
+ const secretAccessKey = String(credentials.secret_access_key ?? '').trim();
58
+ const sessionToken = String(credentials.session_token ?? '').trim() || undefined;
59
+
60
+ if (!accessKeyId || !secretAccessKey) {
61
+ throw new BadRequestException(
62
+ 'Storage profile does not contain AWS credentials (access_key_id/secret_access_key).',
63
+ );
64
+ }
65
+
66
+ const roleArn = String(credentials.role_arn ?? '').trim();
67
+ const externalId = String(credentials.external_id ?? '').trim();
68
+
69
+ const tempCredentials = await this.fileService.getTemporaryCredentials({
70
+ accessKeyId,
71
+ secretAccessKey,
72
+ sessionToken,
73
+ region,
74
+ roleArn: roleArn || undefined,
75
+ externalId: externalId || undefined,
76
+ durationSeconds,
77
+ sessionName: `hedhog-desktop-u${userId}-${Date.now()}`,
78
+ });
79
+
80
+ return {
81
+ provider: 'aws-s3',
82
+ bucket,
83
+ region,
84
+ keyPrefix: prefix,
85
+ credentials: {
86
+ accessKeyId: tempCredentials.AccessKeyId,
87
+ secretAccessKey: tempCredentials.SecretAccessKey,
88
+ sessionToken: tempCredentials.SessionToken,
89
+ expiresAt: tempCredentials.Expiration?.toISOString() ?? null,
90
+ },
91
+ };
92
+ }
93
+
94
+ async verifyFileOnS3(userId: number, payload: { key?: string; fileName?: string }) {
95
+ const { profile, region, bucket, prefix } = await this.resolveStorageProfileAndBucket(userId);
96
+
97
+ const credentials = profile.config as IntegrationProfileConfig;
98
+ const accessKeyId = String(credentials.access_key_id ?? '').trim();
99
+ const secretAccessKey = String(credentials.secret_access_key ?? '').trim();
100
+ const sessionToken = String(credentials.session_token ?? '').trim() || undefined;
101
+
102
+ if (!accessKeyId || !secretAccessKey) {
103
+ throw new BadRequestException(
104
+ 'Storage profile does not contain AWS credentials (access_key_id/secret_access_key).',
105
+ );
106
+ }
107
+
108
+ const objectKey = String(payload.key ?? '').trim() || `${prefix}${String(payload.fileName ?? '').trim()}`;
109
+ if (!objectKey) {
110
+ throw new BadRequestException('Provide file key or fileName to verify on S3.');
111
+ }
112
+
113
+ try {
114
+ const result = await this.fileService.headS3Object({
115
+ accessKeyId,
116
+ secretAccessKey,
117
+ sessionToken,
118
+ region,
119
+ bucket,
120
+ key: objectKey,
121
+ });
122
+
123
+ return {
124
+ isValid: true,
125
+ key: objectKey,
126
+ sizeBytes: result.ContentLength ?? null,
127
+ etag: result.ETag ?? null,
128
+ };
129
+ } catch {
130
+ return {
131
+ isValid: false,
132
+ key: objectKey,
133
+ };
134
+ }
135
+ }
136
+
137
+ private async resolveStorageProfileAndBucket(userId: number) {
138
+ const settings = await this.settingService.getSettingValues([
139
+ 'lms-bulk-upload-storage-profile-id',
140
+ 'lms-bulk-upload-s3-bucket',
141
+ 'lms-bulk-upload-sts-duration-seconds',
142
+ ]);
143
+
144
+ const profileId = Number(settings['lms-bulk-upload-storage-profile-id'] || 0);
145
+ if (!Number.isFinite(profileId) || profileId <= 0) {
146
+ throw new BadRequestException(
147
+ 'LMS bulk upload storage profile is not configured (lms-bulk-upload-storage-profile-id).',
148
+ );
149
+ }
150
+
151
+ const profile = await this.prismaService.integration_profile.findUnique({
152
+ where: { id: profileId },
153
+ include: {
154
+ integration_type: { select: { slug: true } },
155
+ integration_provider: { select: { slug: true } },
156
+ },
157
+ });
158
+
159
+ if (!profile) {
160
+ throw new NotFoundException(`Storage integration profile ${profileId} was not found.`);
161
+ }
162
+
163
+ if (profile.integration_type.slug !== 'storage') {
164
+ throw new BadRequestException(
165
+ `Integration profile ${profileId} is not a storage profile.`,
166
+ );
167
+ }
168
+
169
+ const providerSlug = String(profile.integration_provider.slug ?? '').toLowerCase();
170
+ if (providerSlug !== 's3') {
171
+ throw new BadRequestException(
172
+ `Integration provider ${providerSlug} is not supported for desktop bulk upload. Use AWS S3 profile.`,
173
+ );
174
+ }
175
+
176
+ const config = (profile.config as IntegrationProfileConfig) ?? {};
177
+ const region = String(config.region ?? '').trim() || 'us-east-1';
178
+
179
+ const bucketFromSettings = String(settings['lms-bulk-upload-s3-bucket'] ?? '').trim();
180
+ const bucketFromProfile = String(config.bucket ?? '').trim();
181
+ const bucket = bucketFromSettings || bucketFromProfile;
182
+
183
+ if (!bucket) {
184
+ throw new BadRequestException(
185
+ 'LMS bulk upload bucket is not configured. Set lms-bulk-upload-s3-bucket or profile bucket.',
186
+ );
187
+ }
188
+
189
+ const durationSeconds = Math.max(
190
+ 900,
191
+ Math.min(43200, Number(settings['lms-bulk-upload-sts-duration-seconds'] || 3600) || 3600),
192
+ );
193
+
194
+ const prefix = `desktop/lms/u${userId}/`;
195
+
196
+ return {
197
+ profile,
198
+ region,
199
+ bucket,
200
+ durationSeconds,
201
+ prefix,
202
+ };
203
+ }
204
+ }
@@ -0,0 +1,44 @@
1
+ import { IntegrationDeveloperApiService } from '@hed-hog/core';
2
+ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
3
+ import { PrismaService } from '@hed-hog/api-prisma';
4
+ import { CourseOperationsIntegrationService } from './course-operations-integration.service';
5
+
6
+ @Injectable()
7
+ export class LmsOperationsTaskSubscriber implements OnModuleInit {
8
+ private readonly logger = new Logger(LmsOperationsTaskSubscriber.name);
9
+
10
+ constructor(
11
+ private readonly integrationApi: IntegrationDeveloperApiService,
12
+ private readonly operationsIntegration: CourseOperationsIntegrationService,
13
+ private readonly prisma: PrismaService,
14
+ ) {}
15
+
16
+ onModuleInit(): void {
17
+ this.integrationApi.subscribeMany([
18
+ {
19
+ eventName: 'operations.task.updated',
20
+ consumerName: 'lms.lesson-status-from-task',
21
+ priority: 5,
22
+ handler: async (event) => {
23
+ try {
24
+ const status = event.payload?.status as string | undefined;
25
+ if (status !== 'done') return;
26
+
27
+ const taskId = Number(event.aggregateId);
28
+ if (!taskId || !Number.isInteger(taskId) || taskId <= 0) return;
29
+
30
+ await this.operationsIntegration.applyTaskCompletionToLesson(
31
+ taskId,
32
+ '',
33
+ this.prisma,
34
+ );
35
+ } catch (err) {
36
+ this.logger.error(
37
+ `Error applying task completion to lesson: ${String(err)}`,
38
+ );
39
+ }
40
+ },
41
+ },
42
+ ]);
43
+ }
44
+ }
@@ -1,6 +1,6 @@
1
+ import { Role } from '@hed-hog/api';
1
2
  import { SettingService } from '@hed-hog/core';
2
3
  import { Controller, forwardRef, Get, Inject } from '@nestjs/common';
3
- import { Role } from '@hed-hog/api';
4
4
 
5
5
  @Role()
6
6
  @Controller('lms/settings')
@@ -18,6 +18,9 @@ export class LmsSettingController {
18
18
  'lms-audio-transcription-enabled',
19
19
  'lms-youtube-provider-enabled',
20
20
  'lms-vimeo-provider-enabled',
21
+ 'lms-bulk-upload-storage-profile-id',
22
+ 'lms-bulk-upload-s3-bucket',
23
+ 'lms-bulk-upload-sts-duration-seconds',
21
24
  ]);
22
25
  return {
23
26
  videoConversionEnabled: v['lms-video-conversion-enabled'] !== false,
@@ -25,6 +28,14 @@ export class LmsSettingController {
25
28
  transcriptionEnabled: v['lms-audio-transcription-enabled'] !== false,
26
29
  youtubeEnabled: v['lms-youtube-provider-enabled'] !== false,
27
30
  vimeoEnabled: v['lms-vimeo-provider-enabled'] !== false,
31
+ bulkUploadStorageProfileId:
32
+ Number(v['lms-bulk-upload-storage-profile-id'] || 0) || null,
33
+ bulkUploadBucketName:
34
+ typeof v['lms-bulk-upload-s3-bucket'] === 'string'
35
+ ? v['lms-bulk-upload-s3-bucket']
36
+ : '',
37
+ bulkUploadStsDurationSeconds:
38
+ Number(v['lms-bulk-upload-sts-duration-seconds'] || 3600) || 3600,
28
39
  };
29
40
  }
30
41
  }
@@ -67,7 +67,7 @@ export class EnterpriseService {
67
67
  take: pageSize,
68
68
  orderBy: { name: 'asc' },
69
69
  include: {
70
- person: { select: { id: true, name: true } },
70
+ person: { select: { id: true, name: true, avatar_id: true } },
71
71
  _count: {
72
72
  select: {
73
73
  enterprise_user: true,
@@ -928,7 +928,10 @@ export class InstructorService {
928
928
  status?: string;
929
929
  } = {},
930
930
  ) {
931
- const { page = 1, pageSize = 6, search, status } = options;
931
+ const page = Math.max(Number(options.page) || 1, 1);
932
+ const pageSize = Math.min(Math.max(Number(options.pageSize) || 6, 1), 100);
933
+ const skip = (page - 1) * pageSize;
934
+ const { search, status } = options;
932
935
 
933
936
  const where: Prisma.course_class_groupWhereInput = {
934
937
  instructor_id: instructorId,
@@ -971,7 +974,7 @@ export class InstructorService {
971
974
  },
972
975
  },
973
976
  },
974
- skip: (page - 1) * pageSize,
977
+ skip,
975
978
  take: pageSize,
976
979
  }),
977
980
  this.prisma.course_class_group.count({ where }),
@@ -999,6 +1002,12 @@ export class InstructorService {
999
1002
  };
1000
1003
  });
1001
1004
 
1002
- return { data, total, page, pageSize, lastPage: Math.ceil(total / pageSize) };
1005
+ return {
1006
+ data,
1007
+ total,
1008
+ page,
1009
+ pageSize,
1010
+ lastPage: Math.max(1, Math.ceil(total / pageSize)),
1011
+ };
1003
1012
  }
1004
1013
  }