@things-factory/integration-label-studio 9.1.19

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 (152) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/EXTERNAL_DATA_SOURCING.md +484 -0
  3. package/IMPLEMENTATION_GUIDE.md +469 -0
  4. package/INTEGRATION.md +279 -0
  5. package/README.md +1014 -0
  6. package/SETUP_GUIDE.md +577 -0
  7. package/TEST_GUIDE.md +387 -0
  8. package/UI_CUSTOMIZATION.md +395 -0
  9. package/USER_SYNC_GUIDE.md +514 -0
  10. package/client/bootstrap.ts +1 -0
  11. package/client/index.ts +1 -0
  12. package/client/label-studio-label-page.ts +52 -0
  13. package/client/label-studio-project-create.ts +216 -0
  14. package/client/label-studio-project-list.ts +214 -0
  15. package/client/label-studio-wrapper.ts +294 -0
  16. package/client/route.ts +15 -0
  17. package/client/tsconfig.json +13 -0
  18. package/config/config.development.js +124 -0
  19. package/config/config.production.js +182 -0
  20. package/dist-client/bootstrap.d.ts +1 -0
  21. package/dist-client/bootstrap.js +2 -0
  22. package/dist-client/bootstrap.js.map +1 -0
  23. package/dist-client/index.d.ts +1 -0
  24. package/dist-client/index.js +2 -0
  25. package/dist-client/index.js.map +1 -0
  26. package/dist-client/label-studio-label-page.d.ts +8 -0
  27. package/dist-client/label-studio-label-page.js +54 -0
  28. package/dist-client/label-studio-label-page.js.map +1 -0
  29. package/dist-client/label-studio-project-create.d.ts +16 -0
  30. package/dist-client/label-studio-project-create.js +235 -0
  31. package/dist-client/label-studio-project-create.js.map +1 -0
  32. package/dist-client/label-studio-project-list.d.ts +16 -0
  33. package/dist-client/label-studio-project-list.js +222 -0
  34. package/dist-client/label-studio-project-list.js.map +1 -0
  35. package/dist-client/label-studio-wrapper.d.ts +57 -0
  36. package/dist-client/label-studio-wrapper.js +304 -0
  37. package/dist-client/label-studio-wrapper.js.map +1 -0
  38. package/dist-client/route.d.ts +1 -0
  39. package/dist-client/route.js +14 -0
  40. package/dist-client/route.js.map +1 -0
  41. package/dist-client/tsconfig.tsbuildinfo +1 -0
  42. package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
  43. package/dist-server/controller/label-studio-role-mapper.js +65 -0
  44. package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
  45. package/dist-server/controller/user-provisioning-service.d.ts +66 -0
  46. package/dist-server/controller/user-provisioning-service.js +264 -0
  47. package/dist-server/controller/user-provisioning-service.js.map +1 -0
  48. package/dist-server/index.d.ts +7 -0
  49. package/dist-server/index.js +19 -0
  50. package/dist-server/index.js.map +1 -0
  51. package/dist-server/route/label-studio-sso.d.ts +2 -0
  52. package/dist-server/route/label-studio-sso.js +156 -0
  53. package/dist-server/route/label-studio-sso.js.map +1 -0
  54. package/dist-server/route/webhook.d.ts +65 -0
  55. package/dist-server/route/webhook.js +248 -0
  56. package/dist-server/route/webhook.js.map +1 -0
  57. package/dist-server/route.d.ts +1 -0
  58. package/dist-server/route.js +21 -0
  59. package/dist-server/route.js.map +1 -0
  60. package/dist-server/service/ai-prediction-service.d.ts +27 -0
  61. package/dist-server/service/ai-prediction-service.js +222 -0
  62. package/dist-server/service/ai-prediction-service.js.map +1 -0
  63. package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
  64. package/dist-server/service/dataset-labeling-integration.js +512 -0
  65. package/dist-server/service/dataset-labeling-integration.js.map +1 -0
  66. package/dist-server/service/external-data-source-service.d.ts +78 -0
  67. package/dist-server/service/external-data-source-service.js +415 -0
  68. package/dist-server/service/external-data-source-service.js.map +1 -0
  69. package/dist-server/service/index.d.ts +12 -0
  70. package/dist-server/service/index.js +27 -0
  71. package/dist-server/service/index.js.map +1 -0
  72. package/dist-server/service/label-studio-sso-service.d.ts +38 -0
  73. package/dist-server/service/label-studio-sso-service.js +98 -0
  74. package/dist-server/service/label-studio-sso-service.js.map +1 -0
  75. package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
  76. package/dist-server/service/ml/ml-backend-service.js +153 -0
  77. package/dist-server/service/ml/ml-backend-service.js.map +1 -0
  78. package/dist-server/service/prediction/prediction-management.d.ts +32 -0
  79. package/dist-server/service/prediction/prediction-management.js +299 -0
  80. package/dist-server/service/prediction/prediction-management.js.map +1 -0
  81. package/dist-server/service/project/project-management.d.ts +36 -0
  82. package/dist-server/service/project/project-management.js +309 -0
  83. package/dist-server/service/project/project-management.js.map +1 -0
  84. package/dist-server/service/task/task-management.d.ts +42 -0
  85. package/dist-server/service/task/task-management.js +372 -0
  86. package/dist-server/service/task/task-management.js.map +1 -0
  87. package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
  88. package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
  89. package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
  90. package/dist-server/service/webhook/webhook-management.d.ts +21 -0
  91. package/dist-server/service/webhook/webhook-management.js +134 -0
  92. package/dist-server/service/webhook/webhook-management.js.map +1 -0
  93. package/dist-server/tsconfig.tsbuildinfo +1 -0
  94. package/dist-server/types/dataset-labeling-types.d.ts +71 -0
  95. package/dist-server/types/dataset-labeling-types.js +259 -0
  96. package/dist-server/types/dataset-labeling-types.js.map +1 -0
  97. package/dist-server/types/label-studio-types.d.ts +128 -0
  98. package/dist-server/types/label-studio-types.js +494 -0
  99. package/dist-server/types/label-studio-types.js.map +1 -0
  100. package/dist-server/types/prediction-types.d.ts +39 -0
  101. package/dist-server/types/prediction-types.js +121 -0
  102. package/dist-server/types/prediction-types.js.map +1 -0
  103. package/dist-server/utils/annotation-exporter.d.ts +104 -0
  104. package/dist-server/utils/annotation-exporter.js +261 -0
  105. package/dist-server/utils/annotation-exporter.js.map +1 -0
  106. package/dist-server/utils/label-config-builder.d.ts +117 -0
  107. package/dist-server/utils/label-config-builder.js +286 -0
  108. package/dist-server/utils/label-config-builder.js.map +1 -0
  109. package/dist-server/utils/label-studio-api-client.d.ts +180 -0
  110. package/dist-server/utils/label-studio-api-client.js +401 -0
  111. package/dist-server/utils/label-studio-api-client.js.map +1 -0
  112. package/dist-server/utils/media-url-extractor.d.ts +45 -0
  113. package/dist-server/utils/media-url-extractor.js +152 -0
  114. package/dist-server/utils/media-url-extractor.js.map +1 -0
  115. package/dist-server/utils/task-transformer.d.ts +108 -0
  116. package/dist-server/utils/task-transformer.js +260 -0
  117. package/dist-server/utils/task-transformer.js.map +1 -0
  118. package/package.json +47 -0
  119. package/server/SERVER_STRUCTURE.md +351 -0
  120. package/server/controller/label-studio-role-mapper.ts +76 -0
  121. package/server/controller/user-provisioning-service.ts +340 -0
  122. package/server/index.ts +19 -0
  123. package/server/route/label-studio-sso.ts +194 -0
  124. package/server/route/webhook.ts +304 -0
  125. package/server/route.ts +35 -0
  126. package/server/service/ai-prediction-service.ts +239 -0
  127. package/server/service/dataset-labeling-integration.ts +590 -0
  128. package/server/service/external-data-source-service.ts +438 -0
  129. package/server/service/index.ts +24 -0
  130. package/server/service/label-studio-sso-service.ts +108 -0
  131. package/server/service/labeling-scenario-service.ts.deprecated +566 -0
  132. package/server/service/ml/ml-backend-service.ts +127 -0
  133. package/server/service/prediction/prediction-management.ts +281 -0
  134. package/server/service/project/project-management.ts +284 -0
  135. package/server/service/task/task-management.ts +363 -0
  136. package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
  137. package/server/service/webhook/webhook-management.ts +109 -0
  138. package/server/tsconfig.json +11 -0
  139. package/server/types/dataset-labeling-types.ts +181 -0
  140. package/server/types/global.d.ts +23 -0
  141. package/server/types/label-studio-types.ts +346 -0
  142. package/server/types/prediction-types.ts +86 -0
  143. package/server/types/scenario-types.ts.deprecated +362 -0
  144. package/server/utils/annotation-exporter.ts +340 -0
  145. package/server/utils/label-config-builder.ts +340 -0
  146. package/server/utils/label-studio-api-client.ts +487 -0
  147. package/server/utils/media-url-extractor.ts +193 -0
  148. package/server/utils/task-transformer.ts +342 -0
  149. package/test-ai-prediction.js +268 -0
  150. package/test-dataset-integration.js +449 -0
  151. package/test-simple.js +89 -0
  152. package/things-factory.config.js +12 -0
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UserProvisioningService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const axios_1 = tslib_1.__importDefault(require("axios"));
6
+ const auth_base_1 = require("@things-factory/auth-base");
7
+ const env_1 = require("@things-factory/env");
8
+ const label_studio_role_mapper_js_1 = require("./label-studio-role-mapper.js");
9
+ const shell_1 = require("@things-factory/shell");
10
+ // Get Label Studio config from server config file
11
+ function getLabelStudioConfig() {
12
+ return env_1.config.get('labelStudio', {
13
+ serverUrl: '',
14
+ apiToken: '',
15
+ interfaces: 'panel,controls,annotations:menu'
16
+ });
17
+ }
18
+ /**
19
+ * Label Studio 사용자 프로비저닝 서비스
20
+ *
21
+ * Things-Factory 사용자를 Label Studio에 배치 동기화합니다.
22
+ */
23
+ class UserProvisioningService {
24
+ /**
25
+ * 설정 검증
26
+ */
27
+ static validateConfig() {
28
+ const config = getLabelStudioConfig();
29
+ if (!config.apiToken) {
30
+ throw new Error('Label Studio API token is not configured');
31
+ }
32
+ if (!config.serverUrl) {
33
+ throw new Error('Label Studio server URL is not configured');
34
+ }
35
+ }
36
+ /**
37
+ * 단일 사용자 동기화
38
+ *
39
+ * @param domain Things-Factory 도메인
40
+ * @param user Things-Factory 사용자
41
+ * @returns 동기화 결과
42
+ */
43
+ static async syncUser(domain, user) {
44
+ // 설정 검증
45
+ this.validateConfig();
46
+ const config = getLabelStudioConfig();
47
+ try {
48
+ // 1. Label Studio 권한 확인
49
+ const hasLSPrivilege = (await auth_base_1.User.hasPrivilege('label-studio', 'query', domain, user)) ||
50
+ (await auth_base_1.User.hasPrivilege('label-studio', 'mutation', domain, user));
51
+ if (!hasLSPrivilege) {
52
+ // Label Studio 권한 없음 → Label Studio에서 비활성화
53
+ const deactivated = await this.deactivateUser(user.email, config);
54
+ return {
55
+ success: true,
56
+ email: user.email,
57
+ action: deactivated ? 'deactivated' : 'skipped'
58
+ };
59
+ }
60
+ // 2. Label Studio 권한 매핑
61
+ const lsPermissions = await label_studio_role_mapper_js_1.LabelStudioRoleMapper.mapUserPermissions(domain, user);
62
+ // 3. Label Studio API로 사용자 생성 또는 업데이트
63
+ const result = await this.createOrUpdateLabelStudioUser(user, lsPermissions, config);
64
+ return {
65
+ success: true,
66
+ email: user.email,
67
+ action: result.created ? 'created' : 'updated',
68
+ lsUserId: result.id.toString(),
69
+ lsPermissions: label_studio_role_mapper_js_1.LabelStudioRoleMapper.getPermissionsDescription(lsPermissions)
70
+ };
71
+ }
72
+ catch (error) {
73
+ console.error(`Failed to sync user ${user.email}:`, error.message);
74
+ return {
75
+ success: false,
76
+ email: user.email,
77
+ action: 'error',
78
+ error: error.message
79
+ };
80
+ }
81
+ }
82
+ static async getDomainUsers(domain) {
83
+ const qb = (0, shell_1.getRepository)(auth_base_1.User).createQueryBuilder('USER');
84
+ qb.select().andWhere(qb => {
85
+ const subQuery = qb
86
+ .subQuery()
87
+ .select('USERS_DOMAINS.users_id')
88
+ .from('users_domains', 'USERS_DOMAINS')
89
+ .where('USERS_DOMAINS.domains_id = :domainId', { domainId: domain.id })
90
+ .getQuery();
91
+ return 'USER.id IN ' + subQuery;
92
+ });
93
+ const [items, total] = await qb.getManyAndCount();
94
+ const foundUsers = items.map((item) => {
95
+ item.owner = item.id === domain.owner;
96
+ return item;
97
+ });
98
+ return foundUsers;
99
+ }
100
+ /**
101
+ * 도메인의 모든 사용자 일괄 동기화
102
+ *
103
+ * @param domain Things-Factory 도메인
104
+ * @returns 동기화 요약
105
+ */
106
+ static async syncAllUsers(domain) {
107
+ // 설정 검증
108
+ this.validateConfig();
109
+ // 도메인의 모든 활성 사용자 조회
110
+ const users = await UserProvisioningService.getDomainUsers(domain);
111
+ console.log(`🔄 Starting batch sync for ${users.length} users...`);
112
+ const results = [];
113
+ // 각 사용자 동기화
114
+ for (const user of users) {
115
+ const result = await this.syncUser(domain, user);
116
+ results.push(result);
117
+ // API Rate Limiting 방지
118
+ await this.sleep(100);
119
+ }
120
+ // 요약 생성
121
+ const summary = {
122
+ total: users.length,
123
+ created: results.filter(r => r.action === 'created').length,
124
+ updated: results.filter(r => r.action === 'updated').length,
125
+ deactivated: results.filter(r => r.action === 'deactivated').length,
126
+ skipped: results.filter(r => r.action === 'skipped').length,
127
+ errors: results.filter(r => r.action === 'error').length,
128
+ results
129
+ };
130
+ console.log(`✅ Batch sync completed:`, {
131
+ total: summary.total,
132
+ created: summary.created,
133
+ updated: summary.updated,
134
+ deactivated: summary.deactivated,
135
+ skipped: summary.skipped,
136
+ errors: summary.errors
137
+ });
138
+ return summary;
139
+ }
140
+ /**
141
+ * Label Studio API를 통해 사용자 생성 또는 업데이트
142
+ */
143
+ static async createOrUpdateLabelStudioUser(user, lsPermissions, config) {
144
+ const apiUrl = this.buildApiUrl(config.serverUrl, '/api/users');
145
+ // 이름 파싱
146
+ const nameParts = (user.name || user.email).split(' ');
147
+ const firstName = nameParts[0] || user.email;
148
+ const lastName = nameParts.slice(1).join(' ') || '';
149
+ try {
150
+ // 이메일로 기존 사용자 조회
151
+ const searchResponse = await axios_1.default.get(apiUrl, {
152
+ headers: {
153
+ Authorization: `Token ${config.apiToken}`
154
+ },
155
+ params: {
156
+ email: user.email
157
+ }
158
+ });
159
+ if (searchResponse.data.results && searchResponse.data.results.length > 0) {
160
+ // 기존 사용자 업데이트
161
+ const existingUser = searchResponse.data.results[0];
162
+ const updateResponse = await axios_1.default.patch(`${apiUrl}/${existingUser.id}/`, {
163
+ email: user.email,
164
+ username: user.email,
165
+ first_name: firstName,
166
+ last_name: lastName,
167
+ is_superuser: lsPermissions.is_superuser,
168
+ is_staff: lsPermissions.is_staff,
169
+ is_active: lsPermissions.is_active
170
+ }, {
171
+ headers: {
172
+ Authorization: `Token ${config.apiToken}`,
173
+ 'Content-Type': 'application/json'
174
+ }
175
+ });
176
+ return {
177
+ ...updateResponse.data,
178
+ created: false
179
+ };
180
+ }
181
+ else {
182
+ // 새 사용자 생성
183
+ const createResponse = await axios_1.default.post(apiUrl, {
184
+ email: user.email,
185
+ username: user.email,
186
+ first_name: firstName,
187
+ last_name: lastName,
188
+ password: this.generateRandomPassword(),
189
+ is_superuser: lsPermissions.is_superuser,
190
+ is_staff: lsPermissions.is_staff,
191
+ is_active: lsPermissions.is_active
192
+ }, {
193
+ headers: {
194
+ Authorization: `Token ${config.apiToken}`,
195
+ 'Content-Type': 'application/json'
196
+ }
197
+ });
198
+ return {
199
+ ...createResponse.data,
200
+ created: true
201
+ };
202
+ }
203
+ }
204
+ catch (error) {
205
+ console.error(`Label Studio API error for ${user.email}:`, error.response?.data || error.message);
206
+ throw error;
207
+ }
208
+ }
209
+ /**
210
+ * Label Studio에서 사용자 비활성화
211
+ */
212
+ static async deactivateUser(email, config) {
213
+ const apiUrl = this.buildApiUrl(config.serverUrl, '/api/users');
214
+ try {
215
+ // 이메일로 사용자 조회
216
+ const searchResponse = await axios_1.default.get(apiUrl, {
217
+ headers: {
218
+ Authorization: `Token ${config.apiToken}`
219
+ },
220
+ params: { email }
221
+ });
222
+ if (searchResponse.data.results && searchResponse.data.results.length > 0) {
223
+ const user = searchResponse.data.results[0];
224
+ // 비활성화
225
+ await axios_1.default.patch(`${apiUrl}/${user.id}/`, { is_active: false }, {
226
+ headers: {
227
+ Authorization: `Token ${config.apiToken}`,
228
+ 'Content-Type': 'application/json'
229
+ }
230
+ });
231
+ return true;
232
+ }
233
+ return false;
234
+ }
235
+ catch (error) {
236
+ console.error(`Failed to deactivate user ${email}:`, error.message);
237
+ return false;
238
+ }
239
+ }
240
+ /**
241
+ * API URL 빌드
242
+ */
243
+ static buildApiUrl(serverUrl, path) {
244
+ let url = serverUrl.replace(/\/$/, '');
245
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
246
+ url = `https://${url}`;
247
+ }
248
+ return `${url}${path}`;
249
+ }
250
+ /**
251
+ * 랜덤 비밀번호 생성 (SSO 사용으로 실제로는 사용 안 됨)
252
+ */
253
+ static generateRandomPassword() {
254
+ return Math.random().toString(36).slice(-16) + Math.random().toString(36).slice(-16);
255
+ }
256
+ /**
257
+ * Sleep 유틸리티
258
+ */
259
+ static sleep(ms) {
260
+ return new Promise(resolve => setTimeout(resolve, ms));
261
+ }
262
+ }
263
+ exports.UserProvisioningService = UserProvisioningService;
264
+ //# sourceMappingURL=user-provisioning-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-provisioning-service.js","sourceRoot":"","sources":["../../server/controller/user-provisioning-service.ts"],"names":[],"mappings":"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","sourcesContent":["import axios from 'axios'\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\nimport { LabelStudioRoleMapper, LabelStudioPermissions } from './label-studio-role-mapper.js'\nimport { Domain, getRepository } from '@things-factory/shell'\n\ntype LabelStudioConfig = {\n serverUrl: string\n apiToken: string\n interfaces: string\n}\n\n// Get Label Studio config from server config file\nfunction getLabelStudioConfig(): LabelStudioConfig {\n return config.get('labelStudio', {\n serverUrl: '',\n apiToken: '',\n interfaces: 'panel,controls,annotations:menu'\n })\n}\n\nexport interface SyncResult {\n success: boolean\n email: string\n action: 'created' | 'updated' | 'deactivated' | 'skipped' | 'error'\n lsUserId?: string\n lsPermissions?: string // 'Admin (Full access)' | 'Staff (Labeling only)' | 'Inactive'\n error?: string\n}\n\nexport interface SyncSummary {\n total: number\n created: number\n updated: number\n deactivated: number\n skipped: number\n errors: number\n results: SyncResult[]\n}\n\n/**\n * Label Studio 사용자 프로비저닝 서비스\n *\n * Things-Factory 사용자를 Label Studio에 배치 동기화합니다.\n */\nexport class UserProvisioningService {\n /**\n * 설정 검증\n */\n private static validateConfig(): void {\n const config = getLabelStudioConfig()\n\n if (!config.apiToken) {\n throw new Error('Label Studio API token is not configured')\n }\n\n if (!config.serverUrl) {\n throw new Error('Label Studio server URL is not configured')\n }\n }\n\n /**\n * 단일 사용자 동기화\n *\n * @param domain Things-Factory 도메인\n * @param user Things-Factory 사용자\n * @returns 동기화 결과\n */\n static async syncUser(domain: Domain, user: User): Promise<SyncResult> {\n // 설정 검증\n this.validateConfig()\n\n const config = getLabelStudioConfig()\n try {\n // 1. Label Studio 권한 확인\n const hasLSPrivilege =\n (await User.hasPrivilege('label-studio', 'query', domain, user)) ||\n (await User.hasPrivilege('label-studio', 'mutation', domain, user))\n\n if (!hasLSPrivilege) {\n // Label Studio 권한 없음 → Label Studio에서 비활성화\n const deactivated = await this.deactivateUser(user.email, config)\n\n return {\n success: true,\n email: user.email,\n action: deactivated ? 'deactivated' : 'skipped'\n }\n }\n\n // 2. Label Studio 권한 매핑\n const lsPermissions = await LabelStudioRoleMapper.mapUserPermissions(domain, user)\n\n // 3. Label Studio API로 사용자 생성 또는 업데이트\n const result = await this.createOrUpdateLabelStudioUser(user, lsPermissions, config)\n\n return {\n success: true,\n email: user.email,\n action: result.created ? 'created' : 'updated',\n lsUserId: result.id.toString(),\n lsPermissions: LabelStudioRoleMapper.getPermissionsDescription(lsPermissions)\n }\n } catch (error: any) {\n console.error(`Failed to sync user ${user.email}:`, error.message)\n\n return {\n success: false,\n email: user.email,\n action: 'error',\n error: error.message\n }\n }\n }\n\n static async getDomainUsers(domain: Domain): Promise<User[]> {\n const qb = getRepository(User).createQueryBuilder('USER')\n qb.select().andWhere(qb => {\n const subQuery = qb\n .subQuery()\n .select('USERS_DOMAINS.users_id')\n .from('users_domains', 'USERS_DOMAINS')\n .where('USERS_DOMAINS.domains_id = :domainId', { domainId: domain.id })\n .getQuery()\n\n return 'USER.id IN ' + subQuery\n })\n\n const [items, total] = await qb.getManyAndCount()\n\n const foundUsers: User[] = items.map((item: User) => {\n item.owner = item.id === domain.owner\n return item\n })\n\n return foundUsers\n }\n\n /**\n * 도메인의 모든 사용자 일괄 동기화\n *\n * @param domain Things-Factory 도메인\n * @returns 동기화 요약\n */\n static async syncAllUsers(domain: Domain): Promise<SyncSummary> {\n // 설정 검증\n this.validateConfig()\n\n // 도메인의 모든 활성 사용자 조회\n const users = await UserProvisioningService.getDomainUsers(domain)\n\n console.log(`🔄 Starting batch sync for ${users.length} users...`)\n\n const results: SyncResult[] = []\n\n // 각 사용자 동기화\n for (const user of users) {\n const result = await this.syncUser(domain, user)\n results.push(result)\n\n // API Rate Limiting 방지\n await this.sleep(100)\n }\n\n // 요약 생성\n const summary: SyncSummary = {\n total: users.length,\n created: results.filter(r => r.action === 'created').length,\n updated: results.filter(r => r.action === 'updated').length,\n deactivated: results.filter(r => r.action === 'deactivated').length,\n skipped: results.filter(r => r.action === 'skipped').length,\n errors: results.filter(r => r.action === 'error').length,\n results\n }\n\n console.log(`✅ Batch sync completed:`, {\n total: summary.total,\n created: summary.created,\n updated: summary.updated,\n deactivated: summary.deactivated,\n skipped: summary.skipped,\n errors: summary.errors\n })\n\n return summary\n }\n\n /**\n * Label Studio API를 통해 사용자 생성 또는 업데이트\n */\n private static async createOrUpdateLabelStudioUser(\n user: User,\n lsPermissions: LabelStudioPermissions,\n config: LabelStudioConfig\n ): Promise<any> {\n const apiUrl = this.buildApiUrl(config.serverUrl, '/api/users')\n\n // 이름 파싱\n const nameParts = (user.name || user.email).split(' ')\n const firstName = nameParts[0] || user.email\n const lastName = nameParts.slice(1).join(' ') || ''\n\n try {\n // 이메일로 기존 사용자 조회\n const searchResponse = await axios.get(apiUrl, {\n headers: {\n Authorization: `Token ${config.apiToken}`\n },\n params: {\n email: user.email\n }\n })\n\n if (searchResponse.data.results && searchResponse.data.results.length > 0) {\n // 기존 사용자 업데이트\n const existingUser = searchResponse.data.results[0]\n\n const updateResponse = await axios.patch(\n `${apiUrl}/${existingUser.id}/`,\n {\n email: user.email,\n username: user.email,\n first_name: firstName,\n last_name: lastName,\n is_superuser: lsPermissions.is_superuser,\n is_staff: lsPermissions.is_staff,\n is_active: lsPermissions.is_active\n },\n {\n headers: {\n Authorization: `Token ${config.apiToken}`,\n 'Content-Type': 'application/json'\n }\n }\n )\n\n return {\n ...updateResponse.data,\n created: false\n }\n } else {\n // 새 사용자 생성\n const createResponse = await axios.post(\n apiUrl,\n {\n email: user.email,\n username: user.email,\n first_name: firstName,\n last_name: lastName,\n password: this.generateRandomPassword(),\n is_superuser: lsPermissions.is_superuser,\n is_staff: lsPermissions.is_staff,\n is_active: lsPermissions.is_active\n },\n {\n headers: {\n Authorization: `Token ${config.apiToken}`,\n 'Content-Type': 'application/json'\n }\n }\n )\n\n return {\n ...createResponse.data,\n created: true\n }\n }\n } catch (error: any) {\n console.error(`Label Studio API error for ${user.email}:`, error.response?.data || error.message)\n throw error\n }\n }\n\n /**\n * Label Studio에서 사용자 비활성화\n */\n private static async deactivateUser(email: string, config: LabelStudioConfig): Promise<boolean> {\n const apiUrl = this.buildApiUrl(config.serverUrl, '/api/users')\n\n try {\n // 이메일로 사용자 조회\n const searchResponse = await axios.get(apiUrl, {\n headers: {\n Authorization: `Token ${config.apiToken}`\n },\n params: { email }\n })\n\n if (searchResponse.data.results && searchResponse.data.results.length > 0) {\n const user = searchResponse.data.results[0]\n\n // 비활성화\n await axios.patch(\n `${apiUrl}/${user.id}/`,\n { is_active: false },\n {\n headers: {\n Authorization: `Token ${config.apiToken}`,\n 'Content-Type': 'application/json'\n }\n }\n )\n\n return true\n }\n\n return false\n } catch (error: any) {\n console.error(`Failed to deactivate user ${email}:`, error.message)\n return false\n }\n }\n\n /**\n * API URL 빌드\n */\n private static buildApiUrl(serverUrl: string, path: string): string {\n let url = serverUrl.replace(/\\/$/, '')\n\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\n url = `https://${url}`\n }\n\n return `${url}${path}`\n }\n\n /**\n * 랜덤 비밀번호 생성 (SSO 사용으로 실제로는 사용 안 됨)\n */\n private static generateRandomPassword(): string {\n return Math.random().toString(36).slice(-16) + Math.random().toString(36).slice(-16)\n }\n\n /**\n * Sleep 유틸리티\n */\n private static sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n }\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export * from './service/index.js';
2
+ export * from './utils/label-config-builder.js';
3
+ export * from './utils/task-transformer.js';
4
+ export * from './utils/annotation-exporter.js';
5
+ export { WebhookAction, WebhookPayload, WebhookHandler, registerWebhookHandler, unregisterWebhookHandler, clearWebhookHandlers } from './route/webhook.js';
6
+ import './route.js';
7
+ import './route/webhook.js';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clearWebhookHandlers = exports.unregisterWebhookHandler = exports.registerWebhookHandler = exports.WebhookAction = void 0;
4
+ const tslib_1 = require("tslib");
5
+ tslib_1.__exportStar(require("./service/index.js"), exports);
6
+ tslib_1.__exportStar(require("./utils/label-config-builder.js"), exports);
7
+ tslib_1.__exportStar(require("./utils/task-transformer.js"), exports);
8
+ tslib_1.__exportStar(require("./utils/annotation-exporter.js"), exports);
9
+ var webhook_js_1 = require("./route/webhook.js");
10
+ Object.defineProperty(exports, "WebhookAction", { enumerable: true, get: function () { return webhook_js_1.WebhookAction; } });
11
+ Object.defineProperty(exports, "registerWebhookHandler", { enumerable: true, get: function () { return webhook_js_1.registerWebhookHandler; } });
12
+ Object.defineProperty(exports, "unregisterWebhookHandler", { enumerable: true, get: function () { return webhook_js_1.unregisterWebhookHandler; } });
13
+ Object.defineProperty(exports, "clearWebhookHandlers", { enumerable: true, get: function () { return webhook_js_1.clearWebhookHandlers; } });
14
+ require("./route.js");
15
+ require("./route/webhook.js");
16
+ process.on('bootstrap-module-start', async ({ app, config, client }) => {
17
+ console.log('[integration-label-studio:bootstrap] Label Studio integration initialized with subdomain cookie-sharing SSO');
18
+ });
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA,6DAAkC;AAClC,0EAA+C;AAC/C,sEAA2C;AAC3C,yEAA8C;AAC9C,iDAO2B;AANzB,2GAAA,aAAa,OAAA;AAGb,oHAAA,sBAAsB,OAAA;AACtB,sHAAA,wBAAwB,OAAA;AACxB,kHAAA,oBAAoB,OAAA;AAGtB,sBAAmB;AACnB,8BAA2B;AAE3B,OAAO,CAAC,EAAE,CAAC,wBAA+B,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAO,EAAE,EAAE;IACjF,OAAO,CAAC,GAAG,CAAC,6GAA6G,CAAC,CAAA;AAC5H,CAAC,CAAC,CAAA","sourcesContent":["export * from './service/index.js'\nexport * from './utils/label-config-builder.js'\nexport * from './utils/task-transformer.js'\nexport * from './utils/annotation-exporter.js'\nexport {\n WebhookAction,\n WebhookPayload,\n WebhookHandler,\n registerWebhookHandler,\n unregisterWebhookHandler,\n clearWebhookHandlers\n} from './route/webhook.js'\n\nimport './route.js'\nimport './route/webhook.js'\n\nprocess.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {\n console.log('[integration-label-studio:bootstrap] Label Studio integration initialized with subdomain cookie-sharing SSO')\n})\n"]}
@@ -0,0 +1,2 @@
1
+ declare const ssoRouter: any;
2
+ export { ssoRouter };
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ssoRouter = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
6
+ const env_1 = require("@things-factory/env");
7
+ const label_studio_sso_service_js_1 = require("../service/label-studio-sso-service.js");
8
+ const ssoRouter = new koa_router_1.default();
9
+ exports.ssoRouter = ssoRouter;
10
+ /**
11
+ * Get Label Studio configuration
12
+ */
13
+ function getLabelStudioConfig() {
14
+ const labelStudioConfig = env_1.config.get('labelStudio', {
15
+ serverUrl: 'http://localhost:8080',
16
+ apiToken: '',
17
+ cookieDomain: '' // e.g., '.nubison.localhost' for subdomain sharing
18
+ });
19
+ return {
20
+ serverUrl: labelStudioConfig.serverUrl,
21
+ apiToken: labelStudioConfig.apiToken,
22
+ cookieDomain: labelStudioConfig.cookieDomain || ''
23
+ };
24
+ }
25
+ /**
26
+ * SSO Setup Endpoint
27
+ *
28
+ * This endpoint must be called by the client before loading Label Studio iframe
29
+ * to establish SSO authentication using subdomain cookie sharing.
30
+ *
31
+ * Flow:
32
+ * 1. Client calls /label-studio/sso/setup
33
+ * 2. Backend requests JWT token from Label Studio
34
+ * 3. Backend sets ls_auth_token cookie with shared domain
35
+ * 4. Client loads Label Studio iframe - auto-login succeeds
36
+ *
37
+ * @example
38
+ * fetch('/label-studio/sso/setup', { credentials: 'include' })
39
+ */
40
+ ssoRouter.get('/label-studio/sso/setup', async (ctx) => {
41
+ try {
42
+ const user = ctx.state.user;
43
+ if (!user || !user.email) {
44
+ ctx.status = 401;
45
+ ctx.body = {
46
+ success: false,
47
+ error: 'Unauthorized',
48
+ message: 'User authentication required'
49
+ };
50
+ return;
51
+ }
52
+ const { cookieDomain } = getLabelStudioConfig();
53
+ // Cookie name must match Label Studio's JWT_SSO_COOKIE_NAME setting
54
+ const cookieName = 'ls_auth_token';
55
+ const existingToken = ctx.cookies.get(cookieName);
56
+ if (existingToken) {
57
+ // Token already exists
58
+ ctx.status = 200;
59
+ ctx.body = {
60
+ success: true,
61
+ message: 'SSO token already exists',
62
+ user: user.email
63
+ };
64
+ return;
65
+ }
66
+ console.log(`[Label Studio SSO] Setting up token for ${user.email}`);
67
+ // Request JWT token from Label Studio
68
+ const tokenData = await label_studio_sso_service_js_1.LabelStudioSSOService.getSSOToken(user.email);
69
+ if (tokenData) {
70
+ // Clear existing sessionid to prevent conflict with SSO token
71
+ ctx.cookies.set('sessionid', '', {
72
+ domain: cookieDomain || undefined,
73
+ path: '/',
74
+ maxAge: 0 // Expire immediately
75
+ });
76
+ // Set cookie with shared domain for subdomain access
77
+ const cookieOptions = {
78
+ httpOnly: false, // Allow client-side access for debugging
79
+ secure: ctx.protocol === 'https',
80
+ sameSite: 'lax',
81
+ path: '/',
82
+ maxAge: tokenData.expires_in * 1000 // Convert seconds to milliseconds
83
+ };
84
+ // Only set domain if cookieDomain is configured
85
+ // This allows same-origin cookie for single domain setup
86
+ if (cookieDomain) {
87
+ cookieOptions.domain = cookieDomain;
88
+ console.log(`[Label Studio SSO] Using shared cookie domain: ${cookieDomain}`);
89
+ }
90
+ ctx.cookies.set(cookieName, tokenData.token, cookieOptions);
91
+ console.log(`[Label Studio SSO] Token set for ${user.email} (expires in ${tokenData.expires_in}s, domain: ${cookieDomain || 'same-origin'})`);
92
+ ctx.status = 200;
93
+ ctx.body = {
94
+ success: true,
95
+ message: 'SSO token setup complete',
96
+ user: user.email,
97
+ expiresIn: tokenData.expires_in,
98
+ cookieDomain: cookieDomain || 'same-origin'
99
+ };
100
+ }
101
+ else {
102
+ console.error(`[Label Studio SSO] Failed to acquire token for ${user.email}`);
103
+ ctx.status = 500;
104
+ ctx.body = {
105
+ success: false,
106
+ error: 'Token Acquisition Failed',
107
+ message: 'Failed to acquire SSO token from Label Studio'
108
+ };
109
+ }
110
+ }
111
+ catch (error) {
112
+ console.error('[Label Studio SSO] Setup error:', error.message);
113
+ ctx.status = 500;
114
+ ctx.body = {
115
+ success: false,
116
+ error: 'Internal Server Error',
117
+ message: error.message
118
+ };
119
+ }
120
+ });
121
+ /**
122
+ * Health check endpoint
123
+ */
124
+ ssoRouter.get('/label-studio/sso/health', async (ctx) => {
125
+ try {
126
+ const { serverUrl, cookieDomain } = getLabelStudioConfig();
127
+ ctx.status = 200;
128
+ ctx.body = {
129
+ status: 'ok',
130
+ labelStudioUrl: serverUrl || 'not configured',
131
+ cookieDomain: cookieDomain || 'same-origin',
132
+ message: 'Label Studio SSO is running'
133
+ };
134
+ }
135
+ catch (error) {
136
+ ctx.status = 503;
137
+ ctx.body = {
138
+ status: 'error',
139
+ message: error.message
140
+ };
141
+ }
142
+ });
143
+ /**
144
+ * Configuration endpoint
145
+ */
146
+ ssoRouter.get('/label-studio/sso/config', async (ctx) => {
147
+ const { serverUrl, apiToken, cookieDomain } = getLabelStudioConfig();
148
+ ctx.status = 200;
149
+ ctx.body = {
150
+ labelStudioUrl: serverUrl || 'not configured',
151
+ cookieDomain: cookieDomain || 'same-origin',
152
+ hasApiToken: !!apiToken,
153
+ ssoConfigured: label_studio_sso_service_js_1.LabelStudioSSOService.verifyConfig()
154
+ };
155
+ });
156
+ //# sourceMappingURL=label-studio-sso.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-studio-sso.js","sourceRoot":"","sources":["../../server/route/label-studio-sso.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Label Studio SSO Route\n *\n * Implements subdomain-based cookie sharing for Label Studio SSO authentication.\n * This approach eliminates the need for proxy by using shared domain cookies.\n *\n * ## Architecture:\n * 1. Client calls /label-studio/sso/setup\n * 2. Backend gets JWT token from Label Studio using API Token\n * 3. Backend sets cookie with shared domain (e.g., .nubison.localhost)\n * 4. Client loads Label Studio directly in iframe\n * 5. Cookie is automatically sent with iframe requests\n *\n * ## Configuration:\n * Label Studio .env must include:\n * - JWT_SSO_SECRET: Shared secret for JWT signing\n * - JWT_SSO_COOKIE_NAME: Cookie name (default: ls_auth_token)\n * - CSRF_TRUSTED_ORIGINS: Include all frontend domains\n * - ALLOWED_HOSTS: Include all subdomain patterns\n */\nimport Koa from 'koa'\nimport Router from 'koa-router'\nimport { config } from '@things-factory/env'\nimport { LabelStudioSSOService } from '../service/label-studio-sso-service.js'\n\nconst ssoRouter = new Router()\n\n/**\n * Get Label Studio configuration\n */\nfunction getLabelStudioConfig() {\n const labelStudioConfig = config.get('labelStudio', {\n serverUrl: 'http://localhost:8080',\n apiToken: '',\n cookieDomain: '' // e.g., '.nubison.localhost' for subdomain sharing\n })\n\n return {\n serverUrl: labelStudioConfig.serverUrl,\n apiToken: labelStudioConfig.apiToken,\n cookieDomain: labelStudioConfig.cookieDomain || ''\n }\n}\n\n/**\n * SSO Setup Endpoint\n *\n * This endpoint must be called by the client before loading Label Studio iframe\n * to establish SSO authentication using subdomain cookie sharing.\n *\n * Flow:\n * 1. Client calls /label-studio/sso/setup\n * 2. Backend requests JWT token from Label Studio\n * 3. Backend sets ls_auth_token cookie with shared domain\n * 4. Client loads Label Studio iframe - auto-login succeeds\n *\n * @example\n * fetch('/label-studio/sso/setup', { credentials: 'include' })\n */\nssoRouter.get('/label-studio/sso/setup', async (ctx: Koa.Context) => {\n try {\n const user = ctx.state.user\n\n if (!user || !user.email) {\n ctx.status = 401\n ctx.body = {\n success: false,\n error: 'Unauthorized',\n message: 'User authentication required'\n }\n return\n }\n\n const { cookieDomain } = getLabelStudioConfig()\n\n // Cookie name must match Label Studio's JWT_SSO_COOKIE_NAME setting\n const cookieName = 'ls_auth_token'\n const existingToken = ctx.cookies.get(cookieName)\n\n if (existingToken) {\n // Token already exists\n ctx.status = 200\n ctx.body = {\n success: true,\n message: 'SSO token already exists',\n user: user.email\n }\n return\n }\n\n console.log(`[Label Studio SSO] Setting up token for ${user.email}`)\n\n // Request JWT token from Label Studio\n const tokenData = await LabelStudioSSOService.getSSOToken(user.email)\n\n if (tokenData) {\n // Clear existing sessionid to prevent conflict with SSO token\n ctx.cookies.set('sessionid', '', {\n domain: cookieDomain || undefined,\n path: '/',\n maxAge: 0 // Expire immediately\n })\n\n // Set cookie with shared domain for subdomain access\n const cookieOptions: any = {\n httpOnly: false, // Allow client-side access for debugging\n secure: ctx.protocol === 'https',\n sameSite: 'lax',\n path: '/',\n maxAge: tokenData.expires_in * 1000 // Convert seconds to milliseconds\n }\n\n // Only set domain if cookieDomain is configured\n // This allows same-origin cookie for single domain setup\n if (cookieDomain) {\n cookieOptions.domain = cookieDomain\n console.log(`[Label Studio SSO] Using shared cookie domain: ${cookieDomain}`)\n }\n\n ctx.cookies.set(cookieName, tokenData.token, cookieOptions)\n\n console.log(\n `[Label Studio SSO] Token set for ${user.email} (expires in ${tokenData.expires_in}s, domain: ${cookieDomain || 'same-origin'})`\n )\n\n ctx.status = 200\n ctx.body = {\n success: true,\n message: 'SSO token setup complete',\n user: user.email,\n expiresIn: tokenData.expires_in,\n cookieDomain: cookieDomain || 'same-origin'\n }\n } else {\n console.error(`[Label Studio SSO] Failed to acquire token for ${user.email}`)\n\n ctx.status = 500\n ctx.body = {\n success: false,\n error: 'Token Acquisition Failed',\n message: 'Failed to acquire SSO token from Label Studio'\n }\n }\n } catch (error: any) {\n console.error('[Label Studio SSO] Setup error:', error.message)\n\n ctx.status = 500\n ctx.body = {\n success: false,\n error: 'Internal Server Error',\n message: error.message\n }\n }\n})\n\n/**\n * Health check endpoint\n */\nssoRouter.get('/label-studio/sso/health', async (ctx: Koa.Context) => {\n try {\n const { serverUrl, cookieDomain } = getLabelStudioConfig()\n\n ctx.status = 200\n ctx.body = {\n status: 'ok',\n labelStudioUrl: serverUrl || 'not configured',\n cookieDomain: cookieDomain || 'same-origin',\n message: 'Label Studio SSO is running'\n }\n } catch (error: any) {\n ctx.status = 503\n ctx.body = {\n status: 'error',\n message: error.message\n }\n }\n})\n\n/**\n * Configuration endpoint\n */\nssoRouter.get('/label-studio/sso/config', async (ctx: Koa.Context) => {\n const { serverUrl, apiToken, cookieDomain } = getLabelStudioConfig()\n\n ctx.status = 200\n ctx.body = {\n labelStudioUrl: serverUrl || 'not configured',\n cookieDomain: cookieDomain || 'same-origin',\n hasApiToken: !!apiToken,\n ssoConfigured: LabelStudioSSOService.verifyConfig()\n }\n})\n\nexport { ssoRouter }\n"]}
@@ -0,0 +1,65 @@
1
+ import Koa from 'koa';
2
+ declare const webhookRouter: any;
3
+ /**
4
+ * Label Studio Webhook Event Types
5
+ */
6
+ export declare enum WebhookAction {
7
+ ANNOTATION_CREATED = "ANNOTATION_CREATED",
8
+ ANNOTATION_UPDATED = "ANNOTATION_UPDATED",
9
+ ANNOTATION_DELETED = "ANNOTATION_DELETED",
10
+ TASK_CREATED = "TASK_CREATED",
11
+ TASK_UPDATED = "TASK_UPDATED",
12
+ TASK_DELETED = "TASK_DELETED",
13
+ PROJECT_UPDATED = "PROJECT_UPDATED"
14
+ }
15
+ export interface WebhookPayload {
16
+ action: WebhookAction;
17
+ project: {
18
+ id: number;
19
+ title: string;
20
+ };
21
+ task?: {
22
+ id: number;
23
+ data: any;
24
+ annotations: any[];
25
+ };
26
+ annotation?: {
27
+ id: number;
28
+ result: any[];
29
+ completed_by: {
30
+ id: number;
31
+ email: string;
32
+ };
33
+ lead_time: number;
34
+ };
35
+ }
36
+ /**
37
+ * Webhook handler function type
38
+ * Applications can register custom handlers for any webhook action
39
+ */
40
+ export type WebhookHandler = (payload: WebhookPayload, context: Koa.Context) => Promise<void>;
41
+ /**
42
+ * Register a custom webhook handler
43
+ * Handlers are executed in registration order
44
+ *
45
+ * @param action - Webhook action to handle
46
+ * @param handler - Handler function
47
+ *
48
+ * @example
49
+ * registerWebhookHandler(WebhookAction.ANNOTATION_CREATED, async (payload, ctx) => {
50
+ * console.log('Custom handler:', payload.annotation?.id)
51
+ * // Store annotation in database
52
+ * // Trigger ML training
53
+ * // Send notifications
54
+ * })
55
+ */
56
+ export declare function registerWebhookHandler(action: WebhookAction, handler: WebhookHandler): void;
57
+ /**
58
+ * Unregister a webhook handler
59
+ */
60
+ export declare function unregisterWebhookHandler(action: WebhookAction, handler: WebhookHandler): void;
61
+ /**
62
+ * Clear all custom handlers for an action
63
+ */
64
+ export declare function clearWebhookHandlers(action?: WebhookAction): void;
65
+ export { webhookRouter };