@promptbook/cli 0.114.0-0 → 0.114.0-2
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/README.md +16 -15
- package/apps/agents-server/package.json +6 -4
- package/apps/agents-server/playwright.config.ts +28 -38
- package/apps/agents-server/scripts/build-agents-server.js +62 -0
- package/apps/agents-server/scripts/build-e2e.js +38 -0
- package/apps/agents-server/scripts/ensure-root-dependencies.js +86 -0
- package/apps/agents-server/scripts/prerender-homepage.js +1 -1
- package/apps/agents-server/scripts/run-e2e-tests.js +34 -0
- package/apps/agents-server/scripts/run-npm.js +42 -0
- package/apps/agents-server/src/app/admin/email-server/page.tsx +14 -6
- package/apps/agents-server/src/app/api/elevenlabs/tts/route.ts +3 -3
- package/apps/agents-server/src/app/api/onboarding/book/route.ts +7 -6
- package/apps/agents-server/src/app/superadmin/servers/ServersRegistryTable.tsx +105 -102
- package/apps/agents-server/src/app/superadmin/servers/useServersRegistryState.ts +15 -7
- package/apps/agents-server/src/components/AgentProjectDnsInstructions/AgentProjectDnsInstructions.tsx +11 -11
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/ManGoBookEditor.tsx +82 -0
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/WizardShell.tsx +2 -2
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/steps/BookStep.tsx +13 -16
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/steps/ZadaniStep.tsx +4 -4
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/config/bookSections.ts +23 -16
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/lib/bookSource.ts +54 -0
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/bookService.ts +15 -8
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/createManGoAgentSource.ts +3 -37
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/createManGoOpenEditorRequest.ts +1 -1
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/state/OnboardingProvider.tsx +2 -2
- package/apps/agents-server/src/components/UsersList/CreateUserDialog.tsx +13 -22
- package/apps/agents-server/src/components/UsersList/PasswordGeneratorDialog.tsx +216 -0
- package/apps/agents-server/src/components/UsersList/UsersList.tsx +16 -11
- package/apps/agents-server/src/components/UsersList/generateSecurePassword.ts +112 -24
- package/apps/agents-server/src/components/UsersList/userDialogClassNames.ts +11 -0
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +11 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +11 -0
- package/apps/agents-server/src/languages/translations/english.yaml +11 -0
- package/apps/agents-server/src/utils/agentProjects/agentProjectHrefs.ts +11 -0
- package/apps/agents-server/src/utils/agentProjects/createAgentProjectDnsRecords.ts +53 -7
- package/apps/agents-server/src/utils/agentProjects/createAgentProjectMarkdownReferences.ts +37 -27
- package/apps/agents-server/src/utils/manGoOnboarding/manGoOnboardingAgentBooks.ts +2 -13
- package/apps/agents-server/src/utils/manGoOnboarding/manGoOnboardingAgentRuntime.ts +21 -73
- package/apps/agents-server/src/utils/stalwart/createEmailDnsInstructions.ts +9 -5
- package/apps/agents-server/src/utils/stalwart/readStalwartEmailSnapshot.ts +23 -5
- package/apps/agents-server/src/utils/stalwart/stalwartBootstrap.ts +2 -1
- package/apps/agents-server/src/utils/stalwart/stalwartMailBridge.ts +7 -0
- package/apps/agents-server/tests/e2e/e2eEnvironment.cjs +49 -0
- package/esm/index.es.js +523 -169
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-agent-chat/runCoderPing.d.ts +48 -0
- package/esm/scripts/run-codex-prompts/prompts/findNextTodoPrompt.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/prompts/isPromptCompatibleWithRunner.d.ts +27 -0
- package/esm/scripts/run-codex-prompts/prompts/listRunnablePrompts.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/prompts/listUpcomingTasks.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/prompts/replacePromptTodoStatusLine.d.ts +7 -0
- package/esm/scripts/run-codex-prompts/testing/TestBeforeMode.d.ts +16 -0
- package/esm/scripts/run-codex-prompts/testing/createTestBeforeRepairPrompt.d.ts +9 -0
- package/esm/scripts/run-codex-prompts/testing/limitTestOutput.d.ts +4 -0
- package/esm/scripts/run-codex-prompts/testing/runPromptTestCommand.d.ts +1 -2
- package/esm/scripts/run-codex-prompts/testing/runTestBefore.d.ts +24 -0
- package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -0
- package/esm/src/book-2.0/book-language-documentation/BookLanguageManualDictionary.d.ts +1 -15
- package/esm/src/book-2.0/book-language-documentation/getBookLanguageManualCommitmentGroups.d.ts +1 -1
- package/esm/src/book-2.0/book-language-documentation/renderCommitmentCatalogSection.d.ts +0 -4
- package/esm/src/book-components/Chat/utils/renderMarkdown.d.ts +7 -0
- package/esm/src/cli/cli-commands/coder/agentCodingFile.d.ts +2 -3
- package/esm/src/cli/cli-commands/coder/ping.d.ts +13 -0
- package/esm/src/cli/cli-commands/coder/ping.test.d.ts +1 -0
- package/esm/src/cli/cli-commands/coder.d.ts +1 -0
- package/package.json +1 -1
- package/src/book-2.0/book-language-documentation/BookLanguageManualDictionary.ts +1 -15
- package/src/book-2.0/book-language-documentation/bookLanguageDocumentationExamples.ts +1 -2
- package/src/book-2.0/book-language-documentation/createStandaloneBookLanguageMarkdown.ts +92 -32
- package/src/book-2.0/book-language-documentation/czechBookLanguageManualDictionary.ts +16 -39
- package/src/book-2.0/book-language-documentation/englishBookLanguageManualDictionary.ts +16 -36
- package/src/book-2.0/book-language-documentation/getBookLanguageManualCommitmentGroups.ts +9 -1
- package/src/book-2.0/book-language-documentation/renderCommitmentCatalogSection.ts +4 -51
- package/src/book-2.0/book-language-documentation/renderGroupedCommitmentDocumentationMarkdown.ts +291 -4
- package/src/book-components/Chat/utils/renderMarkdown.ts +133 -9
- package/src/cli/cli-commands/agents-server/startAgentsServer/createLocalAgentRunOptions.ts +1 -0
- package/src/cli/cli-commands/coder/agentCodingFile.ts +6 -129
- package/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.ts +1 -1
- package/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.ts +1 -4
- package/src/cli/cli-commands/coder/ping.ts +94 -0
- package/src/cli/cli-commands/coder/run.ts +25 -2
- package/src/cli/cli-commands/coder.ts +4 -0
- package/src/other/templates/getTemplatesPipelineCollection.ts +805 -842
- package/src/utils/agent-message-runtime/resolveAgentMessageRuntimeActivity.ts +101 -0
- package/src/version.ts +1 -1
- package/src/versions.txt +1 -1
- package/umd/index.umd.js +525 -171
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-agent-chat/runCoderPing.d.ts +48 -0
- package/umd/scripts/run-codex-prompts/prompts/findNextTodoPrompt.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/prompts/isPromptCompatibleWithRunner.d.ts +27 -0
- package/umd/scripts/run-codex-prompts/prompts/listRunnablePrompts.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/prompts/listUpcomingTasks.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/prompts/replacePromptTodoStatusLine.d.ts +7 -0
- package/umd/scripts/run-codex-prompts/testing/TestBeforeMode.d.ts +16 -0
- package/umd/scripts/run-codex-prompts/testing/createTestBeforeRepairPrompt.d.ts +9 -0
- package/umd/scripts/run-codex-prompts/testing/limitTestOutput.d.ts +4 -0
- package/umd/scripts/run-codex-prompts/testing/runPromptTestCommand.d.ts +1 -2
- package/umd/scripts/run-codex-prompts/testing/runTestBefore.d.ts +24 -0
- package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -0
- package/umd/src/book-2.0/book-language-documentation/BookLanguageManualDictionary.d.ts +1 -15
- package/umd/src/book-2.0/book-language-documentation/getBookLanguageManualCommitmentGroups.d.ts +1 -1
- package/umd/src/book-2.0/book-language-documentation/renderCommitmentCatalogSection.d.ts +0 -4
- package/umd/src/book-components/Chat/utils/renderMarkdown.d.ts +7 -0
- package/umd/src/cli/cli-commands/coder/agentCodingFile.d.ts +2 -3
- package/umd/src/cli/cli-commands/coder/ping.d.ts +13 -0
- package/umd/src/cli/cli-commands/coder/ping.test.d.ts +1 -0
- package/umd/src/cli/cli-commands/coder.d.ts +1 -0
- package/apps/agents-server/src/app/api/onboarding/draft/route.ts +0 -23
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/BookLanguagePanel.tsx +0 -125
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/MarkdownBookEditor.tsx +0 -154
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/components/MarkdownPreview.tsx +0 -188
- package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/draftService.ts +0 -32
package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/lib/bookSource.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inserts Book-language sections before the final learning-mode marker.
|
|
3
|
+
*
|
|
4
|
+
* `OPEN` and `CLOSED` apply to the whole agent and should remain the final
|
|
5
|
+
* commitment. Keeping this operation shared prevents the section chips and
|
|
6
|
+
* the knowledge-source integration from creating different source layouts.
|
|
7
|
+
*
|
|
8
|
+
* @param source - Current Book source.
|
|
9
|
+
* @param sections - Book-language sections to insert.
|
|
10
|
+
* @returns Source with the sections inserted before `OPEN` or `CLOSED`.
|
|
11
|
+
*/
|
|
12
|
+
export function insertBookContentBeforeLearningMarker(source: string, sections: readonly string[]): string {
|
|
13
|
+
const normalizedSections = sections.map((section) => section.trim()).filter((section) => section !== '');
|
|
14
|
+
|
|
15
|
+
if (normalizedSections.length === 0) {
|
|
16
|
+
return source;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const normalizedSource = source.replace(/\r\n/g, '\n').trimEnd();
|
|
20
|
+
const lines = normalizedSource.split('\n');
|
|
21
|
+
const markerIndex = findLearningMarkerIndex(lines);
|
|
22
|
+
const sectionSource = normalizedSections.join('\n\n');
|
|
23
|
+
|
|
24
|
+
if (markerIndex === -1) {
|
|
25
|
+
return [normalizedSource, sectionSource].filter((content) => content !== '').join('\n\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sourceBeforeMarker = lines.slice(0, markerIndex).join('\n').trimEnd();
|
|
29
|
+
const sourceFromMarker = lines.slice(markerIndex).join('\n').trim();
|
|
30
|
+
|
|
31
|
+
return [sourceBeforeMarker, sectionSource, sourceFromMarker]
|
|
32
|
+
.filter((content) => content !== '')
|
|
33
|
+
.join('\n\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Finds the last standalone `OPEN` or `CLOSED` line in a Book source.
|
|
38
|
+
*
|
|
39
|
+
* @param lines - Normalized Book source lines.
|
|
40
|
+
* @returns Index of the learning-mode marker, or `-1` when absent.
|
|
41
|
+
*
|
|
42
|
+
* @private internal utility of manGo Book source editing
|
|
43
|
+
*/
|
|
44
|
+
function findLearningMarkerIndex(lines: readonly string[]): number {
|
|
45
|
+
for (let lineIndex = lines.length - 1; lineIndex >= 0; lineIndex -= 1) {
|
|
46
|
+
const line = lines[lineIndex];
|
|
47
|
+
|
|
48
|
+
if (line && (line.trim().toUpperCase() === 'OPEN' || line.trim().toUpperCase() === 'CLOSED')) {
|
|
49
|
+
return lineIndex;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
package/apps/agents-server/src/components/NewAgentDialog/ManGoNewAgentWizard/services/bookService.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { postManGoOnboardingJson } from './postManGoOnboardingJson';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Response returned by the manGo Book
|
|
4
|
+
* Response returned by the manGo Book generation endpoint.
|
|
5
5
|
*
|
|
6
6
|
* @private internal type of the manGo wizard book service.
|
|
7
7
|
*/
|
|
8
|
-
type
|
|
8
|
+
type GenerateBookResponse = {
|
|
9
9
|
readonly book: string;
|
|
10
|
-
readonly isValid: boolean;
|
|
11
10
|
};
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
13
|
+
* Generates the initial Book directly from the assignment step.
|
|
15
14
|
*
|
|
16
|
-
* @param input -
|
|
17
|
-
* @returns
|
|
15
|
+
* @param input - Agent name and brief captured by the entry step.
|
|
16
|
+
* @returns Editable Book source used by the following wizard steps.
|
|
18
17
|
*/
|
|
19
|
-
export async function
|
|
20
|
-
|
|
18
|
+
export async function generateBook(input: {
|
|
19
|
+
readonly agentName: string;
|
|
20
|
+
readonly agentBrief: string;
|
|
21
|
+
}): Promise<string> {
|
|
22
|
+
const response = await postManGoOnboardingJson<GenerateBookResponse>('/api/onboarding/book', {
|
|
23
|
+
agentName: input.agentName,
|
|
24
|
+
agentBrief: input.agentBrief,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return response.book;
|
|
21
28
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spaceTrim } from 'spacetrim';
|
|
2
2
|
import { validateBook, type string_book } from '../../../../../../../src/book-2.0/agent-source/string_book';
|
|
3
|
+
import { insertBookContentBeforeLearningMarker } from '../lib/bookSource';
|
|
3
4
|
import type { KnowledgeItem, OnboardingState } from '../types';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -22,7 +23,7 @@ function getKnowledgeSource(item: KnowledgeItem): string {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Creates a minimal Book source when the editable
|
|
26
|
+
* Creates a minimal Book source when the editable Book is empty.
|
|
26
27
|
*
|
|
27
28
|
* @param state - Current wizard state.
|
|
28
29
|
* @returns Book source fallback.
|
|
@@ -57,41 +58,6 @@ function ensureBookTitle(source: string, agentName: string): string {
|
|
|
57
58
|
return agentName.trim() || FALLBACK_AGENT_NAME;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
/**
|
|
61
|
-
* Inserts extra commitments before a final `OPEN` or `CLOSED` marker when present.
|
|
62
|
-
*
|
|
63
|
-
* @param source - Current Book source.
|
|
64
|
-
* @param commitments - Commitment lines to insert.
|
|
65
|
-
* @returns Source with inserted commitments.
|
|
66
|
-
*/
|
|
67
|
-
function insertCommitmentsBeforeLearningMarker(source: string, commitments: ReadonlyArray<string>): string {
|
|
68
|
-
if (commitments.length === 0) {
|
|
69
|
-
return source;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const lines = source.replace(/\r\n/g, '\n').split('\n');
|
|
73
|
-
let markerIndex = -1;
|
|
74
|
-
for (let lineIndex = lines.length - 1; lineIndex >= 0; lineIndex -= 1) {
|
|
75
|
-
const line = lines[lineIndex];
|
|
76
|
-
if (line === undefined) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const normalizedLine = line.trim().toUpperCase();
|
|
81
|
-
if (normalizedLine === 'OPEN' || normalizedLine === 'CLOSED') {
|
|
82
|
-
markerIndex = lineIndex;
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const insertionLines = ['', ...commitments];
|
|
87
|
-
|
|
88
|
-
if (markerIndex === -1) {
|
|
89
|
-
return [...lines, ...insertionLines].join('\n');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return [...lines.slice(0, markerIndex), ...insertionLines, '', ...lines.slice(markerIndex)].join('\n');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
61
|
/**
|
|
96
62
|
* Builds the final Book source submitted from the manGo wizard.
|
|
97
63
|
*
|
|
@@ -108,5 +74,5 @@ export function createManGoAgentSource(state: OnboardingState): string_book {
|
|
|
108
74
|
.filter((source) => !titledSource.includes(`KNOWLEDGE ${source}`))
|
|
109
75
|
.map((source) => `KNOWLEDGE ${source}`);
|
|
110
76
|
|
|
111
|
-
return validateBook(
|
|
77
|
+
return validateBook(insertBookContentBeforeLearningMarker(titledSource, knowledgeCommitments));
|
|
112
78
|
}
|
|
@@ -8,7 +8,7 @@ import { createManGoAgentSource } from './createManGoAgentSource';
|
|
|
8
8
|
*
|
|
9
9
|
* @param state - Current imported wizard state.
|
|
10
10
|
* @param defaultVisibility - Metadata-backed default visibility for newly created agents.
|
|
11
|
-
* @returns Classic Book editor request carrying the current
|
|
11
|
+
* @returns Classic Book editor request carrying the current Book.
|
|
12
12
|
*
|
|
13
13
|
* @private internal utility of <ManGoNewAgentWizard/>.
|
|
14
14
|
*/
|
|
@@ -6,8 +6,8 @@ import { NotAllowed } from '../../../../../../../src/errors/NotAllowed';
|
|
|
6
6
|
import type { OnboardingState } from '../types';
|
|
7
7
|
|
|
8
8
|
/** sessionStorage key for the wizard session — exported so callers (e.g. the dashboard's
|
|
9
|
-
* "Nový onboarding") can clear a stale
|
|
10
|
-
export const ONBOARDING_STORAGE_KEY = 'onboarding:
|
|
9
|
+
* "Nový onboarding") can clear a stale Book before starting fresh. */
|
|
10
|
+
export const ONBOARDING_STORAGE_KEY = 'onboarding:v2';
|
|
11
11
|
const STORAGE_KEY = ONBOARDING_STORAGE_KEY;
|
|
12
12
|
|
|
13
13
|
const INITIAL_STATE: OnboardingState = {
|
|
@@ -5,25 +5,14 @@ import type { Dispatch, FormEvent, SetStateAction } from 'react';
|
|
|
5
5
|
import { Dialog } from '../Portal/Dialog';
|
|
6
6
|
import { SecretInput } from '../SecretInput/SecretInput';
|
|
7
7
|
import { useServerLanguage } from '../ServerLanguage/ServerLanguageProvider';
|
|
8
|
+
import { USER_DIALOG_PRIMARY_BUTTON_CLASS_NAME, USER_DIALOG_SECONDARY_BUTTON_CLASS_NAME } from './userDialogClassNames';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Styling for ordinary form inputs inside the create-user dialog.
|
|
11
12
|
*/
|
|
12
|
-
const
|
|
13
|
+
const CREATE_USER_INPUT_CLASS_NAME =
|
|
13
14
|
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200';
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* Shared secondary button styling used by the create-user dialog.
|
|
17
|
-
*/
|
|
18
|
-
const SECONDARY_BUTTON_CLASS_NAME =
|
|
19
|
-
'inline-flex items-center justify-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Shared primary button styling used by the create-user dialog.
|
|
23
|
-
*/
|
|
24
|
-
const PRIMARY_BUTTON_CLASS_NAME =
|
|
25
|
-
'inline-flex items-center justify-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60';
|
|
26
|
-
|
|
27
16
|
/**
|
|
28
17
|
* Props for the create-user dialog.
|
|
29
18
|
*
|
|
@@ -31,10 +20,12 @@ const PRIMARY_BUTTON_CLASS_NAME =
|
|
|
31
20
|
*/
|
|
32
21
|
type CreateUserDialogProps = {
|
|
33
22
|
readonly isAdmin: boolean;
|
|
23
|
+
/** Whether the dialog can be dismissed through its backdrop or Escape key. */
|
|
24
|
+
readonly isDismissible?: boolean;
|
|
34
25
|
readonly isOpen: boolean;
|
|
35
26
|
readonly isSubmitting: boolean;
|
|
36
27
|
readonly onClose: () => void;
|
|
37
|
-
readonly
|
|
28
|
+
readonly onOpenPasswordGenerator: () => void;
|
|
38
29
|
readonly onSubmit: (event: FormEvent<HTMLFormElement>) => Promise<void>;
|
|
39
30
|
readonly password: string;
|
|
40
31
|
readonly setIsAdmin: Dispatch<SetStateAction<boolean>>;
|
|
@@ -50,10 +41,11 @@ type CreateUserDialogProps = {
|
|
|
50
41
|
*/
|
|
51
42
|
export function CreateUserDialog({
|
|
52
43
|
isAdmin,
|
|
44
|
+
isDismissible,
|
|
53
45
|
isOpen,
|
|
54
46
|
isSubmitting,
|
|
55
47
|
onClose,
|
|
56
|
-
|
|
48
|
+
onOpenPasswordGenerator,
|
|
57
49
|
onSubmit,
|
|
58
50
|
password,
|
|
59
51
|
setIsAdmin,
|
|
@@ -68,7 +60,7 @@ export function CreateUserDialog({
|
|
|
68
60
|
}
|
|
69
61
|
|
|
70
62
|
return (
|
|
71
|
-
<Dialog onClose={onClose} className="mx-4 w-full max-w-lg overflow-hidden">
|
|
63
|
+
<Dialog onClose={onClose} isDismissible={isDismissible} className="mx-4 w-full max-w-lg overflow-hidden">
|
|
72
64
|
<form onSubmit={(event) => void onSubmit(event)}>
|
|
73
65
|
<div className="flex items-start justify-between gap-4 border-b border-gray-200 px-6 py-5">
|
|
74
66
|
<div>
|
|
@@ -95,7 +87,7 @@ export function CreateUserDialog({
|
|
|
95
87
|
type="text"
|
|
96
88
|
value={username}
|
|
97
89
|
onChange={(event) => setUsername(event.target.value)}
|
|
98
|
-
className={
|
|
90
|
+
className={CREATE_USER_INPUT_CLASS_NAME}
|
|
99
91
|
placeholder={t('users.usernamePlaceholder')}
|
|
100
92
|
autoComplete="off"
|
|
101
93
|
required
|
|
@@ -115,13 +107,12 @@ export function CreateUserDialog({
|
|
|
115
107
|
/>
|
|
116
108
|
<button
|
|
117
109
|
type="button"
|
|
118
|
-
onClick={
|
|
110
|
+
onClick={onOpenPasswordGenerator}
|
|
119
111
|
className="mt-2 text-sm font-medium text-blue-600 hover:text-blue-800"
|
|
120
112
|
>
|
|
121
113
|
<RefreshCw className="mr-1 inline h-3.5 w-3.5" />
|
|
122
114
|
{t('users.generatePassword')}
|
|
123
115
|
</button>
|
|
124
|
-
<p className="mt-1 text-xs text-gray-500">{t('users.passwordGeneratorDescription')}</p>
|
|
125
116
|
</div>
|
|
126
117
|
|
|
127
118
|
<label className="flex items-start gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
|
@@ -139,10 +130,10 @@ export function CreateUserDialog({
|
|
|
139
130
|
</div>
|
|
140
131
|
|
|
141
132
|
<div className="flex flex-col-reverse gap-3 border-t border-gray-100 px-6 py-5 sm:flex-row sm:justify-end">
|
|
142
|
-
<button type="button" onClick={onClose} className={
|
|
133
|
+
<button type="button" onClick={onClose} className={USER_DIALOG_SECONDARY_BUTTON_CLASS_NAME}>
|
|
143
134
|
{t('users.deleteConfirmCancel')}
|
|
144
135
|
</button>
|
|
145
|
-
<button type="submit" disabled={isSubmitting} className={
|
|
136
|
+
<button type="submit" disabled={isSubmitting} className={USER_DIALOG_PRIMARY_BUTTON_CLASS_NAME}>
|
|
146
137
|
{isSubmitting ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
|
147
138
|
{t('users.createUser')}
|
|
148
139
|
</button>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { RefreshCw, X } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import type { ServerTranslationKey } from '../../languages/ServerTranslationKeys';
|
|
6
|
+
import { Dialog } from '../Portal/Dialog';
|
|
7
|
+
import { SecretInput } from '../SecretInput/SecretInput';
|
|
8
|
+
import { useServerLanguage } from '../ServerLanguage/ServerLanguageProvider';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_PASSWORD_GENERATOR_OPTIONS,
|
|
11
|
+
generateSecurePassword,
|
|
12
|
+
PASSWORD_CHARACTER_OPTION_NAMES,
|
|
13
|
+
type PasswordCharacterOptionName,
|
|
14
|
+
type PasswordGeneratorOptions,
|
|
15
|
+
} from './generateSecurePassword';
|
|
16
|
+
import { USER_DIALOG_PRIMARY_BUTTON_CLASS_NAME, USER_DIALOG_SECONDARY_BUTTON_CLASS_NAME } from './userDialogClassNames';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimum password length offered by the password generator.
|
|
20
|
+
*/
|
|
21
|
+
const MINIMUM_PASSWORD_LENGTH = 1;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maximum password length offered by the password generator.
|
|
25
|
+
*/
|
|
26
|
+
const MAXIMUM_PASSWORD_LENGTH = 128;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Translation keys for each configurable character category.
|
|
30
|
+
*/
|
|
31
|
+
const PASSWORD_CHARACTER_OPTION_TRANSLATION_KEYS: Readonly<Record<PasswordCharacterOptionName, ServerTranslationKey>> = {
|
|
32
|
+
isUppercaseLettersIncluded: 'users.passwordGeneratorIncludeUppercaseLetters',
|
|
33
|
+
isLowercaseLettersIncluded: 'users.passwordGeneratorIncludeLowercaseLetters',
|
|
34
|
+
isNumbersIncluded: 'users.passwordGeneratorIncludeNumbers',
|
|
35
|
+
isSpecialCharactersIncluded: 'users.passwordGeneratorIncludeSpecialCharacters',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Props for the password generator dialog.
|
|
40
|
+
*
|
|
41
|
+
* @private component of <UsersList/>
|
|
42
|
+
*/
|
|
43
|
+
type PasswordGeneratorDialogProps = {
|
|
44
|
+
readonly onClose: () => void;
|
|
45
|
+
readonly onUsePassword: (password: string) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Renders configurable, Web Crypto-backed password generation for new users.
|
|
50
|
+
*
|
|
51
|
+
* @private component of <UsersList/>
|
|
52
|
+
*/
|
|
53
|
+
export function PasswordGeneratorDialog({ onClose, onUsePassword }: PasswordGeneratorDialogProps) {
|
|
54
|
+
const { t } = useServerLanguage();
|
|
55
|
+
const [passwordGeneratorOptions, setPasswordGeneratorOptions] = useState<PasswordGeneratorOptions>(
|
|
56
|
+
DEFAULT_PASSWORD_GENERATOR_OPTIONS,
|
|
57
|
+
);
|
|
58
|
+
const [generatedPassword, setGeneratedPassword] = useState(() =>
|
|
59
|
+
generateSecurePassword(DEFAULT_PASSWORD_GENERATOR_OPTIONS),
|
|
60
|
+
);
|
|
61
|
+
const includedCharacterOptionCount = PASSWORD_CHARACTER_OPTION_NAMES.filter(
|
|
62
|
+
(optionName) => passwordGeneratorOptions[optionName],
|
|
63
|
+
).length;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stores an updated configuration and creates a password that exactly matches it.
|
|
67
|
+
*/
|
|
68
|
+
function updatePasswordGeneratorOptions(updatedPasswordGeneratorOptions: PasswordGeneratorOptions) {
|
|
69
|
+
setPasswordGeneratorOptions(updatedPasswordGeneratorOptions);
|
|
70
|
+
setGeneratedPassword(generateSecurePassword(updatedPasswordGeneratorOptions));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Updates the requested length while preserving a valid count for the selected character categories.
|
|
75
|
+
*/
|
|
76
|
+
function handlePasswordLengthChange(passwordLength: number) {
|
|
77
|
+
if (!Number.isSafeInteger(passwordLength)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
updatePasswordGeneratorOptions({
|
|
82
|
+
...passwordGeneratorOptions,
|
|
83
|
+
length: Math.min(
|
|
84
|
+
MAXIMUM_PASSWORD_LENGTH,
|
|
85
|
+
Math.max(includedCharacterOptionCount, MINIMUM_PASSWORD_LENGTH, passwordLength),
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Enables or disables one character category without allowing an empty character set.
|
|
92
|
+
*/
|
|
93
|
+
function handleCharacterOptionChange(optionName: PasswordCharacterOptionName, isIncluded: boolean) {
|
|
94
|
+
const updatedIncludedCharacterOptionCount = includedCharacterOptionCount +
|
|
95
|
+
(isIncluded === passwordGeneratorOptions[optionName] ? 0 : isIncluded ? 1 : -1);
|
|
96
|
+
|
|
97
|
+
if (updatedIncludedCharacterOptionCount === 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const updatedPasswordGeneratorOptions: PasswordGeneratorOptions = {
|
|
102
|
+
...passwordGeneratorOptions,
|
|
103
|
+
[optionName]: isIncluded,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
updatePasswordGeneratorOptions({
|
|
107
|
+
...updatedPasswordGeneratorOptions,
|
|
108
|
+
length: Math.max(updatedPasswordGeneratorOptions.length, updatedIncludedCharacterOptionCount),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Passes the current generated password back to the create-user form.
|
|
114
|
+
*/
|
|
115
|
+
function handleUsePassword() {
|
|
116
|
+
if (!generatedPassword) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
onUsePassword(generatedPassword);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Dialog onClose={onClose} className="mx-4 w-full max-w-lg overflow-hidden" ariaLabelledBy="password-generator-title">
|
|
125
|
+
<div className="flex items-start justify-between gap-4 border-b border-gray-200 px-6 py-5">
|
|
126
|
+
<div>
|
|
127
|
+
<h2 id="password-generator-title" className="text-xl font-semibold text-gray-900">
|
|
128
|
+
{t('users.passwordGeneratorTitle')}
|
|
129
|
+
</h2>
|
|
130
|
+
<p className="mt-1 text-sm text-gray-500">{t('users.passwordGeneratorDescription')}</p>
|
|
131
|
+
</div>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={onClose}
|
|
135
|
+
className="rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-700"
|
|
136
|
+
aria-label={t('users.closePasswordGeneratorDialog')}
|
|
137
|
+
>
|
|
138
|
+
<X className="h-5 w-5" />
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div className="space-y-5 px-6 py-6">
|
|
143
|
+
<div>
|
|
144
|
+
<label htmlFor="password-generator-length" className="mb-1 block text-sm font-medium text-gray-700">
|
|
145
|
+
{t('users.passwordGeneratorLength')}
|
|
146
|
+
</label>
|
|
147
|
+
<input
|
|
148
|
+
id="password-generator-length"
|
|
149
|
+
type="number"
|
|
150
|
+
min={Math.max(includedCharacterOptionCount, MINIMUM_PASSWORD_LENGTH)}
|
|
151
|
+
max={MAXIMUM_PASSWORD_LENGTH}
|
|
152
|
+
step="1"
|
|
153
|
+
value={passwordGeneratorOptions.length}
|
|
154
|
+
onChange={(event) => handlePasswordLengthChange(event.target.valueAsNumber)}
|
|
155
|
+
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<fieldset>
|
|
160
|
+
<legend className="mb-2 text-sm font-medium text-gray-700">
|
|
161
|
+
{t('users.passwordGeneratorCharacters')}
|
|
162
|
+
</legend>
|
|
163
|
+
<div className="space-y-2">
|
|
164
|
+
{PASSWORD_CHARACTER_OPTION_NAMES.map((optionName) => {
|
|
165
|
+
const isIncluded = passwordGeneratorOptions[optionName];
|
|
166
|
+
const isOnlyIncludedCharacterOption = isIncluded && includedCharacterOptionCount === 1;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<label key={optionName} className="flex items-center gap-3 text-sm text-gray-700">
|
|
170
|
+
<input
|
|
171
|
+
type="checkbox"
|
|
172
|
+
checked={isIncluded}
|
|
173
|
+
onChange={(event) => handleCharacterOptionChange(optionName, event.target.checked)}
|
|
174
|
+
disabled={isOnlyIncludedCharacterOption}
|
|
175
|
+
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
|
|
176
|
+
/>
|
|
177
|
+
{t(PASSWORD_CHARACTER_OPTION_TRANSLATION_KEYS[optionName])}
|
|
178
|
+
</label>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
</fieldset>
|
|
183
|
+
|
|
184
|
+
<SecretInput
|
|
185
|
+
id="password-generator-value"
|
|
186
|
+
label={t('users.generatedPassword')}
|
|
187
|
+
value={generatedPassword}
|
|
188
|
+
readOnly
|
|
189
|
+
autoComplete="new-password"
|
|
190
|
+
/>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => setGeneratedPassword(generateSecurePassword(passwordGeneratorOptions))}
|
|
194
|
+
className="text-sm font-medium text-blue-600 hover:text-blue-800"
|
|
195
|
+
>
|
|
196
|
+
<RefreshCw className="mr-1 inline h-3.5 w-3.5" />
|
|
197
|
+
{t('users.regeneratePassword')}
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div className="flex flex-col-reverse gap-3 border-t border-gray-100 px-6 py-5 sm:flex-row sm:justify-end">
|
|
202
|
+
<button type="button" onClick={onClose} className={USER_DIALOG_SECONDARY_BUTTON_CLASS_NAME}>
|
|
203
|
+
{t('users.deleteConfirmCancel')}
|
|
204
|
+
</button>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={handleUsePassword}
|
|
208
|
+
disabled={!generatedPassword}
|
|
209
|
+
className={USER_DIALOG_PRIMARY_BUTTON_CLASS_NAME}
|
|
210
|
+
>
|
|
211
|
+
{t('users.useGeneratedPassword')}
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
</Dialog>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -8,8 +8,8 @@ import { Section } from '../Homepage/Section';
|
|
|
8
8
|
import { useServerLanguage } from '../ServerLanguage/ServerLanguageProvider';
|
|
9
9
|
import { isAnonymousUsername } from '../../utils/anonymousUser';
|
|
10
10
|
import { formatServerLanguageHumanReadableDate } from '@/src/utils/localization/formatServerLanguageHumanReadableDate';
|
|
11
|
-
import { generateSecurePassword } from './generateSecurePassword';
|
|
12
11
|
import { CreateUserDialog } from './CreateUserDialog';
|
|
12
|
+
import { PasswordGeneratorDialog } from './PasswordGeneratorDialog';
|
|
13
13
|
import { useUsersAdmin } from './useUsersAdmin';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -33,6 +33,7 @@ export function UsersList({ allowCreate = true }: UsersListProps) {
|
|
|
33
33
|
const { language, t } = useServerLanguage();
|
|
34
34
|
|
|
35
35
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
36
|
+
const [isPasswordGeneratorOpen, setIsPasswordGeneratorOpen] = useState(false);
|
|
36
37
|
const [isCreatingUser, setIsCreatingUser] = useState(false);
|
|
37
38
|
const [newUsername, setNewUsername] = useState('');
|
|
38
39
|
const [newPassword, setNewPassword] = useState('');
|
|
@@ -65,10 +66,6 @@ export function UsersList({ allowCreate = true }: UsersListProps) {
|
|
|
65
66
|
|
|
66
67
|
const handleOpenCreateDialog = () => {
|
|
67
68
|
resetCreateUserForm();
|
|
68
|
-
const generatedPassword = generateSecurePassword();
|
|
69
|
-
if (generatedPassword) {
|
|
70
|
-
setNewPassword(generatedPassword);
|
|
71
|
-
}
|
|
72
69
|
setIsCreateDialogOpen(true);
|
|
73
70
|
};
|
|
74
71
|
|
|
@@ -78,6 +75,7 @@ export function UsersList({ allowCreate = true }: UsersListProps) {
|
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
setIsCreateDialogOpen(false);
|
|
78
|
+
setIsPasswordGeneratorOpen(false);
|
|
81
79
|
resetCreateUserForm();
|
|
82
80
|
};
|
|
83
81
|
|
|
@@ -93,6 +91,7 @@ export function UsersList({ allowCreate = true }: UsersListProps) {
|
|
|
93
91
|
});
|
|
94
92
|
|
|
95
93
|
setIsCreateDialogOpen(false);
|
|
94
|
+
setIsPasswordGeneratorOpen(false);
|
|
96
95
|
resetCreateUserForm();
|
|
97
96
|
} catch {
|
|
98
97
|
// Error is already handled and exposed via `error` state from the hook
|
|
@@ -271,15 +270,11 @@ export function UsersList({ allowCreate = true }: UsersListProps) {
|
|
|
271
270
|
{allowCreate ? (
|
|
272
271
|
<CreateUserDialog
|
|
273
272
|
isAdmin={newIsAdmin}
|
|
273
|
+
isDismissible={!isPasswordGeneratorOpen}
|
|
274
274
|
isOpen={isCreateDialogOpen}
|
|
275
275
|
isSubmitting={isCreatingUser}
|
|
276
276
|
onClose={handleCloseCreateDialog}
|
|
277
|
-
|
|
278
|
-
const generatedPassword = generateSecurePassword();
|
|
279
|
-
if (generatedPassword) {
|
|
280
|
-
setNewPassword(generatedPassword);
|
|
281
|
-
}
|
|
282
|
-
}}
|
|
277
|
+
onOpenPasswordGenerator={() => setIsPasswordGeneratorOpen(true)}
|
|
283
278
|
onSubmit={handleCreateUser}
|
|
284
279
|
password={newPassword}
|
|
285
280
|
setIsAdmin={setNewIsAdmin}
|
|
@@ -288,6 +283,16 @@ export function UsersList({ allowCreate = true }: UsersListProps) {
|
|
|
288
283
|
username={newUsername}
|
|
289
284
|
/>
|
|
290
285
|
) : null}
|
|
286
|
+
|
|
287
|
+
{isPasswordGeneratorOpen ? (
|
|
288
|
+
<PasswordGeneratorDialog
|
|
289
|
+
onClose={() => setIsPasswordGeneratorOpen(false)}
|
|
290
|
+
onUsePassword={(password) => {
|
|
291
|
+
setNewPassword(password);
|
|
292
|
+
setIsPasswordGeneratorOpen(false);
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
) : null}
|
|
291
296
|
</div>
|
|
292
297
|
);
|
|
293
298
|
}
|