@promptbook/cli 0.112.0-101 → 0.112.0-103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/apps/agents-server/package.json +1 -1
  2. package/apps/agents-server/scripts/prerender-homepage.js +76 -1
  3. package/apps/agents-server/src/app/actions.ts +0 -6
  4. package/apps/agents-server/src/app/admin/about/page.tsx +1 -1
  5. package/apps/agents-server/src/app/admin/image-generator-test/ImageAttachmentsEditor.tsx +11 -6
  6. package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +365 -0
  7. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +13 -15
  8. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +3 -3
  9. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +13 -14
  10. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +12 -3
  11. package/apps/agents-server/src/app/admin/usage/UsageClientTimelineChart.tsx +1 -1
  12. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +21 -14
  13. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatPageLayout.tsx +2 -2
  14. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatSidebarDefault.tsx +11 -7
  15. package/apps/agents-server/src/app/api/admin/cli-access/route.ts +27 -123
  16. package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +33 -125
  17. package/apps/agents-server/src/app/api/auth/login/route.ts +0 -10
  18. package/apps/agents-server/src/app/api/auth/shibboleth/acs/route.ts +77 -57
  19. package/apps/agents-server/src/app/api/auth/shibboleth/login/route.ts +57 -33
  20. package/apps/agents-server/src/app/api/auth/shibboleth/metadata/route.ts +4 -29
  21. package/apps/agents-server/src/app/api/auth/shibboleth/status/route.ts +17 -0
  22. package/apps/agents-server/src/app/api/upload/route.ts +148 -209
  23. package/apps/agents-server/src/app/api/users/[username]/route.ts +1 -1
  24. package/apps/agents-server/src/app/api/users/route.ts +5 -5
  25. package/apps/agents-server/src/app/dashboard/page.tsx +1 -1
  26. package/apps/agents-server/src/app/docs/[docId]/page.tsx +1 -1
  27. package/apps/agents-server/src/app/docs/page.tsx +1 -1
  28. package/apps/agents-server/src/app/globals.css +100 -0
  29. package/apps/agents-server/src/app/layout.tsx +7 -0
  30. package/apps/agents-server/src/app/recycle-bin/page.tsx +1 -1
  31. package/apps/agents-server/src/app/system/settings/KeybindingsSettingsClient.tsx +13 -7
  32. package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +29 -1
  33. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +3 -3
  34. package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +8 -2
  35. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +4 -4
  36. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +9 -9
  37. package/apps/agents-server/src/components/Footer/Footer.tsx +7 -7
  38. package/apps/agents-server/src/components/Header/Header.tsx +24 -4
  39. package/apps/agents-server/src/components/Header/HeaderTypes.ts +6 -0
  40. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +51 -1
  41. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  42. package/apps/agents-server/src/components/Homepage/Section.tsx +3 -1
  43. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +12 -1
  44. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +100 -149
  45. package/apps/agents-server/src/components/Skeleton/ConsolePageLoadingSkeleton.tsx +1 -1
  46. package/apps/agents-server/src/components/Skeleton/DocumentationRouteLoadingSkeleton.tsx +1 -1
  47. package/apps/agents-server/src/components/Skeleton/HomepageLoadingSkeleton.tsx +1 -1
  48. package/apps/agents-server/src/components/UsersList/UsersList.tsx +20 -4
  49. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +3 -0
  50. package/apps/agents-server/src/constants/shibbolethAuth.ts +139 -0
  51. package/apps/agents-server/src/database/metadataDefaults.ts +54 -80
  52. package/apps/agents-server/src/database/migrate.ts +30 -1
  53. package/apps/agents-server/src/database/migrations/2026-06-0100-shibboleth-auth.sql +136 -0
  54. package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +88 -36
  55. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +4 -2
  56. package/apps/agents-server/src/languages/translations/czech.yaml +4 -2
  57. package/apps/agents-server/src/languages/translations/english.yaml +5 -3
  58. package/apps/agents-server/src/tools/$provideCdnForServer.ts +54 -11
  59. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +18 -2
  60. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +6 -5
  61. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +5 -0
  62. package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +11 -0
  63. package/apps/agents-server/src/utils/createAdminTerminalRouteHandlers.ts +264 -0
  64. package/apps/agents-server/src/utils/shareTargetPayloads.ts +19 -66
  65. package/apps/agents-server/src/utils/shibbolethAuthentication.ts +729 -621
  66. package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +19 -28
  67. package/apps/agents-server/src/utils/upload/uploadFileToServer.ts +113 -0
  68. package/esm/index.es.js +194 -35
  69. package/esm/index.es.js.map +1 -1
  70. package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
  71. package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  72. package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  73. package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
  74. package/esm/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  75. package/esm/src/version.d.ts +1 -1
  76. package/package.json +2 -2
  77. package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +63 -4
  78. package/src/other/templates/getTemplatesPipelineCollection.ts +730 -739
  79. package/src/version.ts +2 -2
  80. package/src/versions.txt +2 -0
  81. package/umd/index.umd.js +194 -35
  82. package/umd/index.umd.js.map +1 -1
  83. package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
  84. package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  85. package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  86. package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
  87. package/umd/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  88. package/umd/src/version.d.ts +1 -1
  89. package/apps/agents-server/src/app/api/auth/methods/route.ts +0 -44
  90. package/apps/agents-server/src/constants/authenticationMethods.ts +0 -74
  91. package/apps/agents-server/src/constants/shibbolethAuthentication.ts +0 -107
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { upload } from '@vercel/blob/client';
4
3
  import { useCallback, useMemo, useRef, useState, type ChangeEvent, type RefObject } from 'react';
5
4
  import { showAlert } from '../../../components/AsyncDialogs/asyncDialogs';
6
5
  import { useDirtyModalGuard } from '../../../components/utils/useDirtyModalGuard';
@@ -8,6 +7,7 @@ import { buildServerTablePrefix } from '../../../utils/buildServerTablePrefix';
8
7
  import type { ChatFeedbackMode } from '../../../utils/chatFeedbackMode';
9
8
  import { getSafeCdnPath } from '../../../utils/cdn/utils/getSafeCdnPath';
10
9
  import { normalizeUploadFilename } from '../../../utils/normalization/normalizeUploadFilename';
10
+ import { buildDefaultUserFileUploadPath, uploadFileToServer } from '../../../utils/upload/uploadFileToServer';
11
11
  import type { ManagedServerEnvironment } from './useServersRegistryState';
12
12
 
13
13
  /**
@@ -425,22 +425,21 @@ export function useCreateServerWizard(options: UseCreateServerWizardOptions): Us
425
425
  setIsUploadingIcon(true);
426
426
  setWizardError(null);
427
427
 
428
- const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
429
428
  const normalizedFilename = normalizeUploadFilename(file.name);
430
- const uploadPath = pathPrefix
431
- ? `${pathPrefix}/user/files/${normalizedFilename}`
432
- : `user/files/${normalizedFilename}`;
433
-
434
- const blob = await upload(getSafeCdnPath({ pathname: uploadPath }), file, {
435
- access: 'public',
436
- handleUploadUrl: '/api/upload',
437
- clientPayload: JSON.stringify({
438
- purpose: 'SERVER_ICON',
439
- contentType: file.type,
440
- }),
429
+ const uploadPath = buildDefaultUserFileUploadPath(normalizedFilename);
430
+ const safeUploadPath = getSafeCdnPath({
431
+ pathname: uploadPath,
432
+ pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX,
441
433
  });
442
434
 
443
- updateWizardField('iconUrl', blob.url);
435
+ const uploadResult = await uploadFileToServer({
436
+ file,
437
+ pathname: safeUploadPath,
438
+ purpose: 'SERVER_ICON',
439
+ contentType: file.type,
440
+ });
441
+
442
+ updateWizardField('iconUrl', uploadResult.url);
444
443
  } catch (uploadError) {
445
444
  setWizardError({
446
445
  message: uploadError instanceof Error ? uploadError.message : 'Failed to upload the server icon.',
@@ -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
- <pre className="max-h-[28rem] overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100">
410
- {overview?.job.logTail || 'No persisted update log output yet.'}
411
- </pre>
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>
@@ -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) {