@promptbook/cli 0.112.0-101 → 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.
Files changed (78) hide show
  1. package/apps/agents-server/README.md +6 -0
  2. package/apps/agents-server/package.json +1 -1
  3. package/apps/agents-server/scripts/prerender-homepage.js +76 -1
  4. package/apps/agents-server/src/app/actions.ts +0 -6
  5. package/apps/agents-server/src/app/admin/about/page.tsx +1 -1
  6. package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +365 -0
  7. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +3 -3
  8. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +12 -3
  9. package/apps/agents-server/src/app/admin/usage/UsageClientTimelineChart.tsx +1 -1
  10. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +21 -14
  11. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatPageLayout.tsx +2 -2
  12. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatSidebarDefault.tsx +11 -7
  13. package/apps/agents-server/src/app/api/admin/cli-access/route.ts +27 -123
  14. package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +33 -125
  15. package/apps/agents-server/src/app/api/auth/login/route.ts +0 -10
  16. package/apps/agents-server/src/app/api/auth/shibboleth/acs/route.ts +77 -57
  17. package/apps/agents-server/src/app/api/auth/shibboleth/login/route.ts +57 -33
  18. package/apps/agents-server/src/app/api/auth/shibboleth/metadata/route.ts +4 -29
  19. package/apps/agents-server/src/app/api/auth/shibboleth/status/route.ts +17 -0
  20. package/apps/agents-server/src/app/api/upload/route.ts +230 -18
  21. package/apps/agents-server/src/app/api/users/[username]/route.ts +1 -1
  22. package/apps/agents-server/src/app/api/users/route.ts +5 -5
  23. package/apps/agents-server/src/app/dashboard/page.tsx +1 -1
  24. package/apps/agents-server/src/app/docs/[docId]/page.tsx +1 -1
  25. package/apps/agents-server/src/app/docs/page.tsx +1 -1
  26. package/apps/agents-server/src/app/globals.css +100 -0
  27. package/apps/agents-server/src/app/layout.tsx +7 -0
  28. package/apps/agents-server/src/app/recycle-bin/page.tsx +1 -1
  29. package/apps/agents-server/src/app/system/settings/KeybindingsSettingsClient.tsx +13 -7
  30. package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +29 -1
  31. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +3 -3
  32. package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +8 -2
  33. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +4 -4
  34. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +9 -9
  35. package/apps/agents-server/src/components/Footer/Footer.tsx +7 -7
  36. package/apps/agents-server/src/components/Header/Header.tsx +24 -4
  37. package/apps/agents-server/src/components/Header/HeaderTypes.ts +6 -0
  38. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +51 -1
  39. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  40. package/apps/agents-server/src/components/Homepage/Section.tsx +3 -1
  41. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +12 -1
  42. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +100 -149
  43. package/apps/agents-server/src/components/Skeleton/ConsolePageLoadingSkeleton.tsx +1 -1
  44. package/apps/agents-server/src/components/Skeleton/DocumentationRouteLoadingSkeleton.tsx +1 -1
  45. package/apps/agents-server/src/components/Skeleton/HomepageLoadingSkeleton.tsx +1 -1
  46. package/apps/agents-server/src/components/UsersList/UsersList.tsx +20 -4
  47. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +3 -0
  48. package/apps/agents-server/src/constants/shibbolethAuth.ts +139 -0
  49. package/apps/agents-server/src/database/metadataDefaults.ts +54 -80
  50. package/apps/agents-server/src/database/migrate.ts +30 -1
  51. package/apps/agents-server/src/database/migrations/2026-06-0100-shibboleth-auth.sql +136 -0
  52. package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +88 -36
  53. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +4 -2
  54. package/apps/agents-server/src/languages/translations/czech.yaml +4 -2
  55. package/apps/agents-server/src/languages/translations/english.yaml +5 -3
  56. package/apps/agents-server/src/tools/$provideCdnForServer.ts +69 -23
  57. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +54 -6
  58. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +4 -6
  59. package/apps/agents-server/src/utils/cdn/resolveCdnStorageProvider.ts +40 -0
  60. package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +11 -0
  61. package/apps/agents-server/src/utils/createAdminTerminalRouteHandlers.ts +264 -0
  62. package/apps/agents-server/src/utils/shareTargetPayloads.ts +11 -10
  63. package/apps/agents-server/src/utils/shibbolethAuthentication.ts +729 -621
  64. package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +137 -19
  65. package/esm/index.es.js +1 -1
  66. package/esm/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  67. package/esm/src/version.d.ts +1 -1
  68. package/package.json +2 -2
  69. package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +65 -4
  70. package/src/other/templates/getTemplatesPipelineCollection.ts +788 -719
  71. package/src/version.ts +2 -2
  72. package/src/versions.txt +1 -0
  73. package/umd/index.umd.js +1 -1
  74. package/umd/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  75. package/umd/src/version.d.ts +1 -1
  76. package/apps/agents-server/src/app/api/auth/methods/route.ts +0 -44
  77. package/apps/agents-server/src/constants/authenticationMethods.ts +0 -74
  78. 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 { KeyRound, Loader2, Lock, User } from 'lucide-react';
4
+ import { Loader2, Lock, ShieldCheck, User } from 'lucide-react';
5
5
  import { SecretInput } from '@/src/components/SecretInput/SecretInput';
6
- import { usePathname, useRouter, useSearchParams } from 'next/navigation';
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
- * Authentication-method payload returned by `/api/auth/methods`.
39
+ * Public Shibboleth status returned by the login status endpoint.
40
40
  */
41
- type AuthenticationMethodsResponse = {
42
- /**
43
- * Enabled authentication methods.
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/methods')
79
+ fetch('/api/auth/shibboleth/status')
122
80
  .then((response) => response.json())
123
- .then((data: AuthenticationMethodsResponse) => {
124
- const methods = data.methods || ['PASSWORD'];
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 authentication methods:', error);
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
- {loginMethods.isShibbolethEnabled && (
186
- <a
187
- href={shibbolethLoginHref}
188
- 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 transition-colors"
189
- >
190
- <KeyRound className="mr-2 w-4 h-4" />
191
- {t('login.shibbolethAction', { provider: loginMethods.shibbolethProviderLabel })}
192
- </a>
193
- )}
194
-
195
- {loginMethods.isPasswordEnabled && loginMethods.isShibbolethEnabled && (
196
- <div className="flex items-center gap-3 text-xs font-semibold uppercase text-gray-400">
197
- <span className="h-px flex-1 bg-gray-200" />
198
- <span>{t('login.methodDivider')}</span>
199
- <span className="h-px flex-1 bg-gray-200" />
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
- {loginMethods.isPasswordEnabled && (
204
- <>
205
- <div className="space-y-2">
206
- <label htmlFor="username" className="text-sm font-medium text-gray-700 block">
207
- {t('login.usernameLabel')}
208
- </label>
209
- <div className="relative">
210
- <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
211
- <User className="w-4 h-4" />
212
- </div>
213
- <input
214
- id="username"
215
- name="username"
216
- type="text"
217
- value={username}
218
- onChange={(event) => setUsername(event.target.value)}
219
- required
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="submit"
247
- disabled={isLoading}
248
- 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"
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
- {isLoading ? (
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
- <div className="flex justify-between text-sm">
261
- <button
262
- type="button"
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
- {!loginMethods.isPasswordEnabled && error && (
280
- <div className="p-3 text-sm text-red-500 bg-red-50 border border-red-200 rounded-md">{error}</div>
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
- {user.isAdmin && (
73
- <span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded mt-1">
74
- {t('users.adminRole')}
75
- </span>
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
+ }