libre-webui 0.22.1 → 0.23.0
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/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +8 -0
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/middleware/auth.d.ts +15 -1
- package/backend/dist/middleware/auth.d.ts.map +1 -1
- package/backend/dist/middleware/auth.js +153 -33
- package/backend/dist/middleware/auth.js.map +1 -1
- package/backend/dist/persistence/index.d.ts +1 -0
- package/backend/dist/persistence/index.d.ts.map +1 -1
- package/backend/dist/persistence/index.js.map +1 -1
- package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
- package/backend/dist/persistence/postgresMigrationRegistry.js +2 -0
- package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
- package/backend/dist/persistence/postgresPersistence.d.ts.map +1 -1
- package/backend/dist/persistence/postgresPersistence.js +4 -0
- package/backend/dist/persistence/postgresPersistence.js.map +1 -1
- package/backend/dist/persistence/postgresSecurityRepositories.d.ts +7 -0
- package/backend/dist/persistence/postgresSecurityRepositories.d.ts.map +1 -0
- package/backend/dist/persistence/postgresSecurityRepositories.js +435 -0
- package/backend/dist/persistence/postgresSecurityRepositories.js.map +1 -0
- package/backend/dist/persistence/postgresTrustFoundationMigration.d.ts +8 -0
- package/backend/dist/persistence/postgresTrustFoundationMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresTrustFoundationMigration.js +151 -0
- package/backend/dist/persistence/postgresTrustFoundationMigration.js.map +1 -0
- package/backend/dist/persistence/securityTypes.d.ts +196 -0
- package/backend/dist/persistence/securityTypes.d.ts.map +1 -0
- package/backend/dist/persistence/securityTypes.js +18 -0
- package/backend/dist/persistence/securityTypes.js.map +1 -0
- package/backend/dist/persistence/sqliteMigrations.d.ts +1 -0
- package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteMigrations.js +311 -0
- package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
- package/backend/dist/persistence/sqlitePersistence.d.ts.map +1 -1
- package/backend/dist/persistence/sqlitePersistence.js +3 -0
- package/backend/dist/persistence/sqlitePersistence.js.map +1 -1
- package/backend/dist/persistence/sqliteSecurityRepositories.d.ts +6 -0
- package/backend/dist/persistence/sqliteSecurityRepositories.d.ts.map +1 -0
- package/backend/dist/persistence/sqliteSecurityRepositories.js +497 -0
- package/backend/dist/persistence/sqliteSecurityRepositories.js.map +1 -0
- package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteToPostgresMigration.js +55 -0
- package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
- package/backend/dist/persistence/types.d.ts +4 -0
- package/backend/dist/persistence/types.d.ts.map +1 -1
- package/backend/dist/platform/jobs/domainJobHandlers.d.ts.map +1 -1
- package/backend/dist/platform/jobs/domainJobHandlers.js +7 -0
- package/backend/dist/platform/jobs/domainJobHandlers.js.map +1 -1
- package/backend/dist/routes/access.d.ts +3 -0
- package/backend/dist/routes/access.d.ts.map +1 -0
- package/backend/dist/routes/access.js +134 -0
- package/backend/dist/routes/access.js.map +1 -0
- package/backend/dist/routes/audit.d.ts +3 -0
- package/backend/dist/routes/audit.d.ts.map +1 -0
- package/backend/dist/routes/audit.js +82 -0
- package/backend/dist/routes/audit.js.map +1 -0
- package/backend/dist/routes/auth.d.ts.map +1 -1
- package/backend/dist/routes/auth.js +322 -11
- package/backend/dist/routes/auth.js.map +1 -1
- package/backend/dist/routes/groups.d.ts +3 -0
- package/backend/dist/routes/groups.d.ts.map +1 -0
- package/backend/dist/routes/groups.js +161 -0
- package/backend/dist/routes/groups.js.map +1 -0
- package/backend/dist/services/apiTokenService.d.ts +51 -0
- package/backend/dist/services/apiTokenService.d.ts.map +1 -0
- package/backend/dist/services/apiTokenService.js +220 -0
- package/backend/dist/services/apiTokenService.js.map +1 -0
- package/backend/dist/services/authService.d.ts +20 -4
- package/backend/dist/services/authService.d.ts.map +1 -1
- package/backend/dist/services/authService.js +44 -6
- package/backend/dist/services/authService.js.map +1 -1
- package/backend/dist/services/authSessionService.d.ts +34 -0
- package/backend/dist/services/authSessionService.d.ts.map +1 -0
- package/backend/dist/services/authSessionService.js +205 -0
- package/backend/dist/services/authSessionService.js.map +1 -0
- package/backend/dist/services/authorizationService.d.ts +64 -0
- package/backend/dist/services/authorizationService.d.ts.map +1 -0
- package/backend/dist/services/authorizationService.js +176 -0
- package/backend/dist/services/authorizationService.js.map +1 -0
- package/backend/dist/services/groupService.d.ts +27 -0
- package/backend/dist/services/groupService.d.ts.map +1 -0
- package/backend/dist/services/groupService.js +245 -0
- package/backend/dist/services/groupService.js.map +1 -0
- package/backend/dist/services/groupVectorPrincipalResolver.d.ts +12 -0
- package/backend/dist/services/groupVectorPrincipalResolver.d.ts.map +1 -0
- package/backend/dist/services/groupVectorPrincipalResolver.js +33 -0
- package/backend/dist/services/groupVectorPrincipalResolver.js.map +1 -0
- package/backend/dist/services/modelAccessService.d.ts +1 -0
- package/backend/dist/services/modelAccessService.d.ts.map +1 -1
- package/backend/dist/services/modelAccessService.js +3 -5
- package/backend/dist/services/modelAccessService.js.map +1 -1
- package/backend/dist/services/oauthSecurity.d.ts +12 -1
- package/backend/dist/services/oauthSecurity.d.ts.map +1 -1
- package/backend/dist/services/oauthSecurity.js +41 -0
- package/backend/dist/services/oauthSecurity.js.map +1 -1
- package/backend/dist/services/resourceGrantService.d.ts +28 -0
- package/backend/dist/services/resourceGrantService.d.ts.map +1 -0
- package/backend/dist/services/resourceGrantService.js +192 -0
- package/backend/dist/services/resourceGrantService.js.map +1 -0
- package/backend/dist/services/securityAuditService.d.ts +35 -0
- package/backend/dist/services/securityAuditService.d.ts.map +1 -0
- package/backend/dist/services/securityAuditService.js +159 -0
- package/backend/dist/services/securityAuditService.js.map +1 -0
- package/backend/dist/services/simpleGitHubOAuth.d.ts.map +1 -1
- package/backend/dist/services/simpleGitHubOAuth.js +4 -2
- package/backend/dist/services/simpleGitHubOAuth.js.map +1 -1
- package/backend/dist/services/simpleOidcOAuth.d.ts +62 -0
- package/backend/dist/services/simpleOidcOAuth.d.ts.map +1 -0
- package/backend/dist/services/simpleOidcOAuth.js +397 -0
- package/backend/dist/services/simpleOidcOAuth.js.map +1 -0
- package/backend/dist/services/webSearchService.d.ts +1 -0
- package/backend/dist/services/webSearchService.d.ts.map +1 -1
- package/backend/dist/services/webSearchService.js +3 -5
- package/backend/dist/services/webSearchService.js.map +1 -1
- package/backend/dist/services/websocketTicketService.d.ts +3 -1
- package/backend/dist/services/websocketTicketService.d.ts.map +1 -1
- package/backend/dist/services/websocketTicketService.js +4 -1
- package/backend/dist/services/websocketTicketService.js.map +1 -1
- package/backend/dist/services/workAccessService.d.ts +1 -0
- package/backend/dist/services/workAccessService.d.ts.map +1 -1
- package/backend/dist/services/workAccessService.js +3 -5
- package/backend/dist/services/workAccessService.js.map +1 -1
- package/backend/dist/services/workEventService.d.ts.map +1 -1
- package/backend/dist/services/workEventService.js +74 -18
- package/backend/dist/services/workEventService.js.map +1 -1
- package/backend/dist/services/workRuntimeService.d.ts.map +1 -1
- package/backend/dist/services/workRuntimeService.js +19 -1
- package/backend/dist/services/workRuntimeService.js.map +1 -1
- package/backend/dist/websocketServer.d.ts.map +1 -1
- package/backend/dist/websocketServer.js +25 -1
- package/backend/dist/websocketServer.js.map +1 -1
- package/backend/dist/workTerminalServer.d.ts.map +1 -1
- package/backend/dist/workTerminalServer.js +19 -1
- package/backend/dist/workTerminalServer.js.map +1 -1
- package/backend/dist/worker.js +2 -0
- package/backend/dist/worker.js.map +1 -1
- package/backend/package.json +1 -1
- package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
- package/frontend/dist/assets/index-CudKJ5Bs.js +27 -0
- package/frontend/dist/assets/index-DBaj1DRj.css +1 -0
- package/frontend/dist/index.html +11 -11
- package/frontend/dist/js/ArtifactContainer-CigyGLnW.js +1 -0
- package/frontend/dist/js/{ArtifactDemoPage-BDBXYv9l.js → ArtifactDemoPage-CITpRQXa.js} +1 -1
- package/frontend/dist/js/{ArtifactSandboxFrame-BN16NtKe.js → ArtifactSandboxFrame-C8_7sKm1.js} +2 -2
- package/frontend/dist/js/ArtifactSlideOutPanel-D1f9zUZN.js +1 -0
- package/frontend/dist/js/ChatPage-D-Q1SGlI.js +23 -0
- package/frontend/dist/js/FirstTimeSetup-DwvqU8Jk.js +1 -0
- package/frontend/dist/js/{GalleryPage-CV_vbE7X.js → GalleryPage-CFU6YW9r.js} +1 -1
- package/frontend/dist/js/{HomePage-P-e7Huco.js → HomePage-CEoZABYG.js} +1 -1
- package/frontend/dist/js/{LibreClawPage-Cee06SUM.js → LibreClawPage-VV4sD4-v.js} +1 -1
- package/frontend/dist/js/{ModelManagerModals-D7jIT3v1.js → ModelManagerModals-CZAWUxjI.js} +1 -1
- package/frontend/dist/js/{ModelSelector-DXucbvrz.js → ModelSelector-DVN9nTCB.js} +1 -1
- package/frontend/dist/js/ModelsPage-Ce41crA_.js +2 -0
- package/frontend/dist/js/{NotesPage-rEIPWGIb.js → NotesPage-BV08Gh2X.js} +1 -1
- package/frontend/dist/js/{PersonasPage-BXtepFuD.js → PersonasPage-CSpK5FvR.js} +2 -2
- package/frontend/dist/js/{PluginUsagePage-hhu8cxsu.js → PluginUsagePage-B9YPhusn.js} +1 -1
- package/frontend/dist/js/{RichMessageContent-DkiEvdSg.js → RichMessageContent-C3qddYMx.js} +1 -1
- package/frontend/dist/js/SettingsModal-CQSumlFr.js +1 -0
- package/frontend/dist/js/SystemPage-7KbZlMb_.js +1 -0
- package/frontend/dist/js/UserManagementPage-DnyW0Wmx.js +1 -0
- package/frontend/dist/js/WorkPage-B3cuSSwH.js +6 -0
- package/frontend/dist/js/api-DSzsasYT.js +14 -0
- package/frontend/dist/js/{appStore-DRSSjaHn.js → appStore-CYhqdjge.js} +2 -2
- package/frontend/dist/js/ar-D0z0P4rC.js +2 -0
- package/frontend/dist/js/{authStore-BVykIRec.js → authStore-CnSn5zM7.js} +3 -3
- package/frontend/dist/js/bn-BYYU41A7.js +2 -0
- package/frontend/dist/js/chatStore-P1aGCgiF.js +3 -0
- package/frontend/dist/js/{config-BTlPm-cZ.js → config-EEx_rELa.js} +1 -1
- package/frontend/dist/js/cs-BFx6KNZe.js +2 -0
- package/frontend/dist/js/da-DSDuY5Du.js +2 -0
- package/frontend/dist/js/de-YqhP0FmX.js +2 -0
- package/frontend/dist/js/es-Dpr_y1cN.js +2 -0
- package/frontend/dist/js/{estree-CfYahF_d.js → estree-Dc6SMAYy.js} +1 -1
- package/frontend/dist/js/fr-nVg_iAEC.js +2 -0
- package/frontend/dist/js/hi-gHKYCgkP.js +2 -0
- package/frontend/dist/js/{html-BJsDX1UZ.js → html-Bax5_Bzw.js} +1 -1
- package/frontend/dist/js/id-C_yID1Zk.js +2 -0
- package/frontend/dist/js/is-MoRPLbOM.js +2 -0
- package/frontend/dist/js/it-BZTscLxR.js +2 -0
- package/frontend/dist/js/ja-DpIbtmZq.js +2 -0
- package/frontend/dist/js/ko-LkWggmjU.js +2 -0
- package/frontend/dist/js/{markdown-GLyuUNig.js → markdown-DxaneQad.js} +1 -1
- package/frontend/dist/js/{messageCodeStyles-BHddEFP1.js → messageCodeStyles-CxXrl4jD.js} +2 -2
- package/frontend/dist/js/ms-p3XiQSAo.js +2 -0
- package/frontend/dist/js/nl-sEHHO2Kb.js +2 -0
- package/frontend/dist/js/pl-D2GD6PDW.js +2 -0
- package/frontend/dist/js/pt-DPkn3GUn.js +2 -0
- package/frontend/dist/js/ru-C42iHrEd.js +2 -0
- package/frontend/dist/js/{standalone-Dh56V0YW.js → standalone-CCpjftAD.js} +1 -1
- package/frontend/dist/js/sv-CGvHo4os.js +2 -0
- package/frontend/dist/js/th-BaDNpCdY.js +2 -0
- package/frontend/dist/js/tr-BHQvi1cN.js +2 -0
- package/frontend/dist/js/{typescript-DRy7A9w4.js → typescript-DtQEF6kq.js} +1 -1
- package/frontend/dist/js/{ui-CuY4IifA.js → ui-CUMAcARd.js} +2 -2
- package/frontend/dist/js/{ui-vendor-D3Ahl1x5.js → ui-vendor-54SgntkX.js} +1 -1
- package/frontend/dist/js/uk-7pNGS1b2.js +2 -0
- package/frontend/dist/js/vi-DgAfh6mo.js +2 -0
- package/frontend/dist/js/{workApi-DHuYNHLf.js → workApi-vRuNfpEf.js} +2 -2
- package/frontend/dist/js/zh-FRTz6ZqA.js +2 -0
- package/package.json +2 -2
- package/frontend/dist/assets/index-Jtx9Amhz.js +0 -16
- package/frontend/dist/assets/index-kxtZQLV6.css +0 -1
- package/frontend/dist/js/ArtifactContainer-fZegtmva.js +0 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-x1fS85Ww.js +0 -1
- package/frontend/dist/js/ChatPage-C_pXh7WN.js +0 -23
- package/frontend/dist/js/FirstTimeSetup-oRrlZnBm.js +0 -1
- package/frontend/dist/js/ModelsPage-C3_fZpMM.js +0 -2
- package/frontend/dist/js/SettingsModal-BNxCtJ51.js +0 -1
- package/frontend/dist/js/SystemPage-BvvKFuoG.js +0 -1
- package/frontend/dist/js/UserManagementPage-BccyR7si.js +0 -1
- package/frontend/dist/js/WorkPage-BgxrvY3I.js +0 -6
- package/frontend/dist/js/api-BvPDxsUQ.js +0 -14
- package/frontend/dist/js/ar-xTSvTq05.js +0 -2
- package/frontend/dist/js/bn-iYAsohEA.js +0 -2
- package/frontend/dist/js/chatStore-UPp5TV_t.js +0 -3
- package/frontend/dist/js/cs-YBHN88Hb.js +0 -2
- package/frontend/dist/js/da-D7qlLy_k.js +0 -2
- package/frontend/dist/js/de-DV671k2c.js +0 -2
- package/frontend/dist/js/es-kyBgzNq-.js +0 -2
- package/frontend/dist/js/fr-Cw1vut7I.js +0 -2
- package/frontend/dist/js/hi-DWEFC024.js +0 -2
- package/frontend/dist/js/id-DlMHjjy0.js +0 -2
- package/frontend/dist/js/is-B9E_a7dG.js +0 -2
- package/frontend/dist/js/it-DawmVbLC.js +0 -2
- package/frontend/dist/js/ja-DmVvwY3W.js +0 -2
- package/frontend/dist/js/ko-DzOPpkVs.js +0 -2
- package/frontend/dist/js/ms-DpsaOPht.js +0 -2
- package/frontend/dist/js/nl-B1d8W22n.js +0 -2
- package/frontend/dist/js/pl-nCncRVgt.js +0 -2
- package/frontend/dist/js/pt-DHw8fGuu.js +0 -2
- package/frontend/dist/js/ru-Derl_YP_.js +0 -2
- package/frontend/dist/js/sv-pJAfBVo0.js +0 -2
- package/frontend/dist/js/th-C_7fIGnk.js +0 -2
- package/frontend/dist/js/tr-CPqsJvaP.js +0 -2
- package/frontend/dist/js/uk-3o8P_kFi.js +0 -2
- package/frontend/dist/js/vi-Dv6N7OVD.js +0 -2
- package/frontend/dist/js/zh-qg5iqtPi.js +0 -2
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Libre WebUI
|
|
3
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Resource-grant management (IAM-03). Owners (and grant-admins) manage
|
|
19
|
+
* shares on their own resources; a user can always list what is shared
|
|
20
|
+
* with them and leave a share.
|
|
21
|
+
*/
|
|
22
|
+
import express from 'express';
|
|
23
|
+
import rateLimit from '../middleware/sharedRateLimit.js';
|
|
24
|
+
import { authenticate } from '../middleware/auth.js';
|
|
25
|
+
import { createGrant, deleteGrant, listGrantsForActor, listGrantsForResource, ResourceGrantError, } from '../services/resourceGrantService.js';
|
|
26
|
+
import { createLogger } from '../utils/logger.js';
|
|
27
|
+
const router = express.Router();
|
|
28
|
+
const logger = createLogger('access-routes');
|
|
29
|
+
const accessRateLimiter = rateLimit({
|
|
30
|
+
keyPrefix: 'resource-access',
|
|
31
|
+
windowMs: 15 * 60 * 1000,
|
|
32
|
+
max: 240,
|
|
33
|
+
message: { success: false, message: 'Too many requests' },
|
|
34
|
+
standardHeaders: true,
|
|
35
|
+
legacyHeaders: false,
|
|
36
|
+
});
|
|
37
|
+
router.use(accessRateLimiter, authenticate);
|
|
38
|
+
const actorOf = (req) => ({
|
|
39
|
+
userId: req.user.userId,
|
|
40
|
+
role: req.user.role,
|
|
41
|
+
});
|
|
42
|
+
const handleError = (res, error, fallback) => {
|
|
43
|
+
if (error instanceof ResourceGrantError) {
|
|
44
|
+
res
|
|
45
|
+
.status(error.statusCode)
|
|
46
|
+
.json({ success: false, message: error.message });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
logger.error(fallback, error);
|
|
50
|
+
res.status(500).json({ success: false, message: 'Internal server error' });
|
|
51
|
+
};
|
|
52
|
+
const publicGrant = (grant) => ({
|
|
53
|
+
id: grant.id,
|
|
54
|
+
resourceType: grant.resource_type,
|
|
55
|
+
resourceId: grant.resource_id,
|
|
56
|
+
principalType: grant.principal_type,
|
|
57
|
+
principalId: grant.principal_id,
|
|
58
|
+
permission: grant.permission,
|
|
59
|
+
createdAt: new Date(grant.created_at).toISOString(),
|
|
60
|
+
});
|
|
61
|
+
/** Grants on one of the caller's resources. */
|
|
62
|
+
router.get('/grants', async (req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
const { type, id } = req.query;
|
|
65
|
+
if (typeof type !== 'string' || typeof id !== 'string') {
|
|
66
|
+
res
|
|
67
|
+
.status(400)
|
|
68
|
+
.json({ success: false, message: 'type and id are required' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const grants = await listGrantsForResource(actorOf(req), type, id);
|
|
72
|
+
res.json({ success: true, data: grants.map(publicGrant) });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
handleError(res, error, 'Grant list error:');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
/** Share a resource with a user or group. */
|
|
79
|
+
router.post('/grants', async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const { resourceType, resourceId, principalType, principalId, permission } = req.body ?? {};
|
|
82
|
+
for (const [field, value] of Object.entries({
|
|
83
|
+
resourceType,
|
|
84
|
+
resourceId,
|
|
85
|
+
principalType,
|
|
86
|
+
principalId,
|
|
87
|
+
permission,
|
|
88
|
+
})) {
|
|
89
|
+
if (typeof value !== 'string' || !value) {
|
|
90
|
+
res
|
|
91
|
+
.status(400)
|
|
92
|
+
.json({ success: false, message: `${field} is required` });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const grant = await createGrant(actorOf(req), {
|
|
97
|
+
resourceType,
|
|
98
|
+
resourceId,
|
|
99
|
+
principalType,
|
|
100
|
+
principalId,
|
|
101
|
+
permission,
|
|
102
|
+
});
|
|
103
|
+
res.status(201).json({ success: true, data: publicGrant(grant) });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
handleError(res, error, 'Grant create error:');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
/** Remove a grant (owner, grant-admin, or the granted user leaving). */
|
|
110
|
+
router.delete('/grants/:id', async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const deleted = await deleteGrant(actorOf(req), req.params.id);
|
|
113
|
+
if (!deleted) {
|
|
114
|
+
res.status(404).json({ success: false, message: 'Grant not found' });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
res.json({ success: true });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
handleError(res, error, 'Grant delete error:');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
/** Everything shared with the caller, directly or via groups. */
|
|
124
|
+
router.get('/shared-with-me', async (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
const grants = await listGrantsForActor(actorOf(req));
|
|
127
|
+
res.json({ success: true, data: grants.map(publicGrant) });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
handleError(res, error, 'Shared list error:');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
export default router;
|
|
134
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/routes/access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/routes/audit.ts"],"names":[],"mappings":"AAuBA,QAAA,MAAM,MAAM,4CAAmB,CAAC;eAkEjB,MAAM"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Libre WebUI
|
|
3
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import express from 'express';
|
|
18
|
+
import rateLimit from '../middleware/sharedRateLimit.js';
|
|
19
|
+
import { authenticate, requireAdmin } from '../middleware/auth.js';
|
|
20
|
+
import { queryAuditEvents } from '../services/securityAuditService.js';
|
|
21
|
+
import { createLogger } from '../utils/logger.js';
|
|
22
|
+
const router = express.Router();
|
|
23
|
+
const logger = createLogger('audit-routes');
|
|
24
|
+
const auditRateLimiter = rateLimit({
|
|
25
|
+
keyPrefix: 'security-audit',
|
|
26
|
+
windowMs: 15 * 60 * 1000,
|
|
27
|
+
max: 120,
|
|
28
|
+
message: { success: false, message: 'Too many requests' },
|
|
29
|
+
standardHeaders: true,
|
|
30
|
+
legacyHeaders: false,
|
|
31
|
+
});
|
|
32
|
+
router.use(auditRateLimiter, authenticate, requireAdmin);
|
|
33
|
+
/** Query the security audit log (admin only, redacted at write time). */
|
|
34
|
+
router.get('/', async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const { action, actor, result, targetType, before, after, limit } = req.query;
|
|
37
|
+
const parseEpoch = (value) => {
|
|
38
|
+
if (typeof value !== 'string' || !value)
|
|
39
|
+
return undefined;
|
|
40
|
+
const asNumber = Number(value);
|
|
41
|
+
if (Number.isFinite(asNumber) && asNumber > 0)
|
|
42
|
+
return asNumber;
|
|
43
|
+
const asDate = Date.parse(value);
|
|
44
|
+
return Number.isFinite(asDate) ? asDate : undefined;
|
|
45
|
+
};
|
|
46
|
+
const isResult = (value) => value === 'success' || value === 'denied' || value === 'failure';
|
|
47
|
+
const events = await queryAuditEvents({
|
|
48
|
+
...(typeof action === 'string' && action ? { action } : {}),
|
|
49
|
+
...(typeof actor === 'string' && actor ? { actorUserId: actor } : {}),
|
|
50
|
+
...(isResult(result) ? { result } : {}),
|
|
51
|
+
...(typeof targetType === 'string' && targetType ? { targetType } : {}),
|
|
52
|
+
...(parseEpoch(before) !== undefined
|
|
53
|
+
? { before: parseEpoch(before) }
|
|
54
|
+
: {}),
|
|
55
|
+
...(parseEpoch(after) !== undefined ? { after: parseEpoch(after) } : {}),
|
|
56
|
+
...(typeof limit === 'string' && Number.isFinite(Number(limit))
|
|
57
|
+
? { limit: Number(limit) }
|
|
58
|
+
: {}),
|
|
59
|
+
});
|
|
60
|
+
res.json({
|
|
61
|
+
success: true,
|
|
62
|
+
data: events.map(event => ({
|
|
63
|
+
id: event.id,
|
|
64
|
+
occurredAt: new Date(event.occurred_at).toISOString(),
|
|
65
|
+
actorUserId: event.actor_user_id,
|
|
66
|
+
actorKind: event.actor_kind,
|
|
67
|
+
action: event.action,
|
|
68
|
+
targetType: event.target_type,
|
|
69
|
+
targetId: event.target_id,
|
|
70
|
+
result: event.result,
|
|
71
|
+
requestId: event.request_id,
|
|
72
|
+
details: event.details ? JSON.parse(event.details) : null,
|
|
73
|
+
})),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
logger.error('Audit query error:', error);
|
|
78
|
+
res.status(500).json({ success: false, message: 'Internal server error' });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
export default router;
|
|
82
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/routes/audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AAgEA,QAAA,MAAM,MAAM,4CAAmB,CAAC;eAs/BjB,MAAM"}
|
|
@@ -22,11 +22,15 @@ import { authService } from '../services/authService.js';
|
|
|
22
22
|
import { authenticate, requireAdmin, } from '../middleware/auth.js';
|
|
23
23
|
import { encryptionService } from '../services/encryptionService.js';
|
|
24
24
|
import { turnstileService } from '../services/turnstileService.js';
|
|
25
|
-
import { beginOAuthFlow, consumeOAuthSessionCookie, consumeOAuthState, setOAuthSessionCookie, } from '../services/oauthSecurity.js';
|
|
25
|
+
import { beginOAuthFlow, beginOAuthFlowWithPayload, consumeOAuthSessionCookie, consumeOAuthState, consumeOAuthStatePayload, setOAuthSessionCookie, } from '../services/oauthSecurity.js';
|
|
26
26
|
import { validatePasswordStrength } from '../utils/hash.js';
|
|
27
27
|
import { createLogger } from '../utils/logger.js';
|
|
28
28
|
import { WebSocketTicketRateLimitError, websocketTicketService, } from '../services/websocketTicketService.js';
|
|
29
29
|
import { coordinatedRateLimit } from '../middleware/coordinatedRateLimit.js';
|
|
30
|
+
import { findSessionById, listSessionsForUser, revokeAllAuthSessions, revokeAuthSession, } from '../services/authSessionService.js';
|
|
31
|
+
import { createApiToken, findTokenById, isApiTokenScope, listTokensForUser, revokeApiToken, toPublicToken, } from '../services/apiTokenService.js';
|
|
32
|
+
import { hashClientIp, recordAuditEvent, } from '../services/securityAuditService.js';
|
|
33
|
+
import { OidcError, oidcOAuthService } from '../services/simpleOidcOAuth.js';
|
|
30
34
|
const router = express.Router();
|
|
31
35
|
const logger = createLogger('auth-routes');
|
|
32
36
|
// Fallback frontend URL for OAuth redirects
|
|
@@ -100,7 +104,7 @@ router.post('/websocket-ticket', generalAuthRateLimiter, authenticate, async (re
|
|
|
100
104
|
}
|
|
101
105
|
const sessionExpiresAt = (req.user?.exp ?? 0) * 1000;
|
|
102
106
|
try {
|
|
103
|
-
const ticket = await websocketTicketService.issue(req.user.userId, sessionExpiresAt, audience, audience === 'work-terminal' ? taskId.trim() : undefined);
|
|
107
|
+
const ticket = await websocketTicketService.issue(req.user.userId, sessionExpiresAt, audience, audience === 'work-terminal' ? taskId.trim() : undefined, req.auth?.kind === 'session' ? req.auth.sessionId : undefined);
|
|
104
108
|
res.json({ success: true, data: ticket });
|
|
105
109
|
}
|
|
106
110
|
catch (error) {
|
|
@@ -136,14 +140,31 @@ router.post('/login', loginRateLimiter, async (req, res) => {
|
|
|
136
140
|
});
|
|
137
141
|
return;
|
|
138
142
|
}
|
|
139
|
-
const result = await authService.login(username, password
|
|
143
|
+
const result = await authService.login(username, password, {
|
|
144
|
+
kind: 'password',
|
|
145
|
+
ip: getClientIp(req),
|
|
146
|
+
userAgent: req.headers['user-agent'],
|
|
147
|
+
});
|
|
140
148
|
if (!result) {
|
|
149
|
+
void recordAuditEvent({
|
|
150
|
+
action: 'auth.login',
|
|
151
|
+
result: 'failure',
|
|
152
|
+
ipHash: hashClientIp(getClientIp(req)),
|
|
153
|
+
details: { username: String(username).slice(0, 64) },
|
|
154
|
+
});
|
|
141
155
|
res.status(401).json({
|
|
142
156
|
success: false,
|
|
143
157
|
message: 'Invalid credentials',
|
|
144
158
|
});
|
|
145
159
|
return;
|
|
146
160
|
}
|
|
161
|
+
void recordAuditEvent({
|
|
162
|
+
action: 'auth.login',
|
|
163
|
+
result: result.status === 'authenticated' ? 'success' : 'denied',
|
|
164
|
+
actorUserId: result.user.id,
|
|
165
|
+
ipHash: hashClientIp(getClientIp(req)),
|
|
166
|
+
details: { method: 'password', status: result.status },
|
|
167
|
+
});
|
|
147
168
|
if (result.status === 'pending') {
|
|
148
169
|
res.status(403).json({
|
|
149
170
|
success: false,
|
|
@@ -175,9 +196,16 @@ router.post('/login', loginRateLimiter, async (req, res) => {
|
|
|
175
196
|
*/
|
|
176
197
|
router.post('/logout', generalAuthRateLimiter, authenticate, async (req, res) => {
|
|
177
198
|
try {
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
199
|
+
// Revoke the server-side session so the JWT stops working everywhere,
|
|
200
|
+
// not just in this browser.
|
|
201
|
+
if (req.auth?.kind === 'session' && req.user) {
|
|
202
|
+
await revokeAuthSession(req.auth.sessionId, req.user.userId);
|
|
203
|
+
}
|
|
204
|
+
void recordAuditEvent({
|
|
205
|
+
action: 'auth.logout',
|
|
206
|
+
result: 'success',
|
|
207
|
+
actorUserId: req.user?.userId ?? null,
|
|
208
|
+
});
|
|
181
209
|
res.json({
|
|
182
210
|
success: true,
|
|
183
211
|
message: 'Logged out successfully',
|
|
@@ -191,6 +219,167 @@ router.post('/logout', generalAuthRateLimiter, authenticate, async (req, res) =>
|
|
|
191
219
|
});
|
|
192
220
|
}
|
|
193
221
|
});
|
|
222
|
+
/** List this account's sessions, newest activity first. */
|
|
223
|
+
router.get('/sessions', generalAuthRateLimiter, authenticate, async (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
const sessions = await listSessionsForUser(req.user.userId);
|
|
226
|
+
const currentSessionId = req.auth?.kind === 'session' ? req.auth.sessionId : null;
|
|
227
|
+
res.json({
|
|
228
|
+
success: true,
|
|
229
|
+
data: sessions.map(session => ({
|
|
230
|
+
id: session.id,
|
|
231
|
+
kind: session.kind,
|
|
232
|
+
userAgent: session.user_agent,
|
|
233
|
+
createdAt: new Date(session.created_at).toISOString(),
|
|
234
|
+
lastSeenAt: new Date(session.last_seen_at).toISOString(),
|
|
235
|
+
expiresAt: new Date(session.expires_at).toISOString(),
|
|
236
|
+
revokedAt: session.revoked_at
|
|
237
|
+
? new Date(session.revoked_at).toISOString()
|
|
238
|
+
: null,
|
|
239
|
+
current: session.id === currentSessionId,
|
|
240
|
+
})),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
logger.error('Session inventory error:', error);
|
|
245
|
+
res
|
|
246
|
+
.status(500)
|
|
247
|
+
.json({ success: false, message: 'Internal server error' });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
/** Revoke every other session ("sign out everywhere else"). */
|
|
251
|
+
router.post('/sessions/revoke-others', generalAuthRateLimiter, authenticate, async (req, res) => {
|
|
252
|
+
try {
|
|
253
|
+
const currentSessionId = req.auth?.kind === 'session' ? req.auth.sessionId : undefined;
|
|
254
|
+
const revoked = await revokeAllAuthSessions(req.user.userId, req.user.userId, currentSessionId);
|
|
255
|
+
void recordAuditEvent({
|
|
256
|
+
action: 'session.revoke-others',
|
|
257
|
+
result: 'success',
|
|
258
|
+
actorUserId: req.user.userId,
|
|
259
|
+
details: { revokedCount: revoked.length },
|
|
260
|
+
});
|
|
261
|
+
res.json({ success: true, data: { revokedCount: revoked.length } });
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
logger.error('Session revocation error:', error);
|
|
265
|
+
res
|
|
266
|
+
.status(500)
|
|
267
|
+
.json({ success: false, message: 'Internal server error' });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
/** Revoke one session by id (own sessions only; admins may revoke any). */
|
|
271
|
+
router.delete('/sessions/:id', generalAuthRateLimiter, authenticate, async (req, res) => {
|
|
272
|
+
try {
|
|
273
|
+
const session = await findSessionById(req.params.id);
|
|
274
|
+
const isOwn = session?.user_id === req.user.userId;
|
|
275
|
+
const isAdmin = req.user.role === 'admin';
|
|
276
|
+
if (!session || (!isOwn && !isAdmin)) {
|
|
277
|
+
res.status(404).json({ success: false, message: 'Session not found' });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const revoked = await revokeAuthSession(session.id, req.user.userId);
|
|
281
|
+
void recordAuditEvent({
|
|
282
|
+
action: 'session.revoke',
|
|
283
|
+
result: 'success',
|
|
284
|
+
actorUserId: req.user.userId,
|
|
285
|
+
targetType: 'auth-session',
|
|
286
|
+
targetId: session.id,
|
|
287
|
+
details: { targetUserId: session.user_id },
|
|
288
|
+
});
|
|
289
|
+
res.json({ success: true, data: { revoked } });
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
logger.error('Session revocation error:', error);
|
|
293
|
+
res
|
|
294
|
+
.status(500)
|
|
295
|
+
.json({ success: false, message: 'Internal server error' });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
/** List this account's API tokens (hashes never leave the server). */
|
|
299
|
+
router.get('/tokens', generalAuthRateLimiter, authenticate, async (req, res) => {
|
|
300
|
+
try {
|
|
301
|
+
const tokens = await listTokensForUser(req.user.userId);
|
|
302
|
+
res.json({ success: true, data: tokens.map(toPublicToken) });
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
logger.error('Token list error:', error);
|
|
306
|
+
res
|
|
307
|
+
.status(500)
|
|
308
|
+
.json({ success: false, message: 'Internal server error' });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
/** Create a scoped API token; the secret is returned exactly once. */
|
|
312
|
+
router.post('/tokens', generalAuthRateLimiter, authenticate, async (req, res) => {
|
|
313
|
+
try {
|
|
314
|
+
if (req.auth?.kind === 'api-token') {
|
|
315
|
+
res.status(403).json({
|
|
316
|
+
success: false,
|
|
317
|
+
message: 'API tokens cannot create other tokens',
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const { name, scopes, expiresInDays } = req.body ?? {};
|
|
322
|
+
const requestedScopes = Array.isArray(scopes)
|
|
323
|
+
? scopes.filter(isApiTokenScope)
|
|
324
|
+
: [];
|
|
325
|
+
if (requestedScopes.includes('admin') && req.user.role !== 'admin') {
|
|
326
|
+
res.status(403).json({
|
|
327
|
+
success: false,
|
|
328
|
+
message: 'Only administrators may mint admin-scoped tokens',
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const created = await createApiToken(req.user.userId, {
|
|
333
|
+
name: typeof name === 'string' ? name : '',
|
|
334
|
+
scopes: requestedScopes,
|
|
335
|
+
...(typeof expiresInDays === 'number' ? { expiresInDays } : {}),
|
|
336
|
+
});
|
|
337
|
+
void recordAuditEvent({
|
|
338
|
+
action: 'token.create',
|
|
339
|
+
result: 'success',
|
|
340
|
+
actorUserId: req.user.userId,
|
|
341
|
+
targetType: 'api-token',
|
|
342
|
+
targetId: created.record.id,
|
|
343
|
+
details: { name: created.record.name, scopes: requestedScopes },
|
|
344
|
+
});
|
|
345
|
+
res.status(201).json({
|
|
346
|
+
success: true,
|
|
347
|
+
data: { token: created.token, record: toPublicToken(created.record) },
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
const message = error instanceof Error ? error.message : 'Internal server error';
|
|
352
|
+
res.status(400).json({ success: false, message });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
/** Revoke an API token (own tokens; admins may revoke any). */
|
|
356
|
+
router.delete('/tokens/:id', generalAuthRateLimiter, authenticate, async (req, res) => {
|
|
357
|
+
try {
|
|
358
|
+
const record = await findTokenById(req.params.id);
|
|
359
|
+
const isOwn = record?.user_id === req.user.userId;
|
|
360
|
+
const isAdmin = req.user.role === 'admin';
|
|
361
|
+
if (!record || (!isOwn && !isAdmin)) {
|
|
362
|
+
res.status(404).json({ success: false, message: 'Token not found' });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const revoked = await revokeApiToken(record.id);
|
|
366
|
+
void recordAuditEvent({
|
|
367
|
+
action: 'token.revoke',
|
|
368
|
+
result: 'success',
|
|
369
|
+
actorUserId: req.user.userId,
|
|
370
|
+
targetType: 'api-token',
|
|
371
|
+
targetId: record.id,
|
|
372
|
+
details: { targetUserId: record.user_id },
|
|
373
|
+
});
|
|
374
|
+
res.json({ success: true, data: { revoked } });
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
logger.error('Token revocation error:', error);
|
|
378
|
+
res
|
|
379
|
+
.status(500)
|
|
380
|
+
.json({ success: false, message: 'Internal server error' });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
194
383
|
/**
|
|
195
384
|
* Verify token endpoint
|
|
196
385
|
*/
|
|
@@ -312,7 +501,11 @@ router.post('/signup', signupRateLimiter, async (req, res) => {
|
|
|
312
501
|
});
|
|
313
502
|
return;
|
|
314
503
|
}
|
|
315
|
-
const result = await authService.signup(username, password, email
|
|
504
|
+
const result = await authService.signup(username, password, email, {
|
|
505
|
+
kind: 'signup',
|
|
506
|
+
ip: getClientIp(req),
|
|
507
|
+
userAgent: req.headers['user-agent'],
|
|
508
|
+
});
|
|
316
509
|
if (!result) {
|
|
317
510
|
res.status(500).json({
|
|
318
511
|
success: false,
|
|
@@ -320,6 +513,13 @@ router.post('/signup', signupRateLimiter, async (req, res) => {
|
|
|
320
513
|
});
|
|
321
514
|
return;
|
|
322
515
|
}
|
|
516
|
+
void recordAuditEvent({
|
|
517
|
+
action: 'auth.signup',
|
|
518
|
+
result: 'success',
|
|
519
|
+
actorUserId: result.user.id,
|
|
520
|
+
ipHash: hashClientIp(getClientIp(req)),
|
|
521
|
+
details: { status: result.status },
|
|
522
|
+
});
|
|
323
523
|
const systemInfo = await authService.getSystemInfo();
|
|
324
524
|
if (result.status === 'pending') {
|
|
325
525
|
res.status(202).json({
|
|
@@ -397,8 +597,19 @@ router.get('/oauth/github/callback', generalAuthRateLimiter, async (req, res) =>
|
|
|
397
597
|
if (user.status !== 'active') {
|
|
398
598
|
return res.redirect(pendingApprovalRedirect());
|
|
399
599
|
}
|
|
400
|
-
//
|
|
401
|
-
const token = authService.
|
|
600
|
+
// Issue a revocable session-bound JWT
|
|
601
|
+
const token = await authService.issueSession(user, {
|
|
602
|
+
kind: 'oauth:github',
|
|
603
|
+
ip: getClientIp(req),
|
|
604
|
+
userAgent: req.headers['user-agent'],
|
|
605
|
+
});
|
|
606
|
+
void recordAuditEvent({
|
|
607
|
+
action: 'auth.login',
|
|
608
|
+
result: 'success',
|
|
609
|
+
actorUserId: user.id,
|
|
610
|
+
ipHash: hashClientIp(getClientIp(req)),
|
|
611
|
+
details: { method: 'oauth:github' },
|
|
612
|
+
});
|
|
402
613
|
logger.debug('GitHub OAuth successful for user:', user.username);
|
|
403
614
|
setOAuthSessionCookie(req, res, token);
|
|
404
615
|
res.redirect(frontendRedirect('?auth=success'));
|
|
@@ -462,8 +673,19 @@ router.get('/oauth/huggingface/callback', generalAuthRateLimiter, async (req, re
|
|
|
462
673
|
if (user.status !== 'active') {
|
|
463
674
|
return res.redirect(pendingApprovalRedirect());
|
|
464
675
|
}
|
|
465
|
-
//
|
|
466
|
-
const token = authService.
|
|
676
|
+
// Issue a revocable session-bound JWT
|
|
677
|
+
const token = await authService.issueSession(user, {
|
|
678
|
+
kind: 'oauth:huggingface',
|
|
679
|
+
ip: getClientIp(req),
|
|
680
|
+
userAgent: req.headers['user-agent'],
|
|
681
|
+
});
|
|
682
|
+
void recordAuditEvent({
|
|
683
|
+
action: 'auth.login',
|
|
684
|
+
result: 'success',
|
|
685
|
+
actorUserId: user.id,
|
|
686
|
+
ipHash: hashClientIp(getClientIp(req)),
|
|
687
|
+
details: { method: 'oauth:huggingface' },
|
|
688
|
+
});
|
|
467
689
|
logger.debug('Hugging Face OAuth successful for user:', user.username);
|
|
468
690
|
setOAuthSessionCookie(req, res, token);
|
|
469
691
|
res.redirect(frontendRedirect('?auth=success'));
|
|
@@ -479,6 +701,95 @@ router.get('/oauth/huggingface/callback', generalAuthRateLimiter, async (req, re
|
|
|
479
701
|
router.get('/oauth/huggingface/status', generalAuthRateLimiter, (req, res) => {
|
|
480
702
|
res.json({ configured: isHuggingFaceConfigured });
|
|
481
703
|
});
|
|
704
|
+
/**
|
|
705
|
+
* Generic OIDC - Start authentication (PKCE + state + nonce)
|
|
706
|
+
*/
|
|
707
|
+
router.get('/oauth/oidc', generalAuthRateLimiter, async (req, res) => {
|
|
708
|
+
if (!oidcOAuthService.isConfigured()) {
|
|
709
|
+
return res.status(404).json({ error: 'OIDC is not configured' });
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
const pkce = oidcOAuthService.createPkcePair();
|
|
713
|
+
const nonce = oidcOAuthService.createNonce();
|
|
714
|
+
const state = beginOAuthFlowWithPayload(req, res, 'oidc', {
|
|
715
|
+
verifier: pkce.verifier,
|
|
716
|
+
nonce,
|
|
717
|
+
});
|
|
718
|
+
const authUrl = await oidcOAuthService.getAuthUrl(state, nonce, pkce.challenge);
|
|
719
|
+
res.redirect(authUrl);
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
logger.error('OIDC start error:', error);
|
|
723
|
+
res.redirect(oauthErrorRedirect('oauth_failed'));
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
/**
|
|
727
|
+
* Generic OIDC - Handle callback: verify state, exchange with PKCE,
|
|
728
|
+
* validate the ID token against JWKS, then issue a session-bound JWT.
|
|
729
|
+
*/
|
|
730
|
+
router.get('/oauth/oidc/callback', generalAuthRateLimiter, async (req, res) => {
|
|
731
|
+
try {
|
|
732
|
+
if (!oidcOAuthService.isConfigured()) {
|
|
733
|
+
return res.redirect(oauthErrorRedirect('oauth_not_configured'));
|
|
734
|
+
}
|
|
735
|
+
const { code, state } = req.query;
|
|
736
|
+
const payload = consumeOAuthStatePayload(req, res, 'oidc', typeof state === 'string' ? state : '');
|
|
737
|
+
if (!payload ||
|
|
738
|
+
!payload.verifier ||
|
|
739
|
+
!payload.nonce ||
|
|
740
|
+
!code ||
|
|
741
|
+
typeof code !== 'string') {
|
|
742
|
+
return res.redirect(oauthErrorRedirect('oauth_failed'));
|
|
743
|
+
}
|
|
744
|
+
const exchanged = await oidcOAuthService.exchangeCode(code, payload.verifier);
|
|
745
|
+
const claims = await oidcOAuthService.verifyIdToken(exchanged.idToken, payload.nonce);
|
|
746
|
+
const user = await oidcOAuthService.processClaims(claims);
|
|
747
|
+
if (user.status !== 'active') {
|
|
748
|
+
return res.redirect(pendingApprovalRedirect());
|
|
749
|
+
}
|
|
750
|
+
const token = await authService.issueSession(user, {
|
|
751
|
+
kind: 'oidc',
|
|
752
|
+
ip: getClientIp(req),
|
|
753
|
+
userAgent: req.headers['user-agent'],
|
|
754
|
+
});
|
|
755
|
+
void recordAuditEvent({
|
|
756
|
+
action: 'auth.login',
|
|
757
|
+
result: 'success',
|
|
758
|
+
actorUserId: user.id,
|
|
759
|
+
ipHash: hashClientIp(getClientIp(req)),
|
|
760
|
+
details: { method: 'oidc' },
|
|
761
|
+
});
|
|
762
|
+
setOAuthSessionCookie(req, res, token);
|
|
763
|
+
res.redirect(frontendRedirect('?auth=success'));
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
logger.error('OIDC callback error:', error);
|
|
767
|
+
void recordAuditEvent({
|
|
768
|
+
action: 'auth.login',
|
|
769
|
+
result: 'failure',
|
|
770
|
+
ipHash: hashClientIp(getClientIp(req)),
|
|
771
|
+
details: {
|
|
772
|
+
method: 'oidc',
|
|
773
|
+
reason: error instanceof OidcError ? error.code : 'unknown',
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
const reason = error instanceof OidcError &&
|
|
777
|
+
(error.code === 'registration-disabled' || error.code === 'email-in-use')
|
|
778
|
+
? error.code.replace(/-/g, '_')
|
|
779
|
+
: 'oauth_failed';
|
|
780
|
+
res.redirect(oauthErrorRedirect(reason));
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
/**
|
|
784
|
+
* Check if generic OIDC is configured (includes the login button label)
|
|
785
|
+
*/
|
|
786
|
+
router.get('/oauth/oidc/status', generalAuthRateLimiter, (req, res) => {
|
|
787
|
+
const configured = oidcOAuthService.isConfigured();
|
|
788
|
+
res.json({
|
|
789
|
+
configured,
|
|
790
|
+
...(configured ? { displayName: oidcOAuthService.displayName } : {}),
|
|
791
|
+
});
|
|
792
|
+
});
|
|
482
793
|
/**
|
|
483
794
|
* Exchange the short-lived HttpOnly OAuth transfer cookie for the application's
|
|
484
795
|
* normal bearer-token response. The cookie is cleared even when it is invalid.
|