@promptbook/cli 0.112.0-100 → 0.112.0-102
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/README.md +6 -0
- 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/login-methods/shibboleth/page.tsx +365 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +3 -3
- 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 +230 -18
- 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 +69 -23
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +54 -6
- package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +4 -6
- package/apps/agents-server/src/utils/cdn/resolveCdnStorageProvider.ts +40 -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 +11 -10
- package/apps/agents-server/src/utils/shibbolethAuthentication.ts +729 -621
- package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +137 -19
- package/esm/index.es.js +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 +65 -4
- package/src/other/templates/getTemplatesPipelineCollection.ts +877 -689
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +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
|
@@ -41,6 +41,12 @@ ptbk agents-server start --agent github-copilot --model gpt-5.4 --thinking-level
|
|
|
41
41
|
<a id="agents-server-env-admin-password"></a>
|
|
42
42
|
- `ADMIN_PASSWORD`: Password for the built-in `admin` login on a self-hosted Agents Server. Choose a private value before using the admin UI.
|
|
43
43
|
|
|
44
|
+
<a id="agents-server-env-next-public-cdn-storage-provider"></a>
|
|
45
|
+
- `NEXT_PUBLIC_CDN_STORAGE_PROVIDER`: File storage provider for uploads. Use `s3` for S3-compatible storage such as the VPS installer's self-contained MinIO service or external S3, and `vercel` for Vercel Blob.
|
|
46
|
+
|
|
47
|
+
<a id="agents-server-env-s3-cdn"></a>
|
|
48
|
+
- `CDN_BUCKET`, `CDN_ENDPOINT`, `CDN_ACCESS_KEY_ID`, `CDN_SECRET_ACCESS_KEY`, `NEXT_PUBLIC_CDN_PUBLIC_URL`, and `NEXT_PUBLIC_CDN_PATH_PREFIX`: S3-compatible upload configuration used when `NEXT_PUBLIC_CDN_STORAGE_PROVIDER=s3`.
|
|
49
|
+
|
|
44
50
|
## Creating servers
|
|
45
51
|
|
|
46
52
|
When creating new Agents server, search across the repository for [☁]
|
|
@@ -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>
|
|
@@ -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
|
+
}
|
|
@@ -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"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { CheckCircle2, Download, Loader2, RefreshCcw, Rocket, Server, TriangleAlert } from 'lucide-react';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { AdminXtermTerminal } from '../../../components/AdminTerminal/AdminXtermTerminal';
|
|
5
6
|
import { Card } from '../../../components/Homepage/Card';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -121,6 +122,9 @@ export function UpdateClient() {
|
|
|
121
122
|
const isEnvironmentSwitchRequired =
|
|
122
123
|
Boolean(selectedEnvironment) && selectedEnvironment?.id !== overview?.currentEnvironment.id;
|
|
123
124
|
const isUpdateRunning = overview?.job.status === 'running';
|
|
125
|
+
const updateTerminalId = `standalone-vps-update:${overview?.job.startedAt || overview?.job.finishedAt || overview?.job.status || 'loading'}`;
|
|
126
|
+
const updateTerminalEmptyState =
|
|
127
|
+
isLoading && !overview ? 'Loading update log...' : 'No persisted update log output yet.';
|
|
124
128
|
|
|
125
129
|
/**
|
|
126
130
|
* Starts one detached update run for the selected environment.
|
|
@@ -406,9 +410,14 @@ export function UpdateClient() {
|
|
|
406
410
|
<span className="ml-2 font-mono text-slate-700">{overview.job.logFilePath}</span>
|
|
407
411
|
</div>
|
|
408
412
|
)}
|
|
409
|
-
<
|
|
410
|
-
{
|
|
411
|
-
|
|
413
|
+
<AdminXtermTerminal
|
|
414
|
+
terminalId={updateTerminalId}
|
|
415
|
+
output={overview?.job.logTail || ''}
|
|
416
|
+
emptyState={updateTerminalEmptyState}
|
|
417
|
+
isReadOnly
|
|
418
|
+
isPlainTextOutput
|
|
419
|
+
ariaLabel="Standalone VPS update log"
|
|
420
|
+
/>
|
|
412
421
|
</div>
|
|
413
422
|
</Card>
|
|
414
423
|
</div>
|
|
@@ -81,7 +81,7 @@ export function UsageClientTimelineChart(props: UsageClientTimelineChartProps) {
|
|
|
81
81
|
|
|
82
82
|
return (
|
|
83
83
|
<div>
|
|
84
|
-
<div className="w-full overflow-x-auto rounded-lg border border-gray-100 bg-gradient-to-br from-slate-50 via-white to-blue-50 p-2">
|
|
84
|
+
<div className="w-full overflow-x-auto rounded-lg border border-gray-100 bg-gradient-to-br from-slate-50 via-white to-blue-50 p-2 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900">
|
|
85
85
|
<svg
|
|
86
86
|
width="100%"
|
|
87
87
|
viewBox={`0 0 ${chartGeometry.width} ${chartGeometry.height}`}
|
|
@@ -86,33 +86,40 @@ export function UserDetailClient({ userId }: UserDetailClientProps) {
|
|
|
86
86
|
{t('users.adminRole')}
|
|
87
87
|
</span>
|
|
88
88
|
)}
|
|
89
|
+
{user.authenticationProvider?.includes('SHIBBOLETH') && (
|
|
90
|
+
<Link
|
|
91
|
+
href="/admin/login-methods/shibboleth"
|
|
92
|
+
className="ml-2 inline-block bg-emerald-100 text-emerald-800 text-xs px-2 py-1 rounded mt-1 hover:bg-emerald-200"
|
|
93
|
+
>
|
|
94
|
+
Shibboleth
|
|
95
|
+
</Link>
|
|
96
|
+
)}
|
|
89
97
|
<p className="text-gray-500 text-sm mt-2">
|
|
90
98
|
{t('users.idLabel')}: {user.id}
|
|
91
99
|
</p>
|
|
100
|
+
{user.displayName && (
|
|
101
|
+
<p className="text-gray-500 text-sm mt-1">Display name: {user.displayName}</p>
|
|
102
|
+
)}
|
|
103
|
+
{user.email && <p className="text-gray-500 text-sm mt-1">Email: {user.email}</p>}
|
|
104
|
+
{user.authenticationProvider && (
|
|
105
|
+
<p className="text-gray-500 text-sm mt-1">
|
|
106
|
+
Authentication: {user.authenticationProvider}
|
|
107
|
+
</p>
|
|
108
|
+
)}
|
|
92
109
|
<p className="text-gray-500 text-sm mt-1">
|
|
93
110
|
{t('users.createdAtLabel')}:{' '}
|
|
94
|
-
{user.createdAt
|
|
95
|
-
? new Date(user.createdAt).toLocaleString()
|
|
96
|
-
: t('users.unknownValue')}
|
|
111
|
+
{user.createdAt ? new Date(user.createdAt).toLocaleString() : t('users.unknownValue')}
|
|
97
112
|
</p>
|
|
98
113
|
<p className="text-gray-500 text-sm mt-1">
|
|
99
114
|
{t('users.lastUpdatedLabel')}:{' '}
|
|
100
|
-
{user.updatedAt
|
|
101
|
-
? new Date(user.updatedAt).toLocaleString()
|
|
102
|
-
: t('users.unknownValue')}
|
|
115
|
+
{user.updatedAt ? new Date(user.updatedAt).toLocaleString() : t('users.unknownValue')}
|
|
103
116
|
</p>
|
|
104
117
|
</div>
|
|
105
118
|
<div className="space-x-2">
|
|
106
|
-
<button
|
|
107
|
-
onClick={handleToggleAdmin}
|
|
108
|
-
className="text-sm text-blue-600 hover:text-blue-800"
|
|
109
|
-
>
|
|
119
|
+
<button onClick={handleToggleAdmin} className="text-sm text-blue-600 hover:text-blue-800">
|
|
110
120
|
{user.isAdmin ? t('users.removeAdmin') : t('users.makeAdmin')}
|
|
111
121
|
</button>
|
|
112
|
-
<button
|
|
113
|
-
onClick={handleDelete}
|
|
114
|
-
className="text-sm text-red-600 hover:text-red-800"
|
|
115
|
-
>
|
|
122
|
+
<button onClick={handleDelete} className="text-sm text-red-600 hover:text-red-800">
|
|
116
123
|
{t('users.deleteUserAction')}
|
|
117
124
|
</button>
|
|
118
125
|
</div>
|