@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
@@ -24,11 +24,11 @@ export function AgentChatPageLayout({
24
24
  children,
25
25
  }: AgentChatPageLayoutProps) {
26
26
  if (isHeadlessMode) {
27
- return <div className="flex h-full min-h-0 w-full overflow-hidden bg-slate-50/80">{children}</div>;
27
+ return <div className="flex h-full min-h-0 w-full overflow-hidden bg-slate-50/80 dark:bg-slate-950">{children}</div>;
28
28
  }
29
29
 
30
30
  return (
31
- <div className="flex h-full min-h-0 w-full overflow-hidden bg-slate-50/80">
31
+ <div className="flex h-full min-h-0 w-full overflow-hidden bg-slate-50/80 dark:bg-slate-950">
32
32
  {sidebar}
33
33
  <section className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">{children}</section>
34
34
  </div>
@@ -183,7 +183,7 @@ function AgentChatSidebarDefaultCollapsedRow({
183
183
  onClick={() => onChatSelect(item.id)}
184
184
  className={`group relative flex w-full min-w-0 flex-col items-center gap-1 rounded-2xl border px-1.5 py-2 transition focus-visible:outline focus-visible:outline-blue-400 focus-visible:outline-offset-2 ${
185
185
  item.isActive
186
- ? 'border-blue-300 bg-blue-50 text-blue-700 shadow-sm dark:border-blue-500/40 dark:bg-blue-500/12 dark:text-blue-100'
186
+ ? 'agent-chat-sidebar-item-active border-blue-300 bg-blue-50 text-blue-700 shadow-sm dark:border-blue-500/40 dark:bg-blue-500/12 dark:text-blue-100'
187
187
  : 'border-transparent bg-slate-100/80 text-slate-700 hover:border-slate-300 hover:bg-slate-100 dark:bg-slate-900/88 dark:text-slate-300 dark:hover:border-slate-700 dark:hover:bg-slate-900'
188
188
  } ${item.isEmpty && !item.isActive ? 'opacity-40' : ''}`}
189
189
  aria-label={item.content.accessibilityLabel}
@@ -206,14 +206,16 @@ function AgentChatSidebarDefaultCollapsedRow({
206
206
  {item.content.messagesCount}
207
207
  </span>
208
208
  <div
209
- className={`aspect-square w-full overflow-hidden rounded-xl border px-1.5 py-1.5 text-left ${
209
+ className={`agent-chat-sidebar-item-preview-card aspect-square w-full overflow-hidden rounded-xl border px-1.5 py-1.5 text-left ${
210
210
  item.isActive
211
211
  ? 'border-blue-300 bg-white/90 text-blue-700 dark:border-blue-500/40 dark:bg-slate-950/82 dark:text-blue-100'
212
212
  : 'border-slate-200 bg-white/90 text-slate-600 dark:border-slate-700 dark:bg-slate-950/78 dark:text-slate-300'
213
213
  }`}
214
214
  >
215
- <div className="max-w-full truncate text-[10px] font-semibold leading-none">{item.content.title}</div>
216
- <div className="mt-1 max-w-full truncate text-[9px] leading-tight text-slate-500 dark:text-slate-400">
215
+ <div className="agent-chat-sidebar-item-title max-w-full truncate text-[10px] font-semibold leading-none">
216
+ {item.content.title}
217
+ </div>
218
+ <div className="agent-chat-sidebar-item-preview mt-1 max-w-full truncate text-[9px] leading-tight text-slate-500 dark:text-slate-400">
217
219
  {item.content.preview}
218
220
  </div>
219
221
  </div>
@@ -241,7 +243,7 @@ function AgentChatSidebarDefaultExpandedRow({
241
243
  <div
242
244
  className={`group relative rounded-xl border ${
243
245
  item.isActive
244
- ? 'border-blue-300 bg-blue-50 shadow-sm dark:border-blue-500/40 dark:bg-blue-500/12'
246
+ ? 'agent-chat-sidebar-item-active border-blue-300 bg-blue-50 shadow-sm dark:border-blue-500/40 dark:bg-blue-500/12'
245
247
  : 'border-transparent hover:border-slate-200 hover:bg-slate-100/80 dark:hover:border-slate-700 dark:hover:bg-slate-900/88'
246
248
  } ${item.isEmpty && !item.isActive ? 'opacity-40' : ''}`}
247
249
  >
@@ -256,7 +258,7 @@ function AgentChatSidebarDefaultExpandedRow({
256
258
  title={item.content.accessibilityLabel}
257
259
  >
258
260
  <div className="flex items-center gap-2">
259
- <div className="min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-slate-100">
261
+ <div className="agent-chat-sidebar-item-title min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-slate-100">
260
262
  {item.content.title}
261
263
  </div>
262
264
  {item.content.sourceChipLabel && (
@@ -265,7 +267,9 @@ function AgentChatSidebarDefaultExpandedRow({
265
267
  </span>
266
268
  )}
267
269
  </div>
268
- <div className="mt-1 truncate text-xs text-slate-500 dark:text-slate-400">{item.content.preview}</div>
270
+ <div className="agent-chat-sidebar-item-preview mt-1 truncate text-xs text-slate-500 dark:text-slate-400">
271
+ {item.content.preview}
272
+ </div>
269
273
  <div className="mt-2 flex items-center justify-between gap-2">
270
274
  <div className={`truncate text-[11px] ${statusClassName}`}>
271
275
  {item.content.activityIndicator.compactLabel || item.content.lastActivity}
@@ -1,6 +1,4 @@
1
- import { NextResponse } from 'next/server';
2
- import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
3
- import { createInteractiveTerminalEventStream } from '@/src/utils/createInteractiveTerminalEventStream';
1
+ import { createAdminTerminalRouteHandlers } from '@/src/utils/createAdminTerminalRouteHandlers';
4
2
  import {
5
3
  getLatestServerCliAccessSession,
6
4
  getServerCliAccessSession,
@@ -14,124 +12,30 @@ export const runtime = 'nodejs';
14
12
  export const dynamic = 'force-dynamic';
15
13
 
16
14
  /**
17
- * Loads the latest CLI access session or streams a specific terminal session.
15
+ * Shared route handlers for the raw server shell terminal.
18
16
  */
19
- export async function GET(request: Request) {
20
- if (!(await isUserGlobalAdmin())) {
21
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
22
- }
23
-
24
- try {
25
- const { searchParams } = new URL(request.url);
26
- const sessionId = searchParams.get('sessionId')?.trim() || '';
27
- const isStreamRequested = searchParams.get('stream') === '1';
28
-
29
- if (isStreamRequested) {
30
- if (!sessionId) {
31
- return NextResponse.json({ error: 'CLI access session id is required.' }, { status: 400 });
32
- }
33
-
34
- const session = getServerCliAccessSession(sessionId);
35
- if (!session) {
36
- return NextResponse.json({ error: 'CLI access session was not found.' }, { status: 404 });
37
- }
38
-
39
- return createInteractiveTerminalEventStream(
40
- request,
41
- sessionId,
42
- session,
43
- subscribeToServerCliAccessSession,
44
- );
45
- }
46
-
47
- return NextResponse.json({
48
- session: getLatestServerCliAccessSession(),
49
- });
50
- } catch (error) {
51
- return NextResponse.json(
52
- { error: error instanceof Error ? error.message : 'Failed to load the CLI access session.' },
53
- { status: 500 },
54
- );
55
- }
56
- }
57
-
58
- /**
59
- * Starts or reconnects to the raw server shell exposed in the browser.
60
- */
61
- export async function POST() {
62
- if (!(await isUserGlobalAdmin())) {
63
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
64
- }
65
-
66
- try {
67
- return NextResponse.json({
68
- session: await startServerCliAccessSession(),
69
- });
70
- } catch (error) {
71
- return NextResponse.json(
72
- { error: error instanceof Error ? error.message : 'Failed to start the CLI access session.' },
73
- { status: 500 },
74
- );
75
- }
76
- }
77
-
78
- /**
79
- * Sends raw input to the running CLI access shell.
80
- */
81
- export async function PATCH(request: Request) {
82
- if (!(await isUserGlobalAdmin())) {
83
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
84
- }
85
-
86
- try {
87
- const body = (await request.json().catch(() => null)) as
88
- | {
89
- readonly sessionId?: string;
90
- readonly input?: string;
91
- }
92
- | null;
93
-
94
- if (!body?.sessionId || typeof body.input !== 'string') {
95
- return NextResponse.json({ error: 'CLI access session input is required.' }, { status: 400 });
96
- }
97
-
98
- return NextResponse.json({
99
- session: writeServerCliAccessSessionInput(body.sessionId, body.input),
100
- });
101
- } catch (error) {
102
- return NextResponse.json(
103
- { error: error instanceof Error ? error.message : 'Failed to send CLI access input.' },
104
- { status: 500 },
105
- );
106
- }
107
- }
108
-
109
- /**
110
- * Stops one running CLI access shell session.
111
- */
112
- export async function DELETE(request: Request) {
113
- if (!(await isUserGlobalAdmin())) {
114
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
115
- }
116
-
117
- try {
118
- const body = (await request.json().catch(() => null)) as
119
- | {
120
- readonly sessionId?: string;
121
- }
122
- | null;
123
-
124
- if (!body?.sessionId) {
125
- return NextResponse.json({ error: 'CLI access session id is required.' }, { status: 400 });
126
- }
127
-
128
- return NextResponse.json({
129
- session: stopServerCliAccessSession(body.sessionId),
130
- });
131
- } catch (error) {
132
- return NextResponse.json(
133
- { error: error instanceof Error ? error.message : 'Failed to stop the CLI access session.' },
134
- { status: 500 },
135
- );
136
- }
137
- }
17
+ const cliAccessTerminalRouteHandlers = createAdminTerminalRouteHandlers(
18
+ {
19
+ getLatestSession: getLatestServerCliAccessSession,
20
+ getSession: getServerCliAccessSession,
21
+ startSession: startServerCliAccessSession,
22
+ writeSessionInput: writeServerCliAccessSessionInput,
23
+ stopSession: stopServerCliAccessSession,
24
+ subscribeToSession: subscribeToServerCliAccessSession,
25
+ },
26
+ {
27
+ loadErrorMessage: 'Failed to load the CLI access session.',
28
+ missingStreamSessionIdMessage: 'CLI access session id is required.',
29
+ sessionNotFoundMessage: 'CLI access session was not found.',
30
+ startErrorMessage: 'Failed to start the CLI access session.',
31
+ missingInputMessage: 'CLI access session input is required.',
32
+ sendErrorMessage: 'Failed to send CLI access input.',
33
+ missingStopSessionIdMessage: 'CLI access session id is required.',
34
+ stopErrorMessage: 'Failed to stop the CLI access session.',
35
+ },
36
+ );
37
+
38
+ export const GET = cliAccessTerminalRouteHandlers.GET;
39
+ export const POST = cliAccessTerminalRouteHandlers.POST;
40
+ export const PATCH = cliAccessTerminalRouteHandlers.PATCH;
41
+ export const DELETE = cliAccessTerminalRouteHandlers.DELETE;
@@ -1,6 +1,3 @@
1
- import { NextResponse } from 'next/server';
2
- import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
3
- import { createInteractiveTerminalEventStream } from '@/src/utils/createInteractiveTerminalEventStream';
4
1
  import {
5
2
  getCodeRunnerAuthenticationSession,
6
3
  getLatestCodeRunnerAuthenticationSession,
@@ -9,132 +6,43 @@ import {
9
6
  subscribeToCodeRunnerAuthenticationSession,
10
7
  writeCodeRunnerAuthenticationSessionInput,
11
8
  } from '@/src/utils/codeRunnerAuthentication';
9
+ import { createAdminTerminalRouteHandlers } from '@/src/utils/createAdminTerminalRouteHandlers';
12
10
  import { readConfiguredCodeRunner } from '@/src/utils/codeRunnerConfiguration';
13
11
 
14
12
  export const runtime = 'nodejs';
15
13
  export const dynamic = 'force-dynamic';
16
14
 
17
15
  /**
18
- * Loads the latest authentication session for the saved runner or streams a specific session.
16
+ * Shared route handlers for the code-runner authentication terminal.
19
17
  */
20
- export async function GET(request: Request) {
21
- if (!(await isUserGlobalAdmin())) {
22
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
23
- }
24
-
25
- try {
26
- const { searchParams } = new URL(request.url);
27
- const sessionId = searchParams.get('sessionId')?.trim() || '';
28
- const isStreamRequested = searchParams.get('stream') === '1';
29
-
30
- if (isStreamRequested) {
31
- if (!sessionId) {
32
- return NextResponse.json({ error: 'Authentication session id is required.' }, { status: 400 });
33
- }
34
-
35
- const session = getCodeRunnerAuthenticationSession(sessionId);
36
- if (!session) {
37
- return NextResponse.json({ error: 'Authentication session was not found.' }, { status: 404 });
38
- }
39
-
40
- return createInteractiveTerminalEventStream(
41
- request,
42
- sessionId,
43
- session,
44
- subscribeToCodeRunnerAuthenticationSession,
45
- );
46
- }
47
-
48
- const { agent } = await readConfiguredCodeRunner();
49
- return NextResponse.json({
50
- session: getLatestCodeRunnerAuthenticationSession(agent),
51
- });
52
- } catch (error) {
53
- return NextResponse.json(
54
- { error: error instanceof Error ? error.message : 'Failed to load the authentication session.' },
55
- { status: 500 },
56
- );
57
- }
58
- }
59
-
60
- /**
61
- * Starts a new browser-driven authentication terminal for the saved runner.
62
- */
63
- export async function POST() {
64
- if (!(await isUserGlobalAdmin())) {
65
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
66
- }
67
-
68
- try {
69
- const { agent } = await readConfiguredCodeRunner();
70
- return NextResponse.json({
71
- session: await startCodeRunnerAuthenticationSession(agent),
72
- });
73
- } catch (error) {
74
- return NextResponse.json(
75
- { error: error instanceof Error ? error.message : 'Failed to start the authentication session.' },
76
- { status: 500 },
77
- );
78
- }
79
- }
80
-
81
- /**
82
- * Sends terminal input to a running authentication session.
83
- */
84
- export async function PATCH(request: Request) {
85
- if (!(await isUserGlobalAdmin())) {
86
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
87
- }
88
-
89
- try {
90
- const body = (await request.json().catch(() => null)) as
91
- | {
92
- readonly sessionId?: string;
93
- readonly input?: string;
94
- }
95
- | null;
96
-
97
- if (!body?.sessionId || typeof body.input !== 'string') {
98
- return NextResponse.json({ error: 'Authentication session input is required.' }, { status: 400 });
99
- }
100
-
101
- return NextResponse.json({
102
- session: writeCodeRunnerAuthenticationSessionInput(body.sessionId, body.input),
103
- });
104
- } catch (error) {
105
- return NextResponse.json(
106
- { error: error instanceof Error ? error.message : 'Failed to send authentication input.' },
107
- { status: 500 },
108
- );
109
- }
110
- }
111
-
112
- /**
113
- * Stops one authentication terminal from the admin UI.
114
- */
115
- export async function DELETE(request: Request) {
116
- if (!(await isUserGlobalAdmin())) {
117
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
118
- }
119
-
120
- try {
121
- const body = (await request.json().catch(() => null)) as
122
- | {
123
- readonly sessionId?: string;
124
- }
125
- | null;
126
-
127
- if (!body?.sessionId) {
128
- return NextResponse.json({ error: 'Authentication session id is required.' }, { status: 400 });
129
- }
130
-
131
- return NextResponse.json({
132
- session: stopCodeRunnerAuthenticationSession(body.sessionId),
133
- });
134
- } catch (error) {
135
- return NextResponse.json(
136
- { error: error instanceof Error ? error.message : 'Failed to stop the authentication session.' },
137
- { status: 500 },
138
- );
139
- }
140
- }
18
+ const authenticationTerminalRouteHandlers = createAdminTerminalRouteHandlers(
19
+ {
20
+ async getLatestSession() {
21
+ const { agent } = await readConfiguredCodeRunner();
22
+ return getLatestCodeRunnerAuthenticationSession(agent);
23
+ },
24
+ getSession: getCodeRunnerAuthenticationSession,
25
+ async startSession() {
26
+ const { agent } = await readConfiguredCodeRunner();
27
+ return startCodeRunnerAuthenticationSession(agent);
28
+ },
29
+ writeSessionInput: writeCodeRunnerAuthenticationSessionInput,
30
+ stopSession: stopCodeRunnerAuthenticationSession,
31
+ subscribeToSession: subscribeToCodeRunnerAuthenticationSession,
32
+ },
33
+ {
34
+ loadErrorMessage: 'Failed to load the authentication session.',
35
+ missingStreamSessionIdMessage: 'Authentication session id is required.',
36
+ sessionNotFoundMessage: 'Authentication session was not found.',
37
+ startErrorMessage: 'Failed to start the authentication session.',
38
+ missingInputMessage: 'Authentication session input is required.',
39
+ sendErrorMessage: 'Failed to send authentication input.',
40
+ missingStopSessionIdMessage: 'Authentication session id is required.',
41
+ stopErrorMessage: 'Failed to stop the authentication session.',
42
+ },
43
+ );
44
+
45
+ export const GET = authenticationTerminalRouteHandlers.GET;
46
+ export const POST = authenticationTerminalRouteHandlers.POST;
47
+ export const PATCH = authenticationTerminalRouteHandlers.PATCH;
48
+ export const DELETE = authenticationTerminalRouteHandlers.DELETE;
@@ -1,11 +1,6 @@
1
1
  import { authenticateUser } from '../../../../utils/authenticateUser';
2
2
  import { setSession } from '../../../../utils/session';
3
3
  import { NextResponse } from 'next/server';
4
- import {
5
- AUTHENTICATION_METHODS_METADATA_KEY,
6
- isAuthenticationMethodEnabled,
7
- } from '../../../../constants/authenticationMethods';
8
- import { getMetadata } from '../../../../database/getMetadata';
9
4
 
10
5
  /**
11
6
  * Handles post.
@@ -19,11 +14,6 @@ export async function POST(request: Request) {
19
14
  return NextResponse.json({ error: 'Username and password are required' }, { status: 400 });
20
15
  }
21
16
 
22
- if (!isAuthenticationMethodEnabled(await getMetadata(AUTHENTICATION_METHODS_METADATA_KEY), 'PASSWORD')) {
23
- console.info('Password login rejected because PASSWORD authentication is disabled in metadata.');
24
- return NextResponse.json({ error: 'Password login is disabled on this server' }, { status: 403 });
25
- }
26
-
27
17
  const user = await authenticateUser(username, password);
28
18
 
29
19
  if (user) {
@@ -1,92 +1,112 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { setSession } from '../../../../../utils/session';
1
+ import { revalidatePath } from 'next/cache';
2
+ import { NextResponse } from 'next/server';
3
3
  import {
4
- createShibbolethProfileLogDetails,
5
4
  createShibbolethSamlClient,
6
- loadShibbolethConfiguration,
7
- logShibbolethAuthenticationEvent,
8
- resolveSafeShibbolethRelayState,
9
- resolveShibbolethUser,
10
- ShibbolethConfigurationError,
5
+ findOrCreateShibbolethUser,
6
+ getShibbolethRequestDetails,
7
+ recordShibbolethAuthenticationAttempt,
8
+ resolveShibbolethAuthenticationConfiguration,
9
+ sanitizeShibbolethRelayState,
11
10
  } from '../../../../../utils/shibbolethAuthentication';
12
-
13
- /**
14
- * Forces this route to run in Node.js because node-saml depends on Node crypto/zlib APIs.
15
- */
16
- export const runtime = 'nodejs';
11
+ import { setSession } from '../../../../../utils/session';
17
12
 
18
13
  /**
19
14
  * Handles post.
20
15
  */
21
- export async function POST(request: NextRequest) {
22
- const configuration = await loadShibbolethConfiguration(request);
23
-
24
- if (!configuration.isEnabled) {
25
- logShibbolethAuthenticationEvent('acs_rejected_disabled');
26
- return NextResponse.json({ error: 'Shibboleth authentication is not enabled.' }, { status: 404 });
27
- }
28
-
29
- if (!configuration.isConfigured) {
30
- const error = new ShibbolethConfigurationError(configuration.missingConfiguration);
31
- logShibbolethAuthenticationEvent('acs_rejected_incomplete_configuration', {
32
- missingConfiguration: configuration.missingConfiguration,
33
- });
34
- return NextResponse.json({ error: error.message }, { status: 503 });
35
- }
16
+ export async function POST(request: Request) {
17
+ const requestDetails = getShibbolethRequestDetails(request);
18
+ let relayState = '/';
36
19
 
37
20
  try {
38
21
  const formData = await request.formData();
39
22
  const samlResponse = formData.get('SAMLResponse');
40
- const relayState = resolveSafeShibbolethRelayState(
41
- typeof formData.get('RelayState') === 'string' ? String(formData.get('RelayState')) : null,
42
- );
23
+ relayState = sanitizeShibbolethRelayState(formData.get('RelayState')?.toString());
43
24
 
44
- if (typeof samlResponse !== 'string' || samlResponse.trim() === '') {
45
- logShibbolethAuthenticationEvent('acs_rejected_missing_saml_response');
25
+ if (typeof samlResponse !== 'string' || !samlResponse) {
26
+ await recordShibbolethAuthenticationAttempt({
27
+ stage: 'ASSERTION_CONSUMER_SERVICE',
28
+ status: 'FAILED',
29
+ requestDetails,
30
+ relayState,
31
+ errorMessage: 'Missing SAMLResponse.',
32
+ });
46
33
  return NextResponse.json({ error: 'Missing SAMLResponse.' }, { status: 400 });
47
34
  }
48
35
 
49
- logShibbolethAuthenticationEvent('acs_response_received', {
50
- relayState,
51
- responseLength: samlResponse.length,
36
+ const configuration = await resolveShibbolethAuthenticationConfiguration({
37
+ requestUrl: request.url,
38
+ isIdentityProviderMetadataValidationEnabled: true,
52
39
  });
53
40
 
41
+ if (!configuration.isActive) {
42
+ await recordShibbolethAuthenticationAttempt({
43
+ stage: 'ASSERTION_CONSUMER_SERVICE',
44
+ status: 'REJECTED',
45
+ requestDetails,
46
+ relayState,
47
+ errorMessage: 'Shibboleth authentication is not active.',
48
+ });
49
+ return NextResponse.json({ error: 'Shibboleth authentication is not active.' }, { status: 404 });
50
+ }
51
+
52
+ if (!configuration.isConfigured) {
53
+ const errorMessage = configuration.errors.join(' ');
54
+ await recordShibbolethAuthenticationAttempt({
55
+ stage: 'ASSERTION_CONSUMER_SERVICE',
56
+ status: 'FAILED',
57
+ requestDetails,
58
+ relayState,
59
+ errorMessage,
60
+ });
61
+ return NextResponse.json({ error: errorMessage }, { status: 503 });
62
+ }
63
+
54
64
  const saml = createShibbolethSamlClient(configuration);
55
- const { profile } = await saml.validatePostResponseAsync({
56
- SAMLResponse: samlResponse,
57
- RelayState: relayState,
58
- });
65
+ const { profile } = await saml.validatePostResponseAsync({ SAMLResponse: samlResponse });
59
66
 
60
67
  if (!profile) {
61
- logShibbolethAuthenticationEvent('acs_rejected_empty_profile');
62
- return NextResponse.json({ error: 'Shibboleth did not return a user profile.' }, { status: 401 });
68
+ await recordShibbolethAuthenticationAttempt({
69
+ stage: 'ASSERTION_CONSUMER_SERVICE',
70
+ status: 'FAILED',
71
+ requestDetails,
72
+ relayState,
73
+ errorMessage: 'Shibboleth response did not include a user profile.',
74
+ });
75
+ return NextResponse.json({ error: 'Shibboleth response did not include a user profile.' }, { status: 401 });
63
76
  }
64
77
 
65
- logShibbolethAuthenticationEvent(
66
- 'acs_profile_validated',
67
- createShibbolethProfileLogDetails(profile, configuration.usernameAttribute),
68
- );
69
-
70
- const user = await resolveShibbolethUser(profile, configuration);
78
+ const linkedUser = await findOrCreateShibbolethUser(profile);
71
79
  await setSession({
72
- username: user.username,
73
- isAdmin: user.isAdmin,
80
+ username: linkedUser.user.username,
81
+ isAdmin: linkedUser.user.isAdmin,
74
82
  isGlobalAdmin: false,
75
83
  });
84
+ revalidatePath('/', 'layout');
76
85
 
77
- logShibbolethAuthenticationEvent('acs_session_created', {
78
- username: user.username,
79
- isAdmin: user.isAdmin,
80
- isNewUser: user.isNewUser,
86
+ await recordShibbolethAuthenticationAttempt({
87
+ stage: 'ASSERTION_CONSUMER_SERVICE',
88
+ status: 'SUCCESS',
89
+ requestDetails,
81
90
  relayState,
91
+ userId: linkedUser.user.id,
92
+ email: linkedUser.profileAttributes.email,
93
+ displayName: linkedUser.profileAttributes.displayName,
94
+ nameId: linkedUser.profileAttributes.nameId,
95
+ rawAttributes: linkedUser.profileAttributes.rawAttributes,
82
96
  });
83
97
 
84
98
  return NextResponse.redirect(new URL(relayState, request.url), 303);
85
99
  } catch (error) {
86
- logShibbolethAuthenticationEvent('acs_failed', {
87
- error: error instanceof Error ? error.message : String(error),
100
+ const errorMessage = error instanceof Error ? error.message : 'Failed to finish Shibboleth authentication.';
101
+ await recordShibbolethAuthenticationAttempt({
102
+ stage: 'ASSERTION_CONSUMER_SERVICE',
103
+ status: 'FAILED',
104
+ requestDetails,
105
+ relayState,
106
+ errorMessage,
88
107
  });
89
108
 
90
- return NextResponse.json({ error: 'Shibboleth authentication failed.' }, { status: 401 });
109
+ console.error('Shibboleth assertion consumer service error:', error);
110
+ return NextResponse.json({ error: errorMessage }, { status: 401 });
91
111
  }
92
112
  }