@promptbook/cli 0.112.0-101 → 0.112.0-103
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/apps/agents-server/package.json +1 -1
- package/apps/agents-server/scripts/prerender-homepage.js +76 -1
- package/apps/agents-server/src/app/actions.ts +0 -6
- package/apps/agents-server/src/app/admin/about/page.tsx +1 -1
- package/apps/agents-server/src/app/admin/image-generator-test/ImageAttachmentsEditor.tsx +11 -6
- package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +365 -0
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +13 -15
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +3 -3
- package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +13 -14
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +12 -3
- package/apps/agents-server/src/app/admin/usage/UsageClientTimelineChart.tsx +1 -1
- package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +21 -14
- package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatPageLayout.tsx +2 -2
- package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatSidebarDefault.tsx +11 -7
- package/apps/agents-server/src/app/api/admin/cli-access/route.ts +27 -123
- package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +33 -125
- package/apps/agents-server/src/app/api/auth/login/route.ts +0 -10
- package/apps/agents-server/src/app/api/auth/shibboleth/acs/route.ts +77 -57
- package/apps/agents-server/src/app/api/auth/shibboleth/login/route.ts +57 -33
- package/apps/agents-server/src/app/api/auth/shibboleth/metadata/route.ts +4 -29
- package/apps/agents-server/src/app/api/auth/shibboleth/status/route.ts +17 -0
- package/apps/agents-server/src/app/api/upload/route.ts +148 -209
- package/apps/agents-server/src/app/api/users/[username]/route.ts +1 -1
- package/apps/agents-server/src/app/api/users/route.ts +5 -5
- package/apps/agents-server/src/app/dashboard/page.tsx +1 -1
- package/apps/agents-server/src/app/docs/[docId]/page.tsx +1 -1
- package/apps/agents-server/src/app/docs/page.tsx +1 -1
- package/apps/agents-server/src/app/globals.css +100 -0
- package/apps/agents-server/src/app/layout.tsx +7 -0
- package/apps/agents-server/src/app/recycle-bin/page.tsx +1 -1
- package/apps/agents-server/src/app/system/settings/KeybindingsSettingsClient.tsx +13 -7
- package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +29 -1
- package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +3 -3
- package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +8 -2
- package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +4 -4
- package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +9 -9
- package/apps/agents-server/src/components/Footer/Footer.tsx +7 -7
- package/apps/agents-server/src/components/Header/Header.tsx +24 -4
- package/apps/agents-server/src/components/Header/HeaderTypes.ts +6 -0
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +51 -1
- package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
- package/apps/agents-server/src/components/Homepage/Section.tsx +3 -1
- package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +12 -1
- package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +100 -149
- package/apps/agents-server/src/components/Skeleton/ConsolePageLoadingSkeleton.tsx +1 -1
- package/apps/agents-server/src/components/Skeleton/DocumentationRouteLoadingSkeleton.tsx +1 -1
- package/apps/agents-server/src/components/Skeleton/HomepageLoadingSkeleton.tsx +1 -1
- package/apps/agents-server/src/components/UsersList/UsersList.tsx +20 -4
- package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +3 -0
- package/apps/agents-server/src/constants/shibbolethAuth.ts +139 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +54 -80
- package/apps/agents-server/src/database/migrate.ts +30 -1
- package/apps/agents-server/src/database/migrations/2026-06-0100-shibboleth-auth.sql +136 -0
- package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +88 -36
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +4 -2
- package/apps/agents-server/src/languages/translations/czech.yaml +4 -2
- package/apps/agents-server/src/languages/translations/english.yaml +5 -3
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +54 -11
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +18 -2
- package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +6 -5
- package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +5 -0
- package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +11 -0
- package/apps/agents-server/src/utils/createAdminTerminalRouteHandlers.ts +264 -0
- package/apps/agents-server/src/utils/shareTargetPayloads.ts +19 -66
- package/apps/agents-server/src/utils/shibbolethAuthentication.ts +729 -621
- package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +19 -28
- package/apps/agents-server/src/utils/upload/uploadFileToServer.ts +113 -0
- package/esm/index.es.js +194 -35
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
- package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
- package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
- package/esm/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +63 -4
- package/src/other/templates/getTemplatesPipelineCollection.ts +730 -739
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +194 -35
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
- package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
- package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
- package/umd/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/app/api/auth/methods/route.ts +0 -44
- package/apps/agents-server/src/constants/authenticationMethods.ts +0 -74
- package/apps/agents-server/src/constants/shibbolethAuthentication.ts +0 -107
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"dev": "next dev -p 4440",
|
|
8
8
|
"test": "npm run lint && npm run test-build && npm run test-e2e",
|
|
9
9
|
"test-e2e": "playwright test",
|
|
10
|
-
"pretest-build": "npx kill-port 4021",
|
|
10
|
+
"pretest-build": "npx kill-port 4021 4440 || exit 0",
|
|
11
11
|
"test-build": "node -r ./scripts/ignore-kill-eperm.js ../../node_modules/next/dist/bin/next build && node ./scripts/prerender-homepage.js",
|
|
12
12
|
"build": "node -r ./scripts/ignore-kill-eperm.js ../../node_modules/next/dist/bin/next build && node ./scripts/prerender-homepage.js",
|
|
13
13
|
"start": "next start -p 4440",
|
|
@@ -45,6 +45,10 @@ const WAIT_TIMEOUT_MS = 15000;
|
|
|
45
45
|
* Timeout for the final homepage download once the server is confirmed ready.
|
|
46
46
|
*/
|
|
47
47
|
const HOME_REQUEST_TIMEOUT_MS = 30000;
|
|
48
|
+
/**
|
|
49
|
+
* Timeout for stopping the temporary production server before escalating cleanup.
|
|
50
|
+
*/
|
|
51
|
+
const STOP_SERVER_TIMEOUT_MS = 10000;
|
|
48
52
|
/**
|
|
49
53
|
* When enabled, homepage prerender failures abort the whole build.
|
|
50
54
|
*/
|
|
@@ -136,6 +140,65 @@ function waitForServerReady(serverProcess) {
|
|
|
136
140
|
});
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Waits briefly for the temporary production server to exit after cleanup starts.
|
|
145
|
+
*
|
|
146
|
+
* @param serverProcess - Spawned `next start` child process.
|
|
147
|
+
* @param gracefulExit - Promise resolved by the child `exit` event.
|
|
148
|
+
* @returns Whether the child exited within the timeout window.
|
|
149
|
+
*/
|
|
150
|
+
async function waitForServerStop(serverProcess, gracefulExit) {
|
|
151
|
+
if (serverProcess.exitCode !== null || serverProcess.signalCode !== null) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return await Promise.race([
|
|
156
|
+
gracefulExit.then(() => true),
|
|
157
|
+
new Promise((resolve) => setTimeout(() => resolve(false), STOP_SERVER_TIMEOUT_MS)),
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Force-stops the temporary production server when normal shutdown does not finish in time.
|
|
163
|
+
*
|
|
164
|
+
* Windows can leave `next start` alive after an `EPERM` kill attempt, so use `taskkill`
|
|
165
|
+
* against the exact PID before giving up on cleanup.
|
|
166
|
+
*
|
|
167
|
+
* @param serverProcess - Spawned `next start` child process.
|
|
168
|
+
*/
|
|
169
|
+
async function forceStopServer(serverProcess) {
|
|
170
|
+
if (serverProcess.exitCode !== null || serverProcess.signalCode !== null) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (process.platform !== 'win32') {
|
|
175
|
+
try {
|
|
176
|
+
serverProcess.kill('SIGKILL');
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!isIgnorableStopServerError(error)) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
const taskKillProcess = spawn('taskkill', ['/PID', String(serverProcess.pid), '/T', '/F'], {
|
|
187
|
+
stdio: 'ignore',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
taskKillProcess.on('error', reject);
|
|
191
|
+
taskKillProcess.on('exit', (code) => {
|
|
192
|
+
if (code === 0 || serverProcess.exitCode !== null || serverProcess.signalCode !== null) {
|
|
193
|
+
resolve();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
reject(new Error(`Failed to stop temporary production server process ${serverProcess.pid} with taskkill.`));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
139
202
|
/**
|
|
140
203
|
* Runs the production server for the built app, captures `/`, and keeps the HTML.
|
|
141
204
|
*/
|
|
@@ -196,7 +259,19 @@ async function prerenderHomePage() {
|
|
|
196
259
|
} finally {
|
|
197
260
|
detachStopServerHandlers();
|
|
198
261
|
stopServer();
|
|
199
|
-
|
|
262
|
+
|
|
263
|
+
if (!(await waitForServerStop(serverProcess, gracefulExit))) {
|
|
264
|
+
console.warn(
|
|
265
|
+
`Timed out waiting ${STOP_SERVER_TIMEOUT_MS}ms for the temporary production server to stop. Trying a forceful shutdown.`,
|
|
266
|
+
);
|
|
267
|
+
await forceStopServer(serverProcess);
|
|
268
|
+
|
|
269
|
+
if (!(await waitForServerStop(serverProcess, gracefulExit))) {
|
|
270
|
+
console.warn(
|
|
271
|
+
`Timed out waiting ${STOP_SERVER_TIMEOUT_MS}ms for the temporary production server after forceful shutdown.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
200
275
|
}
|
|
201
276
|
}
|
|
202
277
|
|
|
@@ -6,7 +6,6 @@ import { revalidatePath } from 'next/cache';
|
|
|
6
6
|
import { string_agent_permanent_id } from '../../../../src/types/typeAliases';
|
|
7
7
|
import { DEFAULT_NAME_POOL, NAME_POOL_METADATA_KEY, parseNamePool } from '../constants/namePool';
|
|
8
8
|
import { NEW_AGENT_WIZZARD_METADATA_KEY, parseNewAgentWizardMode } from '../constants/newAgentWizard';
|
|
9
|
-
import { AUTHENTICATION_METHODS_METADATA_KEY, isAuthenticationMethodEnabled } from '../constants/authenticationMethods';
|
|
10
9
|
import { getMetadata } from '../database/getMetadata';
|
|
11
10
|
import { $provideAgentCollectionForServer } from '../tools/$provideAgentCollectionForServer';
|
|
12
11
|
import { type AgentVisibility, parseAgentVisibility } from '../utils/agentVisibility';
|
|
@@ -106,11 +105,6 @@ export async function loginAction(formData: FormData) {
|
|
|
106
105
|
|
|
107
106
|
console.info(`Login attempt for user: ${username}`);
|
|
108
107
|
|
|
109
|
-
if (!isAuthenticationMethodEnabled(await getMetadata(AUTHENTICATION_METHODS_METADATA_KEY), 'PASSWORD')) {
|
|
110
|
-
console.info('Password login rejected because PASSWORD authentication is disabled in metadata.');
|
|
111
|
-
return { success: false, message: 'Password login is disabled on this server.' };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
108
|
const user = await authenticateUser(username, password);
|
|
115
109
|
|
|
116
110
|
if (user) {
|
|
@@ -413,7 +413,7 @@ export default async function AdminAboutPage() {
|
|
|
413
413
|
];
|
|
414
414
|
|
|
415
415
|
return (
|
|
416
|
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
|
|
416
|
+
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900">
|
|
417
417
|
<div className="container mx-auto px-4 py-16">
|
|
418
418
|
<Card className="mb-10 space-y-4">
|
|
419
419
|
<div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { upload } from '@vercel/blob/client';
|
|
2
1
|
import { useCallback, useRef, useState, type ChangeEvent } from 'react';
|
|
3
2
|
import { showAlert } from '../../../components/AsyncDialogs/asyncDialogs';
|
|
4
3
|
import { getSafeCdnPath } from '../../../utils/cdn/utils/getSafeCdnPath';
|
|
5
4
|
import { normalizeUploadFilename } from '../../../utils/normalization/normalizeUploadFilename';
|
|
5
|
+
import { uploadFileToServer } from '../../../utils/upload/uploadFileToServer';
|
|
6
6
|
import type { UseImageGeneratorTestState } from './useImageGeneratorTestState';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -36,17 +36,22 @@ async function uploadImageAttachment(
|
|
|
36
36
|
file: File,
|
|
37
37
|
): Promise<UseImageGeneratorTestState['prompts'][number]['attachments'][number]> {
|
|
38
38
|
const normalizedFilename = normalizeUploadFilename(file.name);
|
|
39
|
-
const uploadPath = getSafeCdnPath({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const uploadPath = getSafeCdnPath({
|
|
40
|
+
pathname: normalizedFilename,
|
|
41
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX,
|
|
42
|
+
});
|
|
43
|
+
const uploadResult = await uploadFileToServer({
|
|
44
|
+
file,
|
|
45
|
+
pathname: uploadPath,
|
|
46
|
+
purpose: 'IMAGE_GENERATOR_TEST_ATTACHMENT',
|
|
47
|
+
contentType: file.type,
|
|
43
48
|
});
|
|
44
49
|
|
|
45
50
|
return {
|
|
46
51
|
id: createAttachmentIdentifier(),
|
|
47
52
|
name: file.name,
|
|
48
53
|
type: file.type,
|
|
49
|
-
url:
|
|
54
|
+
url: uploadResult.url,
|
|
50
55
|
};
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { headers } from 'next/headers';
|
|
3
|
+
import { AlertTriangle, CheckCircle2, ExternalLink, XCircle } from 'lucide-react';
|
|
4
|
+
import { $getTableName } from '../../../../database/$getTableName';
|
|
5
|
+
import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
|
|
6
|
+
import { ForbiddenPage } from '../../../../components/ForbiddenPage/ForbiddenPage';
|
|
7
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
8
|
+
import {
|
|
9
|
+
getShibbolethAuthenticationAttemptTableName,
|
|
10
|
+
getShibbolethUserIdentityTableName,
|
|
11
|
+
resolveShibbolethAuthenticationConfiguration,
|
|
12
|
+
type ShibbolethAuthenticationAttemptRow,
|
|
13
|
+
type ShibbolethUserIdentityRow,
|
|
14
|
+
} from '../../../../utils/shibbolethAuthentication';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* User row subset displayed on the Shibboleth dashboard.
|
|
18
|
+
*/
|
|
19
|
+
type DashboardUserRow = {
|
|
20
|
+
readonly id: number;
|
|
21
|
+
readonly username: string;
|
|
22
|
+
readonly email?: string | null;
|
|
23
|
+
readonly displayName?: string | null;
|
|
24
|
+
readonly authenticationProvider?: string | null;
|
|
25
|
+
readonly isAdmin: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Builds an absolute URL for Shibboleth route configuration inside server components.
|
|
30
|
+
*/
|
|
31
|
+
async function getDashboardRequestUrl(): Promise<string> {
|
|
32
|
+
const headerStore = await headers();
|
|
33
|
+
const forwardedProtocol = headerStore.get('x-forwarded-proto')?.split(',')[0]?.trim() || 'http';
|
|
34
|
+
const forwardedHost = headerStore.get('x-forwarded-host') || headerStore.get('host') || 'localhost';
|
|
35
|
+
return `${forwardedProtocol}://${forwardedHost}/admin/login-methods/shibboleth`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Loads recent Shibboleth authentication attempts for the dashboard.
|
|
40
|
+
*/
|
|
41
|
+
async function loadShibbolethAuthenticationAttempts(): Promise<ReadonlyArray<ShibbolethAuthenticationAttemptRow>> {
|
|
42
|
+
const supabase = $provideSupabaseForServer();
|
|
43
|
+
const { data, error } = await supabase
|
|
44
|
+
.from(await getShibbolethAuthenticationAttemptTableName())
|
|
45
|
+
.select('*')
|
|
46
|
+
.order('createdAt', { ascending: false })
|
|
47
|
+
.limit(100);
|
|
48
|
+
|
|
49
|
+
if (error) {
|
|
50
|
+
console.error('Failed to load Shibboleth authentication attempts:', error);
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (data as ShibbolethAuthenticationAttemptRow[] | null) || [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Loads Shibboleth identities and their linked users for the dashboard.
|
|
59
|
+
*/
|
|
60
|
+
async function loadShibbolethIdentitiesWithUsers(): Promise<{
|
|
61
|
+
readonly identities: ReadonlyArray<ShibbolethUserIdentityRow>;
|
|
62
|
+
readonly usersById: ReadonlyMap<number, DashboardUserRow>;
|
|
63
|
+
}> {
|
|
64
|
+
const supabase = $provideSupabaseForServer();
|
|
65
|
+
const { data: identities, error } = await supabase
|
|
66
|
+
.from(await getShibbolethUserIdentityTableName())
|
|
67
|
+
.select('*')
|
|
68
|
+
.order('lastLoggedInAt', { ascending: false, nullsFirst: false });
|
|
69
|
+
|
|
70
|
+
if (error) {
|
|
71
|
+
console.error('Failed to load Shibboleth identities:', error);
|
|
72
|
+
return {
|
|
73
|
+
identities: [],
|
|
74
|
+
usersById: new Map(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const identityRows = (identities as ShibbolethUserIdentityRow[] | null) || [];
|
|
79
|
+
const userIds = Array.from(new Set(identityRows.map((identity) => identity.userId)));
|
|
80
|
+
|
|
81
|
+
if (userIds.length === 0) {
|
|
82
|
+
return {
|
|
83
|
+
identities: identityRows,
|
|
84
|
+
usersById: new Map(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { data: users, error: usersError } = await supabase
|
|
89
|
+
.from(await $getTableName('User'))
|
|
90
|
+
.select('*')
|
|
91
|
+
.in('id', userIds);
|
|
92
|
+
|
|
93
|
+
if (usersError) {
|
|
94
|
+
console.error('Failed to load Shibboleth users:', usersError);
|
|
95
|
+
return {
|
|
96
|
+
identities: identityRows,
|
|
97
|
+
usersById: new Map(),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const usersById = new Map<number, DashboardUserRow>();
|
|
102
|
+
for (const user of (users as unknown as DashboardUserRow[] | null) || []) {
|
|
103
|
+
usersById.set(user.id, user);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
identities: identityRows,
|
|
108
|
+
usersById,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Formats an optional date-time value for the dashboard.
|
|
114
|
+
*/
|
|
115
|
+
function formatDashboardDateTime(value: string | null | undefined): string {
|
|
116
|
+
if (!value) {
|
|
117
|
+
return 'Never';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return new Date(value).toLocaleString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handles Shibboleth login method dashboard page.
|
|
125
|
+
*/
|
|
126
|
+
export default async function ShibbolethLoginMethodPage() {
|
|
127
|
+
const isAdmin = await isUserAdmin();
|
|
128
|
+
|
|
129
|
+
if (!isAdmin) {
|
|
130
|
+
return <ForbiddenPage />;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const requestUrl = await getDashboardRequestUrl();
|
|
134
|
+
const [configuration, attempts, identitiesWithUsers] = await Promise.all([
|
|
135
|
+
resolveShibbolethAuthenticationConfiguration({
|
|
136
|
+
requestUrl,
|
|
137
|
+
isIdentityProviderMetadataValidationEnabled: true,
|
|
138
|
+
}),
|
|
139
|
+
loadShibbolethAuthenticationAttempts(),
|
|
140
|
+
loadShibbolethIdentitiesWithUsers(),
|
|
141
|
+
]);
|
|
142
|
+
const isWarningShown = configuration.isActive && !configuration.isConfigured;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-8">
|
|
146
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
147
|
+
<div>
|
|
148
|
+
<p className="text-sm font-medium text-gray-500">System / Login Methods</p>
|
|
149
|
+
<h1 className="text-3xl font-semibold text-gray-950">Shibboleth</h1>
|
|
150
|
+
<p className="mt-2 max-w-3xl text-sm text-gray-600">
|
|
151
|
+
Manage Shibboleth SAML login, review authentication attempts, and inspect linked users.
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
<Link
|
|
155
|
+
href="/admin/users"
|
|
156
|
+
className="inline-flex items-center justify-center rounded-md border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
157
|
+
>
|
|
158
|
+
Users
|
|
159
|
+
</Link>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{isWarningShown && (
|
|
163
|
+
<div className="flex gap-3 rounded-md border border-amber-300 bg-amber-50 p-4 text-amber-900">
|
|
164
|
+
<AlertTriangle className="mt-0.5 h-5 w-5 flex-none" />
|
|
165
|
+
<div className="space-y-1 text-sm">
|
|
166
|
+
<p className="font-semibold">
|
|
167
|
+
Shibboleth authentication is active but not configured correctly.
|
|
168
|
+
</p>
|
|
169
|
+
<p>{configuration.errors.join(' ')}</p>
|
|
170
|
+
<a href="#setup-instructions" className="font-medium underline">
|
|
171
|
+
Open setup instructions
|
|
172
|
+
</a>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
<section className="rounded-md border border-gray-200 bg-white p-5">
|
|
178
|
+
<div className="flex items-center gap-2">
|
|
179
|
+
{configuration.isActive ? (
|
|
180
|
+
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
|
181
|
+
) : (
|
|
182
|
+
<XCircle className="h-5 w-5 text-gray-400" />
|
|
183
|
+
)}
|
|
184
|
+
<h2 className="text-lg font-semibold text-gray-950">Configuration</h2>
|
|
185
|
+
</div>
|
|
186
|
+
<dl className="mt-4 grid gap-4 text-sm sm:grid-cols-2">
|
|
187
|
+
<div>
|
|
188
|
+
<dt className="font-medium text-gray-500">Active</dt>
|
|
189
|
+
<dd className="mt-1 text-gray-900">{configuration.isActive ? 'Yes' : 'No'}</dd>
|
|
190
|
+
</div>
|
|
191
|
+
<div>
|
|
192
|
+
<dt className="font-medium text-gray-500">Configured</dt>
|
|
193
|
+
<dd className="mt-1 text-gray-900">{configuration.isConfigured ? 'Yes' : 'No'}</dd>
|
|
194
|
+
</div>
|
|
195
|
+
<div>
|
|
196
|
+
<dt className="font-medium text-gray-500">EntityID</dt>
|
|
197
|
+
<dd className="mt-1 break-all font-mono text-xs text-gray-900">
|
|
198
|
+
{configuration.serviceProviderUrls.entityId}
|
|
199
|
+
</dd>
|
|
200
|
+
</div>
|
|
201
|
+
<div>
|
|
202
|
+
<dt className="font-medium text-gray-500">ACS URL</dt>
|
|
203
|
+
<dd className="mt-1 break-all font-mono text-xs text-gray-900">
|
|
204
|
+
{configuration.serviceProviderUrls.assertionConsumerServiceUrl}
|
|
205
|
+
</dd>
|
|
206
|
+
</div>
|
|
207
|
+
<div>
|
|
208
|
+
<dt className="font-medium text-gray-500">SP metadata</dt>
|
|
209
|
+
<dd className="mt-1 break-all text-xs">
|
|
210
|
+
<a
|
|
211
|
+
href={configuration.serviceProviderUrls.metadataUrl}
|
|
212
|
+
className="inline-flex items-center gap-1 font-mono text-blue-700 hover:text-blue-900"
|
|
213
|
+
>
|
|
214
|
+
{configuration.serviceProviderUrls.metadataUrl}
|
|
215
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
216
|
+
</a>
|
|
217
|
+
</dd>
|
|
218
|
+
</div>
|
|
219
|
+
<div>
|
|
220
|
+
<dt className="font-medium text-gray-500">IdP metadata source</dt>
|
|
221
|
+
<dd className="mt-1 break-all text-xs text-gray-900">
|
|
222
|
+
{configuration.isIdentityProviderMetadataXmlConfigured
|
|
223
|
+
? 'Metadata XML is stored directly in metadata configuration.'
|
|
224
|
+
: configuration.identityProviderMetadataUrl || 'Not set'}
|
|
225
|
+
</dd>
|
|
226
|
+
</div>
|
|
227
|
+
</dl>
|
|
228
|
+
{configuration.errors.length > 0 && (
|
|
229
|
+
<ul className="mt-4 list-disc space-y-1 pl-5 text-sm text-amber-800">
|
|
230
|
+
{configuration.errors.map((error) => (
|
|
231
|
+
<li key={error}>{error}</li>
|
|
232
|
+
))}
|
|
233
|
+
</ul>
|
|
234
|
+
)}
|
|
235
|
+
</section>
|
|
236
|
+
|
|
237
|
+
<section id="setup-instructions" className="rounded-md border border-gray-200 bg-white p-5">
|
|
238
|
+
<h2 className="text-lg font-semibold text-gray-950">Setup Instructions</h2>
|
|
239
|
+
<ol className="mt-4 list-decimal space-y-3 pl-5 text-sm text-gray-700">
|
|
240
|
+
<li>
|
|
241
|
+
In{' '}
|
|
242
|
+
<Link href="/admin/metadata" className="text-blue-700 underline">
|
|
243
|
+
Metadata
|
|
244
|
+
</Link>
|
|
245
|
+
, set <code>IS_SHIBBOLETH_AUTH_ACTIVE</code> to <code>true</code>.
|
|
246
|
+
</li>
|
|
247
|
+
<li>
|
|
248
|
+
Set <code>SHIBBOLETH_IDP_METADATA_URL</code> to the university Identity Provider metadata URL.
|
|
249
|
+
For Silesian University use <code>https://idp-cro.slu.cz/idp/shibboleth</code>, or paste the XML
|
|
250
|
+
into <code>SHIBBOLETH_IDP_METADATA_XML</code>.
|
|
251
|
+
</li>
|
|
252
|
+
<li>
|
|
253
|
+
Send the Service Provider metadata URL above to the IdP administrator, or send the EntityID and
|
|
254
|
+
ACS URL shown in the configuration panel.
|
|
255
|
+
</li>
|
|
256
|
+
<li>
|
|
257
|
+
Ask the IdP administrator to release <code>mail</code>, <code>displayName</code>, and{' '}
|
|
258
|
+
<code>unstructuredName</code>. <code>eduPersonPrincipalName</code> can also be used as an
|
|
259
|
+
institutional identifier.
|
|
260
|
+
</li>
|
|
261
|
+
</ol>
|
|
262
|
+
</section>
|
|
263
|
+
|
|
264
|
+
<section className="rounded-md border border-gray-200 bg-white p-5">
|
|
265
|
+
<h2 className="text-lg font-semibold text-gray-950">Authentication Attempts</h2>
|
|
266
|
+
<div className="mt-4 overflow-x-auto">
|
|
267
|
+
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
|
268
|
+
<thead>
|
|
269
|
+
<tr className="text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
270
|
+
<th className="py-2 pr-4">Time</th>
|
|
271
|
+
<th className="py-2 pr-4">Status</th>
|
|
272
|
+
<th className="py-2 pr-4">Stage</th>
|
|
273
|
+
<th className="py-2 pr-4">Email</th>
|
|
274
|
+
<th className="py-2 pr-4">Name</th>
|
|
275
|
+
<th className="py-2 pr-4">Error</th>
|
|
276
|
+
</tr>
|
|
277
|
+
</thead>
|
|
278
|
+
<tbody className="divide-y divide-gray-100">
|
|
279
|
+
{attempts.length === 0 ? (
|
|
280
|
+
<tr>
|
|
281
|
+
<td className="py-3 text-gray-500" colSpan={6}>
|
|
282
|
+
No Shibboleth authentication attempts recorded yet.
|
|
283
|
+
</td>
|
|
284
|
+
</tr>
|
|
285
|
+
) : (
|
|
286
|
+
attempts.map((attempt) => (
|
|
287
|
+
<tr key={attempt.id}>
|
|
288
|
+
<td className="whitespace-nowrap py-2 pr-4 text-gray-700">
|
|
289
|
+
{formatDashboardDateTime(attempt.createdAt)}
|
|
290
|
+
</td>
|
|
291
|
+
<td className="py-2 pr-4 font-medium text-gray-900">{attempt.status}</td>
|
|
292
|
+
<td className="py-2 pr-4 text-gray-700">{attempt.stage}</td>
|
|
293
|
+
<td className="py-2 pr-4 text-gray-700">{attempt.email || '-'}</td>
|
|
294
|
+
<td className="py-2 pr-4 text-gray-700">{attempt.displayName || '-'}</td>
|
|
295
|
+
<td className="max-w-xs py-2 pr-4 text-gray-600">
|
|
296
|
+
{attempt.errorMessage || '-'}
|
|
297
|
+
</td>
|
|
298
|
+
</tr>
|
|
299
|
+
))
|
|
300
|
+
)}
|
|
301
|
+
</tbody>
|
|
302
|
+
</table>
|
|
303
|
+
</div>
|
|
304
|
+
</section>
|
|
305
|
+
|
|
306
|
+
<section className="rounded-md border border-gray-200 bg-white p-5">
|
|
307
|
+
<h2 className="text-lg font-semibold text-gray-950">Shibboleth Users</h2>
|
|
308
|
+
<div className="mt-4 overflow-x-auto">
|
|
309
|
+
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
|
310
|
+
<thead>
|
|
311
|
+
<tr className="text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
312
|
+
<th className="py-2 pr-4">User</th>
|
|
313
|
+
<th className="py-2 pr-4">Email</th>
|
|
314
|
+
<th className="py-2 pr-4">Display Name</th>
|
|
315
|
+
<th className="py-2 pr-4">Institutional ID</th>
|
|
316
|
+
<th className="py-2 pr-4">Logins</th>
|
|
317
|
+
<th className="py-2 pr-4">Last Login</th>
|
|
318
|
+
</tr>
|
|
319
|
+
</thead>
|
|
320
|
+
<tbody className="divide-y divide-gray-100">
|
|
321
|
+
{identitiesWithUsers.identities.length === 0 ? (
|
|
322
|
+
<tr>
|
|
323
|
+
<td className="py-3 text-gray-500" colSpan={6}>
|
|
324
|
+
No Shibboleth users have logged in yet.
|
|
325
|
+
</td>
|
|
326
|
+
</tr>
|
|
327
|
+
) : (
|
|
328
|
+
identitiesWithUsers.identities.map((identity) => {
|
|
329
|
+
const user = identitiesWithUsers.usersById.get(identity.userId);
|
|
330
|
+
return (
|
|
331
|
+
<tr key={identity.id}>
|
|
332
|
+
<td className="py-2 pr-4">
|
|
333
|
+
{user ? (
|
|
334
|
+
<Link
|
|
335
|
+
href={`/admin/users/${encodeURIComponent(user.username)}`}
|
|
336
|
+
className="font-medium text-blue-700 hover:text-blue-900"
|
|
337
|
+
>
|
|
338
|
+
{user.username}
|
|
339
|
+
</Link>
|
|
340
|
+
) : (
|
|
341
|
+
<span className="text-gray-500">
|
|
342
|
+
Missing user #{identity.userId}
|
|
343
|
+
</span>
|
|
344
|
+
)}
|
|
345
|
+
</td>
|
|
346
|
+
<td className="py-2 pr-4 text-gray-700">{identity.email}</td>
|
|
347
|
+
<td className="py-2 pr-4 text-gray-700">{identity.displayName || '-'}</td>
|
|
348
|
+
<td className="py-2 pr-4 text-gray-700">
|
|
349
|
+
{identity.unstructuredName || identity.eduPersonPrincipalName || '-'}
|
|
350
|
+
</td>
|
|
351
|
+
<td className="py-2 pr-4 text-gray-700">{identity.loginCount}</td>
|
|
352
|
+
<td className="whitespace-nowrap py-2 pr-4 text-gray-700">
|
|
353
|
+
{formatDashboardDateTime(identity.lastLoggedInAt)}
|
|
354
|
+
</td>
|
|
355
|
+
</tr>
|
|
356
|
+
);
|
|
357
|
+
})
|
|
358
|
+
)}
|
|
359
|
+
</tbody>
|
|
360
|
+
</table>
|
|
361
|
+
</div>
|
|
362
|
+
</section>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { upload } from '@vercel/blob/client';
|
|
4
3
|
import { FileTextIcon, HashIcon, ImageIcon, ListIcon, ShieldIcon, ToggleLeftIcon, TypeIcon, Upload } from 'lucide-react';
|
|
5
4
|
import Link from 'next/link';
|
|
6
5
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
@@ -8,6 +7,7 @@ import { showConfirm } from '../../../components/AsyncDialogs/asyncDialogs';
|
|
|
8
7
|
import { getMetadataDefinition, metadataDefaults, type MetadataDefinition } from '../../../database/metadataDefaults';
|
|
9
8
|
import { getSafeCdnPath } from '../../../utils/cdn/utils/getSafeCdnPath';
|
|
10
9
|
import { normalizeUploadFilename } from '../../../utils/normalization/normalizeUploadFilename';
|
|
10
|
+
import { buildDefaultUserFileUploadPath, uploadFileToServer } from '../../../utils/upload/uploadFileToServer';
|
|
11
11
|
import { getDeprecatedLimitMetadataDefinition, type DeprecatedLimitMetadataDefinition } from '../../../constants/serverLimits';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -502,23 +502,21 @@ export function MetadataClient() {
|
|
|
502
502
|
try {
|
|
503
503
|
setUploading(true);
|
|
504
504
|
|
|
505
|
-
const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
|
|
506
505
|
const normalizedFilename = normalizeUploadFilename(file.name);
|
|
507
|
-
const uploadPath =
|
|
508
|
-
|
|
509
|
-
:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const blob = await upload(safeUploadPath, file, {
|
|
513
|
-
access: 'public',
|
|
514
|
-
handleUploadUrl: '/api/upload',
|
|
515
|
-
clientPayload: JSON.stringify({
|
|
516
|
-
purpose: formState.key || 'METADATA_IMAGE',
|
|
517
|
-
contentType: file.type,
|
|
518
|
-
}),
|
|
506
|
+
const uploadPath = buildDefaultUserFileUploadPath(normalizedFilename);
|
|
507
|
+
const safeUploadPath = getSafeCdnPath({
|
|
508
|
+
pathname: uploadPath,
|
|
509
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX,
|
|
519
510
|
});
|
|
520
511
|
|
|
521
|
-
const
|
|
512
|
+
const uploadResult = await uploadFileToServer({
|
|
513
|
+
file,
|
|
514
|
+
pathname: safeUploadPath,
|
|
515
|
+
purpose: formState.key || 'METADATA_IMAGE',
|
|
516
|
+
contentType: file.type,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const fileUrl = uploadResult.url;
|
|
522
520
|
|
|
523
521
|
const LONG_URL = `${process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!}/${process.env
|
|
524
522
|
.NEXT_PUBLIC_CDN_PATH_PREFIX!}/user/files/`;
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
* @private function of <ServersClient/>
|
|
18
18
|
*/
|
|
19
19
|
const INPUT_CLASS_NAME =
|
|
20
|
-
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200';
|
|
20
|
+
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-slate-600 dark:bg-slate-950 dark:text-slate-100 dark:focus:border-sky-400 dark:focus:ring-sky-500/30';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Shared secondary button styling used by the registry table.
|
|
@@ -25,7 +25,7 @@ const INPUT_CLASS_NAME =
|
|
|
25
25
|
* @private function of <ServersClient/>
|
|
26
26
|
*/
|
|
27
27
|
const SECONDARY_BUTTON_CLASS_NAME =
|
|
28
|
-
'inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60';
|
|
28
|
+
'inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60 dark:border-slate-600 dark:bg-slate-950 dark:text-slate-100 dark:hover:bg-slate-800';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Shared primary button styling used by the registry table.
|
|
@@ -196,7 +196,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
|
|
|
196
196
|
|
|
197
197
|
return (
|
|
198
198
|
<>
|
|
199
|
-
<tr className={isCurrent ? 'bg-blue-50/40' : 'hover:bg-gray-50'}>
|
|
199
|
+
<tr className={isCurrent ? 'bg-blue-50/40 dark:bg-sky-950/45' : 'hover:bg-gray-50 dark:hover:bg-slate-900/80'}>
|
|
200
200
|
<td className="px-4 py-3 align-top">
|
|
201
201
|
<input
|
|
202
202
|
type="text"
|