@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,340 @@
1
+ import axios from 'axios'
2
+ import { User } from '@things-factory/auth-base'
3
+ import { config } from '@things-factory/env'
4
+ import { LabelStudioRoleMapper, LabelStudioPermissions } from './label-studio-role-mapper.js'
5
+ import { Domain, getRepository } from '@things-factory/shell'
6
+
7
+ type LabelStudioConfig = {
8
+ serverUrl: string
9
+ apiToken: string
10
+ interfaces: string
11
+ }
12
+
13
+ // Get Label Studio config from server config file
14
+ function getLabelStudioConfig(): LabelStudioConfig {
15
+ return config.get('labelStudio', {
16
+ serverUrl: '',
17
+ apiToken: '',
18
+ interfaces: 'panel,controls,annotations:menu'
19
+ })
20
+ }
21
+
22
+ export interface SyncResult {
23
+ success: boolean
24
+ email: string
25
+ action: 'created' | 'updated' | 'deactivated' | 'skipped' | 'error'
26
+ lsUserId?: string
27
+ lsPermissions?: string // 'Admin (Full access)' | 'Staff (Labeling only)' | 'Inactive'
28
+ error?: string
29
+ }
30
+
31
+ export interface SyncSummary {
32
+ total: number
33
+ created: number
34
+ updated: number
35
+ deactivated: number
36
+ skipped: number
37
+ errors: number
38
+ results: SyncResult[]
39
+ }
40
+
41
+ /**
42
+ * Label Studio 사용자 프로비저닝 서비스
43
+ *
44
+ * Things-Factory 사용자를 Label Studio에 배치 동기화합니다.
45
+ */
46
+ export class UserProvisioningService {
47
+ /**
48
+ * 설정 검증
49
+ */
50
+ private static validateConfig(): void {
51
+ const config = getLabelStudioConfig()
52
+
53
+ if (!config.apiToken) {
54
+ throw new Error('Label Studio API token is not configured')
55
+ }
56
+
57
+ if (!config.serverUrl) {
58
+ throw new Error('Label Studio server URL is not configured')
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 단일 사용자 동기화
64
+ *
65
+ * @param domain Things-Factory 도메인
66
+ * @param user Things-Factory 사용자
67
+ * @returns 동기화 결과
68
+ */
69
+ static async syncUser(domain: Domain, user: User): Promise<SyncResult> {
70
+ // 설정 검증
71
+ this.validateConfig()
72
+
73
+ const config = getLabelStudioConfig()
74
+ try {
75
+ // 1. Label Studio 권한 확인
76
+ const hasLSPrivilege =
77
+ (await User.hasPrivilege('label-studio', 'query', domain, user)) ||
78
+ (await User.hasPrivilege('label-studio', 'mutation', domain, user))
79
+
80
+ if (!hasLSPrivilege) {
81
+ // Label Studio 권한 없음 → Label Studio에서 비활성화
82
+ const deactivated = await this.deactivateUser(user.email, config)
83
+
84
+ return {
85
+ success: true,
86
+ email: user.email,
87
+ action: deactivated ? 'deactivated' : 'skipped'
88
+ }
89
+ }
90
+
91
+ // 2. Label Studio 권한 매핑
92
+ const lsPermissions = await LabelStudioRoleMapper.mapUserPermissions(domain, user)
93
+
94
+ // 3. Label Studio API로 사용자 생성 또는 업데이트
95
+ const result = await this.createOrUpdateLabelStudioUser(user, lsPermissions, config)
96
+
97
+ return {
98
+ success: true,
99
+ email: user.email,
100
+ action: result.created ? 'created' : 'updated',
101
+ lsUserId: result.id.toString(),
102
+ lsPermissions: LabelStudioRoleMapper.getPermissionsDescription(lsPermissions)
103
+ }
104
+ } catch (error: any) {
105
+ console.error(`Failed to sync user ${user.email}:`, error.message)
106
+
107
+ return {
108
+ success: false,
109
+ email: user.email,
110
+ action: 'error',
111
+ error: error.message
112
+ }
113
+ }
114
+ }
115
+
116
+ static async getDomainUsers(domain: Domain): Promise<User[]> {
117
+ const qb = getRepository(User).createQueryBuilder('USER')
118
+ qb.select().andWhere(qb => {
119
+ const subQuery = qb
120
+ .subQuery()
121
+ .select('USERS_DOMAINS.users_id')
122
+ .from('users_domains', 'USERS_DOMAINS')
123
+ .where('USERS_DOMAINS.domains_id = :domainId', { domainId: domain.id })
124
+ .getQuery()
125
+
126
+ return 'USER.id IN ' + subQuery
127
+ })
128
+
129
+ const [items, total] = await qb.getManyAndCount()
130
+
131
+ const foundUsers: User[] = items.map((item: User) => {
132
+ item.owner = item.id === domain.owner
133
+ return item
134
+ })
135
+
136
+ return foundUsers
137
+ }
138
+
139
+ /**
140
+ * 도메인의 모든 사용자 일괄 동기화
141
+ *
142
+ * @param domain Things-Factory 도메인
143
+ * @returns 동기화 요약
144
+ */
145
+ static async syncAllUsers(domain: Domain): Promise<SyncSummary> {
146
+ // 설정 검증
147
+ this.validateConfig()
148
+
149
+ // 도메인의 모든 활성 사용자 조회
150
+ const users = await UserProvisioningService.getDomainUsers(domain)
151
+
152
+ console.log(`🔄 Starting batch sync for ${users.length} users...`)
153
+
154
+ const results: SyncResult[] = []
155
+
156
+ // 각 사용자 동기화
157
+ for (const user of users) {
158
+ const result = await this.syncUser(domain, user)
159
+ results.push(result)
160
+
161
+ // API Rate Limiting 방지
162
+ await this.sleep(100)
163
+ }
164
+
165
+ // 요약 생성
166
+ const summary: SyncSummary = {
167
+ total: users.length,
168
+ created: results.filter(r => r.action === 'created').length,
169
+ updated: results.filter(r => r.action === 'updated').length,
170
+ deactivated: results.filter(r => r.action === 'deactivated').length,
171
+ skipped: results.filter(r => r.action === 'skipped').length,
172
+ errors: results.filter(r => r.action === 'error').length,
173
+ results
174
+ }
175
+
176
+ console.log(`✅ Batch sync completed:`, {
177
+ total: summary.total,
178
+ created: summary.created,
179
+ updated: summary.updated,
180
+ deactivated: summary.deactivated,
181
+ skipped: summary.skipped,
182
+ errors: summary.errors
183
+ })
184
+
185
+ return summary
186
+ }
187
+
188
+ /**
189
+ * Label Studio API를 통해 사용자 생성 또는 업데이트
190
+ */
191
+ private static async createOrUpdateLabelStudioUser(
192
+ user: User,
193
+ lsPermissions: LabelStudioPermissions,
194
+ config: LabelStudioConfig
195
+ ): Promise<any> {
196
+ const apiUrl = this.buildApiUrl(config.serverUrl, '/api/users')
197
+
198
+ // 이름 파싱
199
+ const nameParts = (user.name || user.email).split(' ')
200
+ const firstName = nameParts[0] || user.email
201
+ const lastName = nameParts.slice(1).join(' ') || ''
202
+
203
+ try {
204
+ // 이메일로 기존 사용자 조회
205
+ const searchResponse = await axios.get(apiUrl, {
206
+ headers: {
207
+ Authorization: `Token ${config.apiToken}`
208
+ },
209
+ params: {
210
+ email: user.email
211
+ }
212
+ })
213
+
214
+ if (searchResponse.data.results && searchResponse.data.results.length > 0) {
215
+ // 기존 사용자 업데이트
216
+ const existingUser = searchResponse.data.results[0]
217
+
218
+ const updateResponse = await axios.patch(
219
+ `${apiUrl}/${existingUser.id}/`,
220
+ {
221
+ email: user.email,
222
+ username: user.email,
223
+ first_name: firstName,
224
+ last_name: lastName,
225
+ is_superuser: lsPermissions.is_superuser,
226
+ is_staff: lsPermissions.is_staff,
227
+ is_active: lsPermissions.is_active
228
+ },
229
+ {
230
+ headers: {
231
+ Authorization: `Token ${config.apiToken}`,
232
+ 'Content-Type': 'application/json'
233
+ }
234
+ }
235
+ )
236
+
237
+ return {
238
+ ...updateResponse.data,
239
+ created: false
240
+ }
241
+ } else {
242
+ // 새 사용자 생성
243
+ const createResponse = await axios.post(
244
+ apiUrl,
245
+ {
246
+ email: user.email,
247
+ username: user.email,
248
+ first_name: firstName,
249
+ last_name: lastName,
250
+ password: this.generateRandomPassword(),
251
+ is_superuser: lsPermissions.is_superuser,
252
+ is_staff: lsPermissions.is_staff,
253
+ is_active: lsPermissions.is_active
254
+ },
255
+ {
256
+ headers: {
257
+ Authorization: `Token ${config.apiToken}`,
258
+ 'Content-Type': 'application/json'
259
+ }
260
+ }
261
+ )
262
+
263
+ return {
264
+ ...createResponse.data,
265
+ created: true
266
+ }
267
+ }
268
+ } catch (error: any) {
269
+ console.error(`Label Studio API error for ${user.email}:`, error.response?.data || error.message)
270
+ throw error
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Label Studio에서 사용자 비활성화
276
+ */
277
+ private static async deactivateUser(email: string, config: LabelStudioConfig): Promise<boolean> {
278
+ const apiUrl = this.buildApiUrl(config.serverUrl, '/api/users')
279
+
280
+ try {
281
+ // 이메일로 사용자 조회
282
+ const searchResponse = await axios.get(apiUrl, {
283
+ headers: {
284
+ Authorization: `Token ${config.apiToken}`
285
+ },
286
+ params: { email }
287
+ })
288
+
289
+ if (searchResponse.data.results && searchResponse.data.results.length > 0) {
290
+ const user = searchResponse.data.results[0]
291
+
292
+ // 비활성화
293
+ await axios.patch(
294
+ `${apiUrl}/${user.id}/`,
295
+ { is_active: false },
296
+ {
297
+ headers: {
298
+ Authorization: `Token ${config.apiToken}`,
299
+ 'Content-Type': 'application/json'
300
+ }
301
+ }
302
+ )
303
+
304
+ return true
305
+ }
306
+
307
+ return false
308
+ } catch (error: any) {
309
+ console.error(`Failed to deactivate user ${email}:`, error.message)
310
+ return false
311
+ }
312
+ }
313
+
314
+ /**
315
+ * API URL 빌드
316
+ */
317
+ private static buildApiUrl(serverUrl: string, path: string): string {
318
+ let url = serverUrl.replace(/\/$/, '')
319
+
320
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
321
+ url = `https://${url}`
322
+ }
323
+
324
+ return `${url}${path}`
325
+ }
326
+
327
+ /**
328
+ * 랜덤 비밀번호 생성 (SSO 사용으로 실제로는 사용 안 됨)
329
+ */
330
+ private static generateRandomPassword(): string {
331
+ return Math.random().toString(36).slice(-16) + Math.random().toString(36).slice(-16)
332
+ }
333
+
334
+ /**
335
+ * Sleep 유틸리티
336
+ */
337
+ private static sleep(ms: number): Promise<void> {
338
+ return new Promise(resolve => setTimeout(resolve, ms))
339
+ }
340
+ }
@@ -0,0 +1,19 @@
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 {
6
+ WebhookAction,
7
+ WebhookPayload,
8
+ WebhookHandler,
9
+ registerWebhookHandler,
10
+ unregisterWebhookHandler,
11
+ clearWebhookHandlers
12
+ } from './route/webhook.js'
13
+
14
+ import './route.js'
15
+ import './route/webhook.js'
16
+
17
+ process.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {
18
+ console.log('[integration-label-studio:bootstrap] Label Studio integration initialized with subdomain cookie-sharing SSO')
19
+ })
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Label Studio SSO Route
3
+ *
4
+ * Implements subdomain-based cookie sharing for Label Studio SSO authentication.
5
+ * This approach eliminates the need for proxy by using shared domain cookies.
6
+ *
7
+ * ## Architecture:
8
+ * 1. Client calls /label-studio/sso/setup
9
+ * 2. Backend gets JWT token from Label Studio using API Token
10
+ * 3. Backend sets cookie with shared domain (e.g., .nubison.localhost)
11
+ * 4. Client loads Label Studio directly in iframe
12
+ * 5. Cookie is automatically sent with iframe requests
13
+ *
14
+ * ## Configuration:
15
+ * Label Studio .env must include:
16
+ * - JWT_SSO_SECRET: Shared secret for JWT signing
17
+ * - JWT_SSO_COOKIE_NAME: Cookie name (default: ls_auth_token)
18
+ * - CSRF_TRUSTED_ORIGINS: Include all frontend domains
19
+ * - ALLOWED_HOSTS: Include all subdomain patterns
20
+ */
21
+ import Koa from 'koa'
22
+ import Router from 'koa-router'
23
+ import { config } from '@things-factory/env'
24
+ import { LabelStudioSSOService } from '../service/label-studio-sso-service.js'
25
+
26
+ const ssoRouter = new Router()
27
+
28
+ /**
29
+ * Get Label Studio configuration
30
+ */
31
+ function getLabelStudioConfig() {
32
+ const labelStudioConfig = config.get('labelStudio', {
33
+ serverUrl: 'http://localhost:8080',
34
+ apiToken: '',
35
+ cookieDomain: '' // e.g., '.nubison.localhost' for subdomain sharing
36
+ })
37
+
38
+ return {
39
+ serverUrl: labelStudioConfig.serverUrl,
40
+ apiToken: labelStudioConfig.apiToken,
41
+ cookieDomain: labelStudioConfig.cookieDomain || ''
42
+ }
43
+ }
44
+
45
+ /**
46
+ * SSO Setup Endpoint
47
+ *
48
+ * This endpoint must be called by the client before loading Label Studio iframe
49
+ * to establish SSO authentication using subdomain cookie sharing.
50
+ *
51
+ * Flow:
52
+ * 1. Client calls /label-studio/sso/setup
53
+ * 2. Backend requests JWT token from Label Studio
54
+ * 3. Backend sets ls_auth_token cookie with shared domain
55
+ * 4. Client loads Label Studio iframe - auto-login succeeds
56
+ *
57
+ * @example
58
+ * fetch('/label-studio/sso/setup', { credentials: 'include' })
59
+ */
60
+ ssoRouter.get('/label-studio/sso/setup', async (ctx: Koa.Context) => {
61
+ try {
62
+ const user = ctx.state.user
63
+
64
+ if (!user || !user.email) {
65
+ ctx.status = 401
66
+ ctx.body = {
67
+ success: false,
68
+ error: 'Unauthorized',
69
+ message: 'User authentication required'
70
+ }
71
+ return
72
+ }
73
+
74
+ const { cookieDomain } = getLabelStudioConfig()
75
+
76
+ // Cookie name must match Label Studio's JWT_SSO_COOKIE_NAME setting
77
+ const cookieName = 'ls_auth_token'
78
+ const existingToken = ctx.cookies.get(cookieName)
79
+
80
+ if (existingToken) {
81
+ // Token already exists
82
+ ctx.status = 200
83
+ ctx.body = {
84
+ success: true,
85
+ message: 'SSO token already exists',
86
+ user: user.email
87
+ }
88
+ return
89
+ }
90
+
91
+ console.log(`[Label Studio SSO] Setting up token for ${user.email}`)
92
+
93
+ // Request JWT token from Label Studio
94
+ const tokenData = await LabelStudioSSOService.getSSOToken(user.email)
95
+
96
+ if (tokenData) {
97
+ // Clear existing sessionid to prevent conflict with SSO token
98
+ ctx.cookies.set('sessionid', '', {
99
+ domain: cookieDomain || undefined,
100
+ path: '/',
101
+ maxAge: 0 // Expire immediately
102
+ })
103
+
104
+ // Set cookie with shared domain for subdomain access
105
+ const cookieOptions: any = {
106
+ httpOnly: false, // Allow client-side access for debugging
107
+ secure: ctx.protocol === 'https',
108
+ sameSite: 'lax',
109
+ path: '/',
110
+ maxAge: tokenData.expires_in * 1000 // Convert seconds to milliseconds
111
+ }
112
+
113
+ // Only set domain if cookieDomain is configured
114
+ // This allows same-origin cookie for single domain setup
115
+ if (cookieDomain) {
116
+ cookieOptions.domain = cookieDomain
117
+ console.log(`[Label Studio SSO] Using shared cookie domain: ${cookieDomain}`)
118
+ }
119
+
120
+ ctx.cookies.set(cookieName, tokenData.token, cookieOptions)
121
+
122
+ console.log(
123
+ `[Label Studio SSO] Token set for ${user.email} (expires in ${tokenData.expires_in}s, domain: ${cookieDomain || 'same-origin'})`
124
+ )
125
+
126
+ ctx.status = 200
127
+ ctx.body = {
128
+ success: true,
129
+ message: 'SSO token setup complete',
130
+ user: user.email,
131
+ expiresIn: tokenData.expires_in,
132
+ cookieDomain: cookieDomain || 'same-origin'
133
+ }
134
+ } else {
135
+ console.error(`[Label Studio SSO] Failed to acquire token for ${user.email}`)
136
+
137
+ ctx.status = 500
138
+ ctx.body = {
139
+ success: false,
140
+ error: 'Token Acquisition Failed',
141
+ message: 'Failed to acquire SSO token from Label Studio'
142
+ }
143
+ }
144
+ } catch (error: any) {
145
+ console.error('[Label Studio SSO] Setup error:', error.message)
146
+
147
+ ctx.status = 500
148
+ ctx.body = {
149
+ success: false,
150
+ error: 'Internal Server Error',
151
+ message: error.message
152
+ }
153
+ }
154
+ })
155
+
156
+ /**
157
+ * Health check endpoint
158
+ */
159
+ ssoRouter.get('/label-studio/sso/health', async (ctx: Koa.Context) => {
160
+ try {
161
+ const { serverUrl, cookieDomain } = getLabelStudioConfig()
162
+
163
+ ctx.status = 200
164
+ ctx.body = {
165
+ status: 'ok',
166
+ labelStudioUrl: serverUrl || 'not configured',
167
+ cookieDomain: cookieDomain || 'same-origin',
168
+ message: 'Label Studio SSO is running'
169
+ }
170
+ } catch (error: any) {
171
+ ctx.status = 503
172
+ ctx.body = {
173
+ status: 'error',
174
+ message: error.message
175
+ }
176
+ }
177
+ })
178
+
179
+ /**
180
+ * Configuration endpoint
181
+ */
182
+ ssoRouter.get('/label-studio/sso/config', async (ctx: Koa.Context) => {
183
+ const { serverUrl, apiToken, cookieDomain } = getLabelStudioConfig()
184
+
185
+ ctx.status = 200
186
+ ctx.body = {
187
+ labelStudioUrl: serverUrl || 'not configured',
188
+ cookieDomain: cookieDomain || 'same-origin',
189
+ hasApiToken: !!apiToken,
190
+ ssoConfigured: LabelStudioSSOService.verifyConfig()
191
+ }
192
+ })
193
+
194
+ export { ssoRouter }