@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { loginAction } from '@/src/app/actions';
|
|
4
|
-
import {
|
|
4
|
+
import { Loader2, Lock, ShieldCheck, User } from 'lucide-react';
|
|
5
5
|
import { SecretInput } from '@/src/components/SecretInput/SecretInput';
|
|
6
|
-
import {
|
|
6
|
+
import { useRouter } from 'next/navigation';
|
|
7
7
|
import { useCallback, useEffect, useState } from 'react';
|
|
8
8
|
import { ForgottenPasswordDialog } from '../ForgottenPasswordDialog/ForgottenPasswordDialog';
|
|
9
9
|
import { RegisterUserDialog } from '../RegisterUserDialog/RegisterUserDialog';
|
|
@@ -36,43 +36,11 @@ type LoginFormProps = {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* Public Shibboleth status returned by the login status endpoint.
|
|
40
40
|
*/
|
|
41
|
-
type
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/
|
|
45
|
-
readonly methods?: ReadonlyArray<string>;
|
|
46
|
-
/**
|
|
47
|
-
* Shibboleth login metadata.
|
|
48
|
-
*/
|
|
49
|
-
readonly shibboleth?: {
|
|
50
|
-
readonly isEnabled?: boolean;
|
|
51
|
-
readonly providerLabel?: string;
|
|
52
|
-
readonly loginUrl?: string;
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Client-side state describing visible login methods.
|
|
58
|
-
*/
|
|
59
|
-
type LoginMethodState = {
|
|
60
|
-
/**
|
|
61
|
-
* Whether password login should be shown.
|
|
62
|
-
*/
|
|
63
|
-
readonly isPasswordEnabled: boolean;
|
|
64
|
-
/**
|
|
65
|
-
* Whether Shibboleth login should be shown.
|
|
66
|
-
*/
|
|
67
|
-
readonly isShibbolethEnabled: boolean;
|
|
68
|
-
/**
|
|
69
|
-
* User-facing Shibboleth provider label.
|
|
70
|
-
*/
|
|
71
|
-
readonly shibbolethProviderLabel: string;
|
|
72
|
-
/**
|
|
73
|
-
* Route that starts Shibboleth authentication.
|
|
74
|
-
*/
|
|
75
|
-
readonly shibbolethLoginUrl: string;
|
|
41
|
+
type ShibbolethLoginStatus = {
|
|
42
|
+
readonly isActive: boolean;
|
|
43
|
+
readonly isConfigured: boolean;
|
|
76
44
|
};
|
|
77
45
|
|
|
78
46
|
/**
|
|
@@ -86,21 +54,11 @@ export function LoginForm(props: LoginFormProps) {
|
|
|
86
54
|
const [adminEmail, setAdminEmail] = useState<string>('support@ptbk.io');
|
|
87
55
|
const [isForgottenPasswordOpen, setIsForgottenPasswordOpen] = useState(false);
|
|
88
56
|
const [isRegisterUserOpen, setIsRegisterUserOpen] = useState(false);
|
|
57
|
+
const [shibbolethLoginStatus, setShibbolethLoginStatus] = useState<ShibbolethLoginStatus | null>(null);
|
|
89
58
|
const [username, setUsername] = useState('');
|
|
90
59
|
const [password, setPassword] = useState('');
|
|
91
|
-
const [loginMethods, setLoginMethods] = useState<LoginMethodState>({
|
|
92
|
-
isPasswordEnabled: true,
|
|
93
|
-
isShibbolethEnabled: false,
|
|
94
|
-
shibbolethProviderLabel: 'Shibboleth',
|
|
95
|
-
shibbolethLoginUrl: '/api/auth/shibboleth/login',
|
|
96
|
-
});
|
|
97
60
|
const hasUnsavedChanges = username.length > 0 || password.length > 0;
|
|
98
61
|
const router = useRouter();
|
|
99
|
-
const pathname = usePathname();
|
|
100
|
-
const searchParams = useSearchParams();
|
|
101
|
-
const search = searchParams?.toString();
|
|
102
|
-
const redirectTo = `${pathname || '/'}${search ? `?${search}` : ''}`;
|
|
103
|
-
const shibbolethLoginHref = `${loginMethods.shibbolethLoginUrl}?redirectTo=${encodeURIComponent(redirectTo)}`;
|
|
104
62
|
|
|
105
63
|
useEffect(() => {
|
|
106
64
|
// Fetch admin email on component mount
|
|
@@ -118,20 +76,13 @@ export function LoginForm(props: LoginFormProps) {
|
|
|
118
76
|
}, []);
|
|
119
77
|
|
|
120
78
|
useEffect(() => {
|
|
121
|
-
fetch('/api/auth/
|
|
79
|
+
fetch('/api/auth/shibboleth/status')
|
|
122
80
|
.then((response) => response.json())
|
|
123
|
-
.then((data:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
setLoginMethods({
|
|
127
|
-
isPasswordEnabled: methods.includes('PASSWORD'),
|
|
128
|
-
isShibbolethEnabled: Boolean(data.shibboleth?.isEnabled),
|
|
129
|
-
shibbolethProviderLabel: data.shibboleth?.providerLabel || 'Shibboleth',
|
|
130
|
-
shibbolethLoginUrl: data.shibboleth?.loginUrl || '/api/auth/shibboleth/login',
|
|
131
|
-
});
|
|
81
|
+
.then((data: ShibbolethLoginStatus) => {
|
|
82
|
+
setShibbolethLoginStatus(data);
|
|
132
83
|
})
|
|
133
84
|
.catch((error) => {
|
|
134
|
-
console.error('Failed to fetch
|
|
85
|
+
console.error('Failed to fetch Shibboleth status:', error);
|
|
135
86
|
});
|
|
136
87
|
}, []);
|
|
137
88
|
|
|
@@ -180,105 +131,105 @@ export function LoginForm(props: LoginFormProps) {
|
|
|
180
131
|
[onSuccess, refreshAfterSuccess, router, t],
|
|
181
132
|
);
|
|
182
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Starts Shibboleth login while preserving the current page as RelayState.
|
|
136
|
+
*/
|
|
137
|
+
const handleShibbolethLogin = useCallback(() => {
|
|
138
|
+
const returnTo =
|
|
139
|
+
typeof window === 'undefined'
|
|
140
|
+
? '/'
|
|
141
|
+
: `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
142
|
+
window.location.href = `/api/auth/shibboleth/login?returnTo=${encodeURIComponent(returnTo)}`;
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
183
145
|
return (
|
|
184
146
|
<form onSubmit={handleSubmit} className={`space-y-4 ${className || ''}`}>
|
|
185
|
-
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
>
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
147
|
+
<div className="space-y-2">
|
|
148
|
+
<label htmlFor="username" className="text-sm font-medium text-gray-700 block">
|
|
149
|
+
{t('login.usernameLabel')}
|
|
150
|
+
</label>
|
|
151
|
+
<div className="relative">
|
|
152
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
|
153
|
+
<User className="w-4 h-4" />
|
|
154
|
+
</div>
|
|
155
|
+
<input
|
|
156
|
+
id="username"
|
|
157
|
+
name="username"
|
|
158
|
+
type="text"
|
|
159
|
+
value={username}
|
|
160
|
+
onChange={(event) => setUsername(event.target.value)}
|
|
161
|
+
required
|
|
162
|
+
className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
|
|
163
|
+
placeholder={t('login.usernamePlaceholder')}
|
|
164
|
+
/>
|
|
200
165
|
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className="space-y-2">
|
|
169
|
+
<SecretInput
|
|
170
|
+
id="password"
|
|
171
|
+
name="password"
|
|
172
|
+
value={password}
|
|
173
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
174
|
+
label={t('login.passwordLabel')}
|
|
175
|
+
placeholder={t('login.passwordPlaceholder')}
|
|
176
|
+
required
|
|
177
|
+
startIcon={<Lock className="w-4 h-4" />}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{error && (
|
|
182
|
+
<div className="p-3 text-sm text-red-500 bg-red-50 border border-red-200 rounded-md">{error}</div>
|
|
201
183
|
)}
|
|
202
184
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
|
|
221
|
-
placeholder={t('login.usernamePlaceholder')}
|
|
222
|
-
/>
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<div className="space-y-2">
|
|
227
|
-
<SecretInput
|
|
228
|
-
id="password"
|
|
229
|
-
name="password"
|
|
230
|
-
value={password}
|
|
231
|
-
onChange={(event) => setPassword(event.target.value)}
|
|
232
|
-
label={t('login.passwordLabel')}
|
|
233
|
-
placeholder={t('login.passwordPlaceholder')}
|
|
234
|
-
required
|
|
235
|
-
startIcon={<Lock className="w-4 h-4" />}
|
|
236
|
-
/>
|
|
237
|
-
</div>
|
|
238
|
-
|
|
239
|
-
{error && (
|
|
240
|
-
<div className="p-3 text-sm text-red-500 bg-red-50 border border-red-200 rounded-md">
|
|
241
|
-
{error}
|
|
242
|
-
</div>
|
|
243
|
-
)}
|
|
244
|
-
|
|
185
|
+
<button
|
|
186
|
+
type="submit"
|
|
187
|
+
disabled={isLoading}
|
|
188
|
+
className="w-full inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-promptbook-blue-dark text-white hover:bg-promptbook-blue-dark/90 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none transition-colors"
|
|
189
|
+
>
|
|
190
|
+
{isLoading ? (
|
|
191
|
+
<>
|
|
192
|
+
<Loader2 className="mr-2 w-4 h-4 animate-spin" />
|
|
193
|
+
{t('login.loggingIn')}
|
|
194
|
+
</>
|
|
195
|
+
) : (
|
|
196
|
+
t('login.loginAction')
|
|
197
|
+
)}
|
|
198
|
+
</button>
|
|
199
|
+
|
|
200
|
+
{shibbolethLoginStatus?.isActive && (
|
|
201
|
+
<div className="space-y-3 border-t border-gray-200 pt-4">
|
|
245
202
|
<button
|
|
246
|
-
type="
|
|
247
|
-
disabled={
|
|
248
|
-
|
|
203
|
+
type="button"
|
|
204
|
+
disabled={!shibbolethLoginStatus.isConfigured}
|
|
205
|
+
onClick={handleShibbolethLogin}
|
|
206
|
+
className="w-full inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none transition-colors"
|
|
249
207
|
>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
<Loader2 className="mr-2 w-4 h-4 animate-spin" />
|
|
253
|
-
{t('login.loggingIn')}
|
|
254
|
-
</>
|
|
255
|
-
) : (
|
|
256
|
-
t('login.loginAction')
|
|
257
|
-
)}
|
|
208
|
+
<ShieldCheck className="mr-2 w-4 h-4" />
|
|
209
|
+
{t('login.shibbolethLoginAction')}
|
|
258
210
|
</button>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
onClick={() => setIsForgottenPasswordOpen(true)}
|
|
264
|
-
className="text-promptbook-blue hover:text-promptbook-blue-dark underline focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 rounded-sm"
|
|
265
|
-
>
|
|
266
|
-
{t('login.forgottenPassword')}
|
|
267
|
-
</button>
|
|
268
|
-
<button
|
|
269
|
-
type="button"
|
|
270
|
-
onClick={() => setIsRegisterUserOpen(true)}
|
|
271
|
-
className="text-promptbook-blue hover:text-promptbook-blue-dark underline focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 rounded-sm"
|
|
272
|
-
>
|
|
273
|
-
{t('login.registerNewUser')}
|
|
274
|
-
</button>
|
|
275
|
-
</div>
|
|
276
|
-
</>
|
|
211
|
+
{!shibbolethLoginStatus.isConfigured && (
|
|
212
|
+
<p className="text-xs text-amber-700">{t('login.shibbolethNotConfigured')}</p>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
277
215
|
)}
|
|
278
216
|
|
|
279
|
-
|
|
280
|
-
<
|
|
281
|
-
|
|
217
|
+
<div className="flex justify-between text-sm">
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={() => setIsForgottenPasswordOpen(true)}
|
|
221
|
+
className="text-promptbook-blue hover:text-promptbook-blue-dark underline focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 rounded-sm"
|
|
222
|
+
>
|
|
223
|
+
{t('login.forgottenPassword')}
|
|
224
|
+
</button>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
onClick={() => setIsRegisterUserOpen(true)}
|
|
228
|
+
className="text-promptbook-blue hover:text-promptbook-blue-dark underline focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 rounded-sm"
|
|
229
|
+
>
|
|
230
|
+
{t('login.registerNewUser')}
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
282
233
|
|
|
283
234
|
{isForgottenPasswordOpen && (
|
|
284
235
|
<ForgottenPasswordDialog onClose={() => setIsForgottenPasswordOpen(false)} adminEmail={adminEmail} />
|
|
@@ -44,7 +44,7 @@ export function ConsolePageLoadingSkeleton({
|
|
|
44
44
|
const normalizedPanelHeights = panelHeights.length > 0 ? panelHeights : DEFAULT_PANEL_HEIGHTS;
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
|
-
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
|
|
47
|
+
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900">
|
|
48
48
|
<div
|
|
49
49
|
className={`mx-auto w-full px-4 py-8 sm:px-6 sm:py-10 ${maxWidthClassName}`}
|
|
50
50
|
role="status"
|
|
@@ -32,7 +32,7 @@ export function DocumentationRouteLoadingSkeleton({
|
|
|
32
32
|
showSidebar = false,
|
|
33
33
|
}: DocumentationRouteLoadingSkeletonProps) {
|
|
34
34
|
return (
|
|
35
|
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
|
|
35
|
+
<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">
|
|
36
36
|
<div
|
|
37
37
|
className="container mx-auto px-4 py-16"
|
|
38
38
|
role="status"
|
|
@@ -17,7 +17,7 @@ type HomepageLoadingSkeletonProps = {
|
|
|
17
17
|
*/
|
|
18
18
|
export function HomepageLoadingSkeleton({ showGraphPlaceholder = true }: HomepageLoadingSkeletonProps) {
|
|
19
19
|
return (
|
|
20
|
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
|
|
20
|
+
<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">
|
|
21
21
|
<div
|
|
22
22
|
className="container mx-auto px-4 py-16"
|
|
23
23
|
role="status"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import Link from 'next/link';
|
|
3
4
|
import { FormEvent, useState } from 'react';
|
|
4
5
|
import { Card } from '../Homepage/Card';
|
|
5
6
|
import { Section } from '../Homepage/Section';
|
|
@@ -69,10 +70,25 @@ export function UsersList({ allowCreate = true }: UsersListProps) {
|
|
|
69
70
|
<div className="flex justify-between items-start">
|
|
70
71
|
<div>
|
|
71
72
|
<h3 className="text-xl font-semibold text-gray-900">{user.username}</h3>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
<div className="mt-1 flex flex-wrap gap-2">
|
|
74
|
+
{user.isAdmin && (
|
|
75
|
+
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
|
|
76
|
+
{t('users.adminRole')}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
{user.authenticationProvider?.includes('SHIBBOLETH') && (
|
|
80
|
+
<Link
|
|
81
|
+
href="/admin/login-methods/shibboleth"
|
|
82
|
+
className="inline-block bg-emerald-100 text-emerald-800 text-xs px-2 py-1 rounded hover:bg-emerald-200"
|
|
83
|
+
>
|
|
84
|
+
Shibboleth
|
|
85
|
+
</Link>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
{(user.displayName || user.email) && (
|
|
89
|
+
<p className="text-gray-600 text-sm mt-2">
|
|
90
|
+
{[user.displayName, user.email].filter(Boolean).join(' / ')}
|
|
91
|
+
</p>
|
|
76
92
|
)}
|
|
77
93
|
<p className="text-gray-500 text-sm mt-2">
|
|
78
94
|
{t('users.createdLabel')}: {new Date(user.createdAt).toLocaleDateString()}
|
|
@@ -8,6 +8,9 @@ import { useServerLanguage } from '../ServerLanguage/ServerLanguageProvider';
|
|
|
8
8
|
export type AdminUser = {
|
|
9
9
|
id: number;
|
|
10
10
|
username: string;
|
|
11
|
+
email?: string | null;
|
|
12
|
+
displayName?: string | null;
|
|
13
|
+
authenticationProvider?: string | null;
|
|
11
14
|
createdAt: string;
|
|
12
15
|
updatedAt: string;
|
|
13
16
|
isAdmin: boolean;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata key that enables Shibboleth authentication in Agents Server.
|
|
3
|
+
*
|
|
4
|
+
* @private internal Shibboleth authentication metadata key
|
|
5
|
+
*/
|
|
6
|
+
export const IS_SHIBBOLETH_AUTH_ACTIVE_METADATA_KEY = 'IS_SHIBBOLETH_AUTH_ACTIVE' as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Metadata key containing a URL with the Identity Provider SAML metadata XML.
|
|
10
|
+
*
|
|
11
|
+
* @private internal Shibboleth authentication metadata key
|
|
12
|
+
*/
|
|
13
|
+
export const SHIBBOLETH_IDENTITY_PROVIDER_METADATA_URL_METADATA_KEY = 'SHIBBOLETH_IDP_METADATA_URL' as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Metadata key containing pasted Identity Provider SAML metadata XML.
|
|
17
|
+
*
|
|
18
|
+
* @private internal Shibboleth authentication metadata key
|
|
19
|
+
*/
|
|
20
|
+
export const SHIBBOLETH_IDENTITY_PROVIDER_METADATA_XML_METADATA_KEY = 'SHIBBOLETH_IDP_METADATA_XML' as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Metadata key overriding the Service Provider entity ID sent to the Shibboleth Identity Provider.
|
|
24
|
+
*
|
|
25
|
+
* @private internal Shibboleth authentication metadata key
|
|
26
|
+
*/
|
|
27
|
+
export const SHIBBOLETH_SERVICE_PROVIDER_ENTITY_ID_METADATA_KEY = 'SHIBBOLETH_SP_ENTITY_ID' as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Metadata key defining accepted email attribute names.
|
|
31
|
+
*
|
|
32
|
+
* @private internal Shibboleth authentication metadata key
|
|
33
|
+
*/
|
|
34
|
+
export const SHIBBOLETH_EMAIL_ATTRIBUTE_NAMES_METADATA_KEY = 'SHIBBOLETH_EMAIL_ATTRIBUTE_NAMES' as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Metadata key defining accepted display-name attribute names.
|
|
38
|
+
*
|
|
39
|
+
* @private internal Shibboleth authentication metadata key
|
|
40
|
+
*/
|
|
41
|
+
export const SHIBBOLETH_DISPLAY_NAME_ATTRIBUTE_NAMES_METADATA_KEY = 'SHIBBOLETH_DISPLAY_NAME_ATTRIBUTE_NAMES' as const;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Metadata key defining accepted institutional identifier attribute names.
|
|
45
|
+
*
|
|
46
|
+
* @private internal Shibboleth authentication metadata key
|
|
47
|
+
*/
|
|
48
|
+
export const SHIBBOLETH_UNSTRUCTURED_NAME_ATTRIBUTE_NAMES_METADATA_KEY =
|
|
49
|
+
'SHIBBOLETH_UNSTRUCTURED_NAME_ATTRIBUTE_NAMES' as const;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default Shibboleth email attribute names accepted from SAML assertions.
|
|
53
|
+
*
|
|
54
|
+
* @private internal Shibboleth authentication metadata default
|
|
55
|
+
*/
|
|
56
|
+
export const DEFAULT_SHIBBOLETH_EMAIL_ATTRIBUTE_NAMES = 'mail email urn:oid:0.9.2342.19200300.100.1.3' as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Default Shibboleth display-name attribute names accepted from SAML assertions.
|
|
60
|
+
*
|
|
61
|
+
* @private internal Shibboleth authentication metadata default
|
|
62
|
+
*/
|
|
63
|
+
export const DEFAULT_SHIBBOLETH_DISPLAY_NAME_ATTRIBUTE_NAMES = 'displayName urn:oid:2.16.840.1.113730.3.1.241' as const;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default Shibboleth institutional identifier attribute names accepted from SAML assertions.
|
|
67
|
+
*
|
|
68
|
+
* @private internal Shibboleth authentication metadata default
|
|
69
|
+
*/
|
|
70
|
+
export const DEFAULT_SHIBBOLETH_UNSTRUCTURED_NAME_ATTRIBUTE_NAMES =
|
|
71
|
+
'unstructuredName eduPersonPrincipalName urn:oid:1.3.6.1.4.1.5923.1.1.1.6 uid' as const;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Metadata keys required by the Shibboleth authentication integration.
|
|
75
|
+
*
|
|
76
|
+
* @private internal Shibboleth authentication metadata key list
|
|
77
|
+
*/
|
|
78
|
+
export const SHIBBOLETH_AUTHENTICATION_METADATA_KEYS = [
|
|
79
|
+
IS_SHIBBOLETH_AUTH_ACTIVE_METADATA_KEY,
|
|
80
|
+
SHIBBOLETH_IDENTITY_PROVIDER_METADATA_URL_METADATA_KEY,
|
|
81
|
+
SHIBBOLETH_IDENTITY_PROVIDER_METADATA_XML_METADATA_KEY,
|
|
82
|
+
SHIBBOLETH_SERVICE_PROVIDER_ENTITY_ID_METADATA_KEY,
|
|
83
|
+
SHIBBOLETH_EMAIL_ATTRIBUTE_NAMES_METADATA_KEY,
|
|
84
|
+
SHIBBOLETH_DISPLAY_NAME_ATTRIBUTE_NAMES_METADATA_KEY,
|
|
85
|
+
SHIBBOLETH_UNSTRUCTURED_NAME_ATTRIBUTE_NAMES_METADATA_KEY,
|
|
86
|
+
] as const;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Shape used by the header/menu layer to decide whether the Shibboleth menu item should appear.
|
|
90
|
+
*
|
|
91
|
+
* @private internal Shibboleth menu state
|
|
92
|
+
*/
|
|
93
|
+
export type ShibbolethAuthenticationMenuStatus = {
|
|
94
|
+
/**
|
|
95
|
+
* True when Shibboleth login is enabled by metadata.
|
|
96
|
+
*/
|
|
97
|
+
readonly isActive: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* True when the minimum metadata required to contact the Identity Provider is present.
|
|
100
|
+
*/
|
|
101
|
+
readonly isConfigured: boolean;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parses a metadata boolean value.
|
|
106
|
+
*
|
|
107
|
+
* @param value - Raw metadata value.
|
|
108
|
+
* @returns Boolean interpretation of the metadata value.
|
|
109
|
+
*
|
|
110
|
+
* @private internal Shibboleth metadata parser
|
|
111
|
+
*/
|
|
112
|
+
export function parseShibbolethBooleanMetadata(value: string | null | undefined): boolean {
|
|
113
|
+
return (value || '').trim().toLowerCase() === 'true';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolves the Shibboleth menu status from a metadata map.
|
|
118
|
+
*
|
|
119
|
+
* @param metadata - Metadata values keyed by metadata name.
|
|
120
|
+
* @returns Shibboleth activation/configuration status.
|
|
121
|
+
*
|
|
122
|
+
* @private internal Shibboleth menu helper
|
|
123
|
+
*/
|
|
124
|
+
export function resolveShibbolethAuthenticationMenuStatus(
|
|
125
|
+
metadata: Readonly<Record<string, string | null | undefined>>,
|
|
126
|
+
): ShibbolethAuthenticationMenuStatus {
|
|
127
|
+
const isActive = parseShibbolethBooleanMetadata(metadata[IS_SHIBBOLETH_AUTH_ACTIVE_METADATA_KEY]);
|
|
128
|
+
const isIdentityProviderMetadataUrlConfigured = Boolean(
|
|
129
|
+
(metadata[SHIBBOLETH_IDENTITY_PROVIDER_METADATA_URL_METADATA_KEY] || '').trim(),
|
|
130
|
+
);
|
|
131
|
+
const isIdentityProviderMetadataXmlConfigured = Boolean(
|
|
132
|
+
(metadata[SHIBBOLETH_IDENTITY_PROVIDER_METADATA_XML_METADATA_KEY] || '').trim(),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
isActive,
|
|
137
|
+
isConfigured: isActive && (isIdentityProviderMetadataUrlConfigured || isIdentityProviderMetadataXmlConfigured),
|
|
138
|
+
};
|
|
139
|
+
}
|