@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.
- package/CHANGELOG.md +85 -0
- package/EXTERNAL_DATA_SOURCING.md +484 -0
- package/IMPLEMENTATION_GUIDE.md +469 -0
- package/INTEGRATION.md +279 -0
- package/README.md +1014 -0
- package/SETUP_GUIDE.md +577 -0
- package/TEST_GUIDE.md +387 -0
- package/UI_CUSTOMIZATION.md +395 -0
- package/USER_SYNC_GUIDE.md +514 -0
- package/client/bootstrap.ts +1 -0
- package/client/index.ts +1 -0
- package/client/label-studio-label-page.ts +52 -0
- package/client/label-studio-project-create.ts +216 -0
- package/client/label-studio-project-list.ts +214 -0
- package/client/label-studio-wrapper.ts +294 -0
- package/client/route.ts +15 -0
- package/client/tsconfig.json +13 -0
- package/config/config.development.js +124 -0
- package/config/config.production.js +182 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/label-studio-label-page.d.ts +8 -0
- package/dist-client/label-studio-label-page.js +54 -0
- package/dist-client/label-studio-label-page.js.map +1 -0
- package/dist-client/label-studio-project-create.d.ts +16 -0
- package/dist-client/label-studio-project-create.js +235 -0
- package/dist-client/label-studio-project-create.js.map +1 -0
- package/dist-client/label-studio-project-list.d.ts +16 -0
- package/dist-client/label-studio-project-list.js +222 -0
- package/dist-client/label-studio-project-list.js.map +1 -0
- package/dist-client/label-studio-wrapper.d.ts +57 -0
- package/dist-client/label-studio-wrapper.js +304 -0
- package/dist-client/label-studio-wrapper.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +14 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
- package/dist-server/controller/label-studio-role-mapper.js +65 -0
- package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
- package/dist-server/controller/user-provisioning-service.d.ts +66 -0
- package/dist-server/controller/user-provisioning-service.js +264 -0
- package/dist-server/controller/user-provisioning-service.js.map +1 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +19 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/route/label-studio-sso.d.ts +2 -0
- package/dist-server/route/label-studio-sso.js +156 -0
- package/dist-server/route/label-studio-sso.js.map +1 -0
- package/dist-server/route/webhook.d.ts +65 -0
- package/dist-server/route/webhook.js +248 -0
- package/dist-server/route/webhook.js.map +1 -0
- package/dist-server/route.d.ts +1 -0
- package/dist-server/route.js +21 -0
- package/dist-server/route.js.map +1 -0
- package/dist-server/service/ai-prediction-service.d.ts +27 -0
- package/dist-server/service/ai-prediction-service.js +222 -0
- package/dist-server/service/ai-prediction-service.js.map +1 -0
- package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
- package/dist-server/service/dataset-labeling-integration.js +512 -0
- package/dist-server/service/dataset-labeling-integration.js.map +1 -0
- package/dist-server/service/external-data-source-service.d.ts +78 -0
- package/dist-server/service/external-data-source-service.js +415 -0
- package/dist-server/service/external-data-source-service.js.map +1 -0
- package/dist-server/service/index.d.ts +12 -0
- package/dist-server/service/index.js +27 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/label-studio-sso-service.d.ts +38 -0
- package/dist-server/service/label-studio-sso-service.js +98 -0
- package/dist-server/service/label-studio-sso-service.js.map +1 -0
- package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
- package/dist-server/service/ml/ml-backend-service.js +153 -0
- package/dist-server/service/ml/ml-backend-service.js.map +1 -0
- package/dist-server/service/prediction/prediction-management.d.ts +32 -0
- package/dist-server/service/prediction/prediction-management.js +299 -0
- package/dist-server/service/prediction/prediction-management.js.map +1 -0
- package/dist-server/service/project/project-management.d.ts +36 -0
- package/dist-server/service/project/project-management.js +309 -0
- package/dist-server/service/project/project-management.js.map +1 -0
- package/dist-server/service/task/task-management.d.ts +42 -0
- package/dist-server/service/task/task-management.js +372 -0
- package/dist-server/service/task/task-management.js.map +1 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
- package/dist-server/service/webhook/webhook-management.d.ts +21 -0
- package/dist-server/service/webhook/webhook-management.js +134 -0
- package/dist-server/service/webhook/webhook-management.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/dist-server/types/dataset-labeling-types.d.ts +71 -0
- package/dist-server/types/dataset-labeling-types.js +259 -0
- package/dist-server/types/dataset-labeling-types.js.map +1 -0
- package/dist-server/types/label-studio-types.d.ts +128 -0
- package/dist-server/types/label-studio-types.js +494 -0
- package/dist-server/types/label-studio-types.js.map +1 -0
- package/dist-server/types/prediction-types.d.ts +39 -0
- package/dist-server/types/prediction-types.js +121 -0
- package/dist-server/types/prediction-types.js.map +1 -0
- package/dist-server/utils/annotation-exporter.d.ts +104 -0
- package/dist-server/utils/annotation-exporter.js +261 -0
- package/dist-server/utils/annotation-exporter.js.map +1 -0
- package/dist-server/utils/label-config-builder.d.ts +117 -0
- package/dist-server/utils/label-config-builder.js +286 -0
- package/dist-server/utils/label-config-builder.js.map +1 -0
- package/dist-server/utils/label-studio-api-client.d.ts +180 -0
- package/dist-server/utils/label-studio-api-client.js +401 -0
- package/dist-server/utils/label-studio-api-client.js.map +1 -0
- package/dist-server/utils/media-url-extractor.d.ts +45 -0
- package/dist-server/utils/media-url-extractor.js +152 -0
- package/dist-server/utils/media-url-extractor.js.map +1 -0
- package/dist-server/utils/task-transformer.d.ts +108 -0
- package/dist-server/utils/task-transformer.js +260 -0
- package/dist-server/utils/task-transformer.js.map +1 -0
- package/package.json +47 -0
- package/server/SERVER_STRUCTURE.md +351 -0
- package/server/controller/label-studio-role-mapper.ts +76 -0
- package/server/controller/user-provisioning-service.ts +340 -0
- package/server/index.ts +19 -0
- package/server/route/label-studio-sso.ts +194 -0
- package/server/route/webhook.ts +304 -0
- package/server/route.ts +35 -0
- package/server/service/ai-prediction-service.ts +239 -0
- package/server/service/dataset-labeling-integration.ts +590 -0
- package/server/service/external-data-source-service.ts +438 -0
- package/server/service/index.ts +24 -0
- package/server/service/label-studio-sso-service.ts +108 -0
- package/server/service/labeling-scenario-service.ts.deprecated +566 -0
- package/server/service/ml/ml-backend-service.ts +127 -0
- package/server/service/prediction/prediction-management.ts +281 -0
- package/server/service/project/project-management.ts +284 -0
- package/server/service/task/task-management.ts +363 -0
- package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
- package/server/service/webhook/webhook-management.ts +109 -0
- package/server/tsconfig.json +11 -0
- package/server/types/dataset-labeling-types.ts +181 -0
- package/server/types/global.d.ts +23 -0
- package/server/types/label-studio-types.ts +346 -0
- package/server/types/prediction-types.ts +86 -0
- package/server/types/scenario-types.ts.deprecated +362 -0
- package/server/utils/annotation-exporter.ts +340 -0
- package/server/utils/label-config-builder.ts +340 -0
- package/server/utils/label-studio-api-client.ts +487 -0
- package/server/utils/media-url-extractor.ts +193 -0
- package/server/utils/task-transformer.ts +342 -0
- package/test-ai-prediction.js +268 -0
- package/test-dataset-integration.js +449 -0
- package/test-simple.js +89 -0
- 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,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 };
|