@riducms/admin 0.1.0
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/LICENSE +21 -0
- package/README.md +31 -0
- package/THIRD_PARTY_NOTICES.md +217 -0
- package/package.json +62 -0
- package/src/app/admin-core-view-route.svelte +89 -0
- package/src/app/admin-layout.svelte +457 -0
- package/src/app/admin-navigation-progress.svelte +204 -0
- package/src/app/admin-page-title.svelte +94 -0
- package/src/app/admin-provider-layer.svelte +26 -0
- package/src/app/admin-router-root.svelte +9 -0
- package/src/app/admin-routes.svelte +116 -0
- package/src/app/deferred-redirect.svelte +23 -0
- package/src/app/loading-boundary.svelte +70 -0
- package/src/app/not-found-route.svelte +29 -0
- package/src/app-meta.ts +2 -0
- package/src/app.css +467 -0
- package/src/app.svelte +88 -0
- package/src/assets/ridu-logo-arch.svg +3 -0
- package/src/assets/ridu-word-logo.svg +7 -0
- package/src/components/brand/ridu-logo.svelte +24 -0
- package/src/components/code-block/code-block.svelte +107 -0
- package/src/components/json-tree/json-tree-node.svelte +77 -0
- package/src/components/json-tree/json-viewer.svelte +113 -0
- package/src/components/ui/banner/banner.svelte +41 -0
- package/src/components/ui/banner/index.ts +5 -0
- package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +50 -0
- package/src/components/ui/bulk-action-bar/index.ts +1 -0
- package/src/components/ui/button/index.ts +8 -0
- package/src/components/ui/calendar/calendar-caption.svelte +72 -0
- package/src/components/ui/calendar/calendar-cell.svelte +20 -0
- package/src/components/ui/calendar/calendar-day.svelte +27 -0
- package/src/components/ui/calendar/calendar-grid-row.svelte +13 -0
- package/src/components/ui/calendar/calendar-grid.svelte +17 -0
- package/src/components/ui/calendar/calendar-head-cell.svelte +20 -0
- package/src/components/ui/calendar/calendar-header.svelte +20 -0
- package/src/components/ui/calendar/calendar-heading.svelte +17 -0
- package/src/components/ui/calendar/calendar-month-select.svelte +45 -0
- package/src/components/ui/calendar/calendar-month.svelte +16 -0
- package/src/components/ui/calendar/calendar-months.svelte +20 -0
- package/src/components/ui/calendar/calendar-nav.svelte +20 -0
- package/src/components/ui/calendar/calendar-next-button.svelte +30 -0
- package/src/components/ui/calendar/calendar-prev-button.svelte +30 -0
- package/src/components/ui/calendar/calendar-year-select.svelte +44 -0
- package/src/components/ui/calendar/calendar.svelte +118 -0
- package/src/components/ui/calendar/index.ts +1 -0
- package/src/components/ui/checkbox/checkbox.svelte +35 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/confirmation-dialog/confirmation-dialog.svelte +37 -0
- package/src/components/ui/confirmation-dialog/index.ts +1 -0
- package/src/components/ui/date-value-control/date-value-calendar.svelte +37 -0
- package/src/components/ui/date-value-control/date-value-control-loader.svelte +36 -0
- package/src/components/ui/date-value-control/date-value-control.svelte +177 -0
- package/src/components/ui/date-value-control/index.ts +3 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +52 -0
- package/src/components/ui/dialog/dialog-description.svelte +20 -0
- package/src/components/ui/dialog/dialog-footer.svelte +20 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +20 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +8 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +5 -0
- package/src/components/ui/empty-state/empty-state.svelte +34 -0
- package/src/components/ui/empty-state/index.ts +1 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/kbd/index.ts +1 -0
- package/src/components/ui/kbd/kbd.svelte +24 -0
- package/src/components/ui/pagination/index.ts +1 -0
- package/src/components/ui/pagination/pagination.svelte +88 -0
- package/src/components/ui/popover/index.ts +3 -0
- package/src/components/ui/popover/popover-content.svelte +29 -0
- package/src/components/ui/popover/popover-trigger.svelte +11 -0
- package/src/components/ui/popover/popover.svelte +7 -0
- package/src/components/ui/radio-group/index.ts +3 -0
- package/src/components/ui/radio-group/radio-card-item.svelte +38 -0
- package/src/components/ui/radio-group/radio-group-item.svelte +17 -0
- package/src/components/ui/radio-group/radio-group.svelte +19 -0
- package/src/components/ui/select/index.ts +4 -0
- package/src/components/ui/select/select-content.svelte +38 -0
- package/src/components/ui/select/select-item.svelte +40 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +23 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +23 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/sheet/index.ts +6 -0
- package/src/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/components/ui/sheet/sheet-content.svelte +77 -0
- package/src/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/components/ui/sheet/sheet-overlay.svelte +36 -0
- package/src/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/components/ui/sheet/sheet.svelte +7 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/skeleton/skeleton.svelte +31 -0
- package/src/components/ui/slider/index.ts +3 -0
- package/src/components/ui/slider/slider-content.svelte +59 -0
- package/src/components/ui/slider/slider.svelte +35 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +55 -0
- package/src/components/ui/status-indicator/index.ts +5 -0
- package/src/components/ui/status-indicator/status-indicator.svelte +54 -0
- package/src/components/ui/table/index.ts +6 -0
- package/src/components/ui/table/table-body.svelte +20 -0
- package/src/components/ui/table/table-cell.svelte +23 -0
- package/src/components/ui/table/table-head.svelte +23 -0
- package/src/components/ui/table/table-header.svelte +20 -0
- package/src/components/ui/table/table-row.svelte +23 -0
- package/src/components/ui/table/table.svelte +22 -0
- package/src/components/ui/tabs/index.ts +8 -0
- package/src/components/ui/tabs/tabs-content.svelte +17 -0
- package/src/components/ui/tabs/tabs-list.svelte +40 -0
- package/src/components/ui/tabs/tabs-trigger.svelte +23 -0
- package/src/components/ui/tabs/tabs.svelte +19 -0
- package/src/components/ui/textarea/index.ts +1 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/components/ui/tooltip/tooltip-content.svelte +50 -0
- package/src/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/core/api/admin-client.ts +45 -0
- package/src/core/forms/field-condition.ts +63 -0
- package/src/core/forms/form-controller.svelte.ts +369 -0
- package/src/core/forms/form-draft-recovery.ts +80 -0
- package/src/core/forms/form-schema.ts +609 -0
- package/src/core/forms/form-validation.ts +750 -0
- package/src/core/i18n/admin-i18n.svelte.ts +364 -0
- package/src/core/i18n/localized-schema.ts +111 -0
- package/src/core/notifications/notification-center.svelte.ts +69 -0
- package/src/core/notifications/validation-toast.svelte +18 -0
- package/src/core/plugins/field-registry.ts +60 -0
- package/src/core/plugins/row-label-registry.ts +62 -0
- package/src/core/preferences/preference-write-queue.ts +107 -0
- package/src/core/routing/admin-paths.ts +137 -0
- package/src/core/runtime/admin-runtime.svelte.ts +625 -0
- package/src/features/auth/account-menu.svelte +159 -0
- package/src/features/auth/account-navigation.svelte +44 -0
- package/src/features/auth/account-route.svelte +395 -0
- package/src/features/auth/account-security-controller.svelte.ts +119 -0
- package/src/features/auth/account-security-route.svelte +460 -0
- package/src/features/auth/auth-brand.svelte +17 -0
- package/src/features/auth/auth-frame.svelte +18 -0
- package/src/features/auth/create-first-user-controller.svelte.ts +220 -0
- package/src/features/auth/create-first-user-route.svelte +174 -0
- package/src/features/auth/forgot-password-route.svelte +78 -0
- package/src/features/auth/login-route.svelte +221 -0
- package/src/features/auth/request-verification-route.svelte +73 -0
- package/src/features/auth/reset-password-route.svelte +107 -0
- package/src/features/auth/verify-email-route.svelte +60 -0
- package/src/features/collections/collection-list-bulk-actions.svelte +143 -0
- package/src/features/collections/collection-list-bulk-editor.svelte +157 -0
- package/src/features/collections/collection-list-controller.svelte.ts +717 -0
- package/src/features/collections/collection-list-filter-popover.svelte +238 -0
- package/src/features/collections/collection-list-preferences-controller.svelte.ts +393 -0
- package/src/features/collections/collection-list-results.svelte +410 -0
- package/src/features/collections/collection-list-route.svelte +951 -0
- package/src/features/collections/collection-list-workspace-toolbar.svelte +409 -0
- package/src/features/collections/collection-table-skeleton.svelte +44 -0
- package/src/features/collections/list-workspace.ts +265 -0
- package/src/features/dashboard/dashboard-route.svelte +141 -0
- package/src/features/development/development-tools.svelte +217 -0
- package/src/features/documents/document-api-view-loader.svelte +31 -0
- package/src/features/documents/document-api-view.svelte +547 -0
- package/src/features/documents/document-controller.svelte.ts +1252 -0
- package/src/features/documents/document-field-partition.ts +18 -0
- package/src/features/documents/document-field-sections.svelte +46 -0
- package/src/features/documents/document-heading.ts +20 -0
- package/src/features/documents/document-route-dirty.ts +43 -0
- package/src/features/documents/document-route-loader.svelte +22 -0
- package/src/features/documents/document-route.svelte +1124 -0
- package/src/features/documents/live-preview-panel.svelte +390 -0
- package/src/features/documents/live-preview.ts +61 -0
- package/src/features/documents/upload-document-preview-loader.svelte +40 -0
- package/src/features/documents/upload-document-preview.svelte +525 -0
- package/src/features/localization/content-locale-switcher.svelte +93 -0
- package/src/features/navigation/admin-breadcrumbs.svelte +124 -0
- package/src/features/navigation/admin-command-menu.svelte +245 -0
- package/src/features/navigation/admin-resource-groups.ts +72 -0
- package/src/features/reference-browser/reference-browser-loader.svelte +15 -0
- package/src/features/reference-browser/reference-browser-workflow.svelte.ts +691 -0
- package/src/features/reference-browser/reference-browser.svelte +844 -0
- package/src/features/uploads/bulk-upload-route.svelte +624 -0
- package/src/features/uploads/bulk-upload.ts +39 -0
- package/src/features/uploads/upload-metadata-fields.svelte +112 -0
- package/src/features/versions/version-diff.ts +72 -0
- package/src/features/versions/version-history-route.svelte +581 -0
- package/src/fields/field-clipboard.ts +132 -0
- package/src/fields/field-layout.svelte +60 -0
- package/src/fields/field-layout.ts +57 -0
- package/src/fields/field-messages.svelte +29 -0
- package/src/fields/field-renderer.svelte +152 -0
- package/src/fields/field-shell.svelte +30 -0
- package/src/fields/field-tab-layout.svelte +99 -0
- package/src/fields/join/join-field.svelte +358 -0
- package/src/fields/localized-field-presentation.ts +17 -0
- package/src/fields/nested/nested-field.svelte +602 -0
- package/src/fields/nested/scoped-field.ts +78 -0
- package/src/fields/nested/sortable-row.svelte +32 -0
- package/src/fields/point/point-field.svelte +52 -0
- package/src/fields/radio/radio-field.svelte +34 -0
- package/src/fields/relationship/relationship-field-controller.svelte.ts +245 -0
- package/src/fields/relationship/relationship-field.svelte +258 -0
- package/src/fields/relationship/relationship-lookup-controller.svelte.ts +129 -0
- package/src/fields/relationship/relationship-query.ts +88 -0
- package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +198 -0
- package/src/fields/relationship/relationship-quick-picker.svelte +327 -0
- package/src/fields/relationship/relationship-value.ts +47 -0
- package/src/fields/scalar/date-control-value.ts +60 -0
- package/src/fields/scalar/date-value.ts +26 -0
- package/src/fields/scalar/scalar-field.svelte +137 -0
- package/src/fields/select/select-field.svelte +132 -0
- package/src/fields/select/select-value.ts +14 -0
- package/src/fields/text/slug-field.svelte +87 -0
- package/src/fields/text/slug.ts +38 -0
- package/src/fields/text/text-field.svelte +41 -0
- package/src/fields/ui/ui-field.svelte +12 -0
- package/src/fields/virtual/virtual-field.svelte +28 -0
- package/src/index.ts +49 -0
- package/src/main.ts +12 -0
- package/src/vite-env.d.ts +9 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { SchemaCollection } from "@riducms/protocol";
|
|
2
|
+
import { RiduError } from "@riducms/sdk";
|
|
3
|
+
|
|
4
|
+
import { FormController, FormValidationError } from "@admin/core/forms/form-controller.svelte";
|
|
5
|
+
import { initialFormValues } from "@admin/core/forms/form-schema";
|
|
6
|
+
import type { NotificationCenter } from "@admin/core/notifications/notification-center.svelte";
|
|
7
|
+
import { adminRoutePatterns } from "@admin/core/routing/admin-paths";
|
|
8
|
+
import type { AdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
9
|
+
|
|
10
|
+
type Navigate = (to: string, options?: { replace?: boolean }) => void;
|
|
11
|
+
|
|
12
|
+
interface CreateFirstUserControllerOptions {
|
|
13
|
+
runtime: AdminRuntime;
|
|
14
|
+
notifications: NotificationCenter;
|
|
15
|
+
navigate: Navigate;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class CreateFirstUserController {
|
|
19
|
+
readonly form: FormController;
|
|
20
|
+
password = $state("");
|
|
21
|
+
passwordConfirmation = $state("");
|
|
22
|
+
pending = $state(false);
|
|
23
|
+
error = $state<string>();
|
|
24
|
+
credentialIssue = $state<string>();
|
|
25
|
+
#request?: AbortController;
|
|
26
|
+
#generation = 0;
|
|
27
|
+
|
|
28
|
+
constructor(readonly options: CreateFirstUserControllerOptions) {
|
|
29
|
+
this.form = new FormController({}, options.runtime.i18n);
|
|
30
|
+
this.form.reset(initialFormValues(this.fields));
|
|
31
|
+
this.form.setAccess(undefined, "create");
|
|
32
|
+
if (this.collection !== undefined) {
|
|
33
|
+
this.form.setResource({ collection: this.collection.slug });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get collection(): SchemaCollection | undefined {
|
|
38
|
+
return this.options.runtime.authCollection;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get fields() {
|
|
42
|
+
return this.collection?.fields ?? [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get passwordHelp() {
|
|
46
|
+
const minimum = this.collection?.authSettings?.passwordMinLength ?? 8;
|
|
47
|
+
const maximum = this.collection?.authSettings?.passwordMaxBytes;
|
|
48
|
+
return maximum === undefined
|
|
49
|
+
? this.options.runtime.i18n.t("auth:passwordMinimum", {
|
|
50
|
+
minimum: this.options.runtime.i18n.formatNumber(minimum),
|
|
51
|
+
})
|
|
52
|
+
: this.options.runtime.i18n.t("auth:passwordRange", {
|
|
53
|
+
minimum: this.options.runtime.i18n.formatNumber(minimum),
|
|
54
|
+
maximum: this.options.runtime.i18n.formatNumber(maximum),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
submit = async () => {
|
|
59
|
+
if (this.pending) return undefined;
|
|
60
|
+
const collection = this.collection;
|
|
61
|
+
if (collection === undefined) {
|
|
62
|
+
this.error = this.options.runtime.i18n.t("auth:configuredCollectionUnavailable");
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
this.credentialIssue = this.#validatePassword(collection);
|
|
66
|
+
if (this.credentialIssue !== undefined) return "password";
|
|
67
|
+
|
|
68
|
+
const generation = ++this.#generation;
|
|
69
|
+
const request = new AbortController();
|
|
70
|
+
this.#request?.abort();
|
|
71
|
+
this.#request = request;
|
|
72
|
+
this.pending = true;
|
|
73
|
+
this.error = undefined;
|
|
74
|
+
let accountCreated = false;
|
|
75
|
+
try {
|
|
76
|
+
await this.form.submit(this.fields, true, (values) =>
|
|
77
|
+
this.options.runtime.client.createAuthUser(collection.slug, values, this.password, {
|
|
78
|
+
signal: request.signal,
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
accountCreated = true;
|
|
82
|
+
if (!this.#current(generation, request)) return undefined;
|
|
83
|
+
|
|
84
|
+
const identity = this.form.get(collection.authSettings?.identityField ?? "email");
|
|
85
|
+
if (typeof identity !== "string" || identity.trim() === "") {
|
|
86
|
+
throw new Error(this.options.runtime.i18n.t("auth:firstAccountIdentityUnavailable"));
|
|
87
|
+
}
|
|
88
|
+
const session = await this.options.runtime.client.login(
|
|
89
|
+
collection.slug,
|
|
90
|
+
{ email: identity, password: this.password },
|
|
91
|
+
{ signal: request.signal }
|
|
92
|
+
);
|
|
93
|
+
if (!this.#current(generation, request)) return undefined;
|
|
94
|
+
const initialAccessAuthoritative = await this.options.runtime.refreshAccess();
|
|
95
|
+
if (!this.#current(generation, request)) return undefined;
|
|
96
|
+
if (
|
|
97
|
+
initialAccessAuthoritative &&
|
|
98
|
+
this.options.runtime.collectionOperations[collection.slug]?.admin !== true
|
|
99
|
+
) {
|
|
100
|
+
try {
|
|
101
|
+
await this.options.runtime.client.logout({ signal: request.signal });
|
|
102
|
+
} catch {
|
|
103
|
+
// The setup route remains closed even if cookie cleanup fails.
|
|
104
|
+
}
|
|
105
|
+
throw new Error(this.options.runtime.i18n.t("auth:firstAccountNoAdminAccess"));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.options.runtime.authBootstrapAvailable = false;
|
|
109
|
+
this.options.runtime.session = session;
|
|
110
|
+
await Promise.all([
|
|
111
|
+
this.options.runtime.loadTheme(),
|
|
112
|
+
this.options.runtime.loadContentLocale(),
|
|
113
|
+
this.options.runtime.i18n.loadPreferences(),
|
|
114
|
+
]);
|
|
115
|
+
const preferredAccessAuthoritative = await this.options.runtime.refreshAccess(
|
|
116
|
+
this.options.runtime.contentLocale
|
|
117
|
+
);
|
|
118
|
+
if (
|
|
119
|
+
preferredAccessAuthoritative &&
|
|
120
|
+
this.options.runtime.collectionOperations[collection.slug]?.admin !== true
|
|
121
|
+
) {
|
|
122
|
+
try {
|
|
123
|
+
await this.options.runtime.client.logout({ signal: request.signal });
|
|
124
|
+
} catch {
|
|
125
|
+
// The setup route remains closed even if cookie cleanup fails.
|
|
126
|
+
}
|
|
127
|
+
this.options.runtime.session = undefined;
|
|
128
|
+
throw new Error(this.options.runtime.i18n.t("auth:firstAccountNoAdminAccess"));
|
|
129
|
+
}
|
|
130
|
+
if (!this.#current(generation, request)) return undefined;
|
|
131
|
+
this.options.navigate(adminRoutePatterns.home, { replace: true });
|
|
132
|
+
return undefined;
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
if (!this.#current(generation, request)) return undefined;
|
|
135
|
+
if (accountCreated) {
|
|
136
|
+
this.#continueToLogin(
|
|
137
|
+
this.options.runtime.i18n.t("auth:accountCreated"),
|
|
138
|
+
cause instanceof Error
|
|
139
|
+
? this.options.runtime.i18n.t("auth:autoSignInFailed", { message: cause.message })
|
|
140
|
+
: this.options.runtime.i18n.t("auth:accountReady")
|
|
141
|
+
);
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
if (cause instanceof FormValidationError) {
|
|
145
|
+
this.error = cause.message;
|
|
146
|
+
return this.form.issues[0]?.path;
|
|
147
|
+
}
|
|
148
|
+
if (cause instanceof RiduError) {
|
|
149
|
+
const passwordIssue = cause.issues.find((issue) => issue.path === "password");
|
|
150
|
+
if (passwordIssue !== undefined) {
|
|
151
|
+
this.credentialIssue = passwordIssue.message;
|
|
152
|
+
this.form.issues = this.form.issues.filter((issue) => issue.path !== "password");
|
|
153
|
+
this.error = this.options.runtime.i18n.t("auth:correctInvalidFields");
|
|
154
|
+
return "password";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const bootstrap = await this.options.runtime.client.authBootstrap(collection.slug, {
|
|
160
|
+
signal: request.signal,
|
|
161
|
+
});
|
|
162
|
+
if (!this.#current(generation, request)) return undefined;
|
|
163
|
+
if (!bootstrap.available) {
|
|
164
|
+
this.#continueToLogin(
|
|
165
|
+
this.options.runtime.i18n.t("auth:setupCompleted"),
|
|
166
|
+
this.options.runtime.i18n.t("auth:firstAccountExists")
|
|
167
|
+
);
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
if (!this.#current(generation, request)) return undefined;
|
|
172
|
+
}
|
|
173
|
+
this.error =
|
|
174
|
+
cause instanceof Error
|
|
175
|
+
? cause.message
|
|
176
|
+
: this.options.runtime.i18n.t("auth:firstAccountCreateFailed");
|
|
177
|
+
return this.form.issues[0]?.path;
|
|
178
|
+
} finally {
|
|
179
|
+
if (this.#current(generation, request)) {
|
|
180
|
+
this.pending = false;
|
|
181
|
+
this.#request = undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
destroy() {
|
|
187
|
+
this.#generation += 1;
|
|
188
|
+
this.#request?.abort();
|
|
189
|
+
this.#request = undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#continueToLogin(title: string, message: string) {
|
|
193
|
+
this.options.notifications.error({ title, message });
|
|
194
|
+
this.options.navigate(adminRoutePatterns.login, { replace: true });
|
|
195
|
+
this.options.runtime.authBootstrapAvailable = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#current(generation: number, request: AbortController) {
|
|
199
|
+
return generation === this.#generation && !request.signal.aborted;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#validatePassword(collection: SchemaCollection) {
|
|
203
|
+
const settings = collection.authSettings;
|
|
204
|
+
if (this.password === "") return this.options.runtime.i18n.t("auth:enterPassword");
|
|
205
|
+
if (Array.from(this.password).length < (settings?.passwordMinLength ?? 8)) {
|
|
206
|
+
return this.options.runtime.i18n.t("auth:passwordTooShort", {
|
|
207
|
+
minimum: this.options.runtime.i18n.formatNumber(settings?.passwordMinLength ?? 8),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (new TextEncoder().encode(this.password).length > (settings?.passwordMaxBytes ?? 72)) {
|
|
211
|
+
return this.options.runtime.i18n.t("auth:passwordTooLong", {
|
|
212
|
+
maximum: this.options.runtime.i18n.formatNumber(settings?.passwordMaxBytes ?? 72),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (this.password !== this.passwordConfirmation) {
|
|
216
|
+
return this.options.runtime.i18n.t("auth:passwordMismatch");
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useNavigate } from "@hvniel/svelte-router";
|
|
3
|
+
import EyeIcon from "~icons/lucide/eye";
|
|
4
|
+
import EyeOffIcon from "~icons/lucide/eye-off";
|
|
5
|
+
import { tick } from "svelte";
|
|
6
|
+
|
|
7
|
+
import { Banner } from "@admin/components/ui/banner";
|
|
8
|
+
import { Button } from "@admin/components/ui/button";
|
|
9
|
+
import { Input } from "@admin/components/ui/input";
|
|
10
|
+
import { getNotificationCenter } from "@admin/core/notifications/notification-center.svelte";
|
|
11
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
12
|
+
import AuthBrand from "@admin/features/auth/auth-brand.svelte";
|
|
13
|
+
import AuthFrame from "@admin/features/auth/auth-frame.svelte";
|
|
14
|
+
import { CreateFirstUserController } from "@admin/features/auth/create-first-user-controller.svelte";
|
|
15
|
+
import FieldLayout from "@admin/fields/field-layout.svelte";
|
|
16
|
+
|
|
17
|
+
const runtime = getAdminRuntime();
|
|
18
|
+
const notifications = getNotificationCenter();
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
const controller = new CreateFirstUserController({ runtime, notifications, navigate });
|
|
21
|
+
const { form, fields } = $derived(controller);
|
|
22
|
+
const applicationName = $derived(
|
|
23
|
+
runtime.manifest?.application.name ?? runtime.i18n.t("general:riduApplication")
|
|
24
|
+
);
|
|
25
|
+
let passwordVisible = $state(false);
|
|
26
|
+
let confirmationVisible = $state(false);
|
|
27
|
+
|
|
28
|
+
$effect(() => () => controller.destroy());
|
|
29
|
+
|
|
30
|
+
async function handleSubmit(event: SubmitEvent) {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
const issuePath = await controller.submit();
|
|
33
|
+
if (issuePath === undefined) return;
|
|
34
|
+
await tick();
|
|
35
|
+
focusIssue(issuePath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function focusIssue(path: string) {
|
|
39
|
+
if (path === "password") {
|
|
40
|
+
document.getElementById("ridu-first-user-password")?.focus();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const segments = path.split(".");
|
|
44
|
+
while (segments.length > 0) {
|
|
45
|
+
const candidate = segments.join(".");
|
|
46
|
+
const container = document.querySelector<HTMLElement>(
|
|
47
|
+
`[data-field-path="${CSS.escape(candidate)}"]`
|
|
48
|
+
);
|
|
49
|
+
if (container !== null) {
|
|
50
|
+
container.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
51
|
+
container
|
|
52
|
+
.querySelector<HTMLElement>("input, textarea, button, [tabindex]:not([tabindex='-1'])")
|
|
53
|
+
?.focus({ preventScroll: true });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
segments.pop();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
{#snippet brand()}
|
|
62
|
+
<AuthBrand />
|
|
63
|
+
{/snippet}
|
|
64
|
+
|
|
65
|
+
<AuthFrame {brand} wide>
|
|
66
|
+
<header class="grid gap-2">
|
|
67
|
+
<h1 class="text-[28px] leading-tight font-semibold tracking-[-0.015em] text-foreground">
|
|
68
|
+
{runtime.i18n.t("auth:welcome", { application: applicationName })}
|
|
69
|
+
</h1>
|
|
70
|
+
<p class="max-w-[590px] text-[13.5px] leading-5.5 text-foreground-muted">
|
|
71
|
+
{runtime.i18n.t("auth:firstUserDescription")}
|
|
72
|
+
</p>
|
|
73
|
+
</header>
|
|
74
|
+
|
|
75
|
+
<form class="mt-8 grid gap-7" onsubmit={handleSubmit}>
|
|
76
|
+
<fieldset class="contents" disabled={controller.pending}>
|
|
77
|
+
<FieldLayout {fields} {form} />
|
|
78
|
+
|
|
79
|
+
<fieldset class="grid gap-4 rounded-[4px] border border-control-border bg-control p-4.5">
|
|
80
|
+
<legend class="px-1 text-[13px] font-semibold text-foreground">
|
|
81
|
+
{runtime.i18n.t("auth:credentials")}
|
|
82
|
+
</legend>
|
|
83
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
84
|
+
<div class="grid gap-2">
|
|
85
|
+
<label class="ridu-field-label" for="ridu-first-user-password">
|
|
86
|
+
{runtime.i18n.t("auth:password")}
|
|
87
|
+
<span class="ridu-field-required" aria-hidden="true">*</span>
|
|
88
|
+
</label>
|
|
89
|
+
<span class="relative block">
|
|
90
|
+
<Input
|
|
91
|
+
id="ridu-first-user-password"
|
|
92
|
+
class="pe-11"
|
|
93
|
+
type={passwordVisible ? "text" : "password"}
|
|
94
|
+
autocomplete="new-password"
|
|
95
|
+
required
|
|
96
|
+
aria-invalid={controller.credentialIssue !== undefined}
|
|
97
|
+
aria-describedby="ridu-first-user-password-help"
|
|
98
|
+
bind:value={controller.password}
|
|
99
|
+
oninput={() => (controller.credentialIssue = undefined)}
|
|
100
|
+
/>
|
|
101
|
+
<Button
|
|
102
|
+
variant="ghost"
|
|
103
|
+
size="icon-sm"
|
|
104
|
+
class="absolute top-1/2 end-2.5 -translate-y-1/2"
|
|
105
|
+
onclick={() => (passwordVisible = !passwordVisible)}
|
|
106
|
+
aria-label={passwordVisible
|
|
107
|
+
? runtime.i18n.t("auth:hidePassword")
|
|
108
|
+
: runtime.i18n.t("auth:showPassword")}
|
|
109
|
+
>
|
|
110
|
+
{#if passwordVisible}
|
|
111
|
+
<EyeOffIcon class="size-3.5" />
|
|
112
|
+
{:else}
|
|
113
|
+
<EyeIcon class="size-3.5" />
|
|
114
|
+
{/if}
|
|
115
|
+
</Button>
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="grid gap-2">
|
|
120
|
+
<label class="ridu-field-label" for="ridu-first-user-password-confirmation">
|
|
121
|
+
{runtime.i18n.t("auth:confirmPassword")}
|
|
122
|
+
<span class="ridu-field-required" aria-hidden="true">*</span>
|
|
123
|
+
</label>
|
|
124
|
+
<span class="relative block">
|
|
125
|
+
<Input
|
|
126
|
+
id="ridu-first-user-password-confirmation"
|
|
127
|
+
class="pe-11"
|
|
128
|
+
type={confirmationVisible ? "text" : "password"}
|
|
129
|
+
autocomplete="new-password"
|
|
130
|
+
required
|
|
131
|
+
aria-invalid={controller.credentialIssue !== undefined}
|
|
132
|
+
aria-describedby="ridu-first-user-password-help"
|
|
133
|
+
bind:value={controller.passwordConfirmation}
|
|
134
|
+
oninput={() => (controller.credentialIssue = undefined)}
|
|
135
|
+
/>
|
|
136
|
+
<Button
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="icon-sm"
|
|
139
|
+
class="absolute top-1/2 end-2.5 -translate-y-1/2"
|
|
140
|
+
onclick={() => (confirmationVisible = !confirmationVisible)}
|
|
141
|
+
aria-label={confirmationVisible
|
|
142
|
+
? runtime.i18n.t("auth:hidePasswordConfirmation")
|
|
143
|
+
: runtime.i18n.t("auth:showPasswordConfirmation")}
|
|
144
|
+
>
|
|
145
|
+
{#if confirmationVisible}
|
|
146
|
+
<EyeOffIcon class="size-3.5" />
|
|
147
|
+
{:else}
|
|
148
|
+
<EyeIcon class="size-3.5" />
|
|
149
|
+
{/if}
|
|
150
|
+
</Button>
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<p
|
|
155
|
+
id="ridu-first-user-password-help"
|
|
156
|
+
class={controller.credentialIssue === undefined ? "ridu-field-help" : "ridu-field-error"}
|
|
157
|
+
role={controller.credentialIssue === undefined ? undefined : "alert"}
|
|
158
|
+
>
|
|
159
|
+
{controller.credentialIssue ?? controller.passwordHelp}
|
|
160
|
+
</p>
|
|
161
|
+
</fieldset>
|
|
162
|
+
</fieldset>
|
|
163
|
+
|
|
164
|
+
{#if controller.error !== undefined}
|
|
165
|
+
<Banner tone="destructive">{controller.error}</Banner>
|
|
166
|
+
{/if}
|
|
167
|
+
|
|
168
|
+
<Button class="h-10 w-full" type="submit" size="lg" disabled={controller.pending}>
|
|
169
|
+
{controller.pending
|
|
170
|
+
? runtime.i18n.t("auth:creatingAccount")
|
|
171
|
+
: runtime.i18n.t("auth:createAccount")}
|
|
172
|
+
</Button>
|
|
173
|
+
</form>
|
|
174
|
+
</AuthFrame>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Link } from "@hvniel/svelte-router";
|
|
3
|
+
import { RiduError } from "@riducms/sdk";
|
|
4
|
+
|
|
5
|
+
import { Banner } from "@admin/components/ui/banner";
|
|
6
|
+
import { Button } from "@admin/components/ui/button";
|
|
7
|
+
import { Input } from "@admin/components/ui/input";
|
|
8
|
+
import { adminRoutePatterns } from "@admin/core/routing/admin-paths";
|
|
9
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
10
|
+
import AuthFrame from "@admin/features/auth/auth-frame.svelte";
|
|
11
|
+
|
|
12
|
+
const runtime = getAdminRuntime();
|
|
13
|
+
const authCollection = $derived(runtime.authCollection);
|
|
14
|
+
let email = $state("");
|
|
15
|
+
let pending = $state(false);
|
|
16
|
+
let sent = $state(false);
|
|
17
|
+
let error = $state<string>();
|
|
18
|
+
|
|
19
|
+
async function submit(event: SubmitEvent) {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
if (authCollection === undefined) return;
|
|
22
|
+
pending = true;
|
|
23
|
+
error = undefined;
|
|
24
|
+
try {
|
|
25
|
+
await runtime.client.requestPasswordReset(authCollection.slug, email);
|
|
26
|
+
sent = true;
|
|
27
|
+
} catch (cause) {
|
|
28
|
+
error =
|
|
29
|
+
cause instanceof RiduError ? cause.message : runtime.i18n.t("auth:resetRequestFailed");
|
|
30
|
+
} finally {
|
|
31
|
+
pending = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<AuthFrame>
|
|
37
|
+
{#if sent}
|
|
38
|
+
<div class="grid gap-5">
|
|
39
|
+
<h1 class="text-[32px] leading-tight font-medium tracking-[-0.025em] text-foreground">
|
|
40
|
+
{runtime.i18n.t("auth:emailSent")}
|
|
41
|
+
</h1>
|
|
42
|
+
<p class="max-w-112 text-[13.5px] leading-5.5 text-foreground-muted">
|
|
43
|
+
{runtime.i18n.t("auth:passwordResetSent")}
|
|
44
|
+
</p>
|
|
45
|
+
<Link
|
|
46
|
+
class="w-fit text-[12.5px] text-foreground underline underline-offset-2 hover:text-foreground-muted"
|
|
47
|
+
to={adminRoutePatterns.login}
|
|
48
|
+
>
|
|
49
|
+
{runtime.i18n.t("auth:backToLogin")}
|
|
50
|
+
</Link>
|
|
51
|
+
</div>
|
|
52
|
+
{:else}
|
|
53
|
+
<form class="grid gap-5" onsubmit={submit}>
|
|
54
|
+
<div>
|
|
55
|
+
<h1 class="text-[32px] leading-tight font-medium tracking-[-0.025em] text-foreground">
|
|
56
|
+
{runtime.i18n.t("auth:forgotPassword")}
|
|
57
|
+
</h1>
|
|
58
|
+
<p class="mt-2 text-[13.5px] leading-5.5 text-foreground-muted">
|
|
59
|
+
{runtime.i18n.t("auth:forgotPasswordDescription")}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<label class="grid gap-2" for="reset-email">
|
|
63
|
+
<span class="ridu-field-label">{runtime.i18n.t("auth:email")}</span>
|
|
64
|
+
<Input id="reset-email" type="email" autocomplete="email" bind:value={email} required />
|
|
65
|
+
</label>
|
|
66
|
+
{#if error !== undefined}<Banner tone="destructive">{error}</Banner>{/if}
|
|
67
|
+
<Button class="h-10 w-full" type="submit" size="lg" disabled={pending}>
|
|
68
|
+
{pending ? runtime.i18n.t("auth:sending") : runtime.i18n.t("auth:sendResetLink")}
|
|
69
|
+
</Button>
|
|
70
|
+
<Link
|
|
71
|
+
class="w-fit text-[12.5px] text-foreground underline underline-offset-2 hover:text-foreground-muted"
|
|
72
|
+
to={adminRoutePatterns.login}
|
|
73
|
+
>
|
|
74
|
+
{runtime.i18n.t("auth:backToLogin")}
|
|
75
|
+
</Link>
|
|
76
|
+
</form>
|
|
77
|
+
{/if}
|
|
78
|
+
</AuthFrame>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Link, useLocation } from "@hvniel/svelte-router";
|
|
3
|
+
import { RiduError } from "@riducms/sdk";
|
|
4
|
+
import EyeIcon from "~icons/lucide/eye";
|
|
5
|
+
import EyeOffIcon from "~icons/lucide/eye-off";
|
|
6
|
+
|
|
7
|
+
import { Banner } from "@admin/components/ui/banner";
|
|
8
|
+
import { Button } from "@admin/components/ui/button";
|
|
9
|
+
import { Input } from "@admin/components/ui/input";
|
|
10
|
+
import { adminRedirectFromSearch, adminRoutePatterns } from "@admin/core/routing/admin-paths";
|
|
11
|
+
import { getNotificationCenter } from "@admin/core/notifications/notification-center.svelte";
|
|
12
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
13
|
+
import AuthBrand from "@admin/features/auth/auth-brand.svelte";
|
|
14
|
+
import AuthFrame from "@admin/features/auth/auth-frame.svelte";
|
|
15
|
+
|
|
16
|
+
const runtime = getAdminRuntime();
|
|
17
|
+
const notifications = getNotificationCenter();
|
|
18
|
+
const location = $derived(useLocation());
|
|
19
|
+
const postLoginPath = $derived(adminRedirectFromSearch(location.search));
|
|
20
|
+
let email = $state("");
|
|
21
|
+
let password = $state("");
|
|
22
|
+
let passwordVisible = $state(false);
|
|
23
|
+
let pending = $state(false);
|
|
24
|
+
let error = $state<string>();
|
|
25
|
+
const authCollection = $derived(runtime.authCollection);
|
|
26
|
+
const replacement = $derived(
|
|
27
|
+
runtime.loginComponents.find((component) => component.position === "replace")
|
|
28
|
+
);
|
|
29
|
+
const loginLogo = $derived(
|
|
30
|
+
runtime.brandComponents.find((component) => component.surface === "loginLogo")
|
|
31
|
+
);
|
|
32
|
+
const beforeComponents = $derived(
|
|
33
|
+
runtime.loginComponents.filter((component) => component.position === "before")
|
|
34
|
+
);
|
|
35
|
+
const afterComponents = $derived(
|
|
36
|
+
runtime.loginComponents.filter(
|
|
37
|
+
(component) => component.position === undefined || component.position === "after"
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
const host = {
|
|
41
|
+
login: signIn,
|
|
42
|
+
notify: (tone: "success" | "error", title: string, message?: string) =>
|
|
43
|
+
notifications[tone]({ title, message }),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async function signIn(credentials: { email: string; password: string }) {
|
|
47
|
+
if (authCollection === undefined) {
|
|
48
|
+
throw new Error(runtime.i18n.t("auth:configuredCollectionUnavailable"));
|
|
49
|
+
}
|
|
50
|
+
const session = await runtime.client.login(authCollection.slug, credentials);
|
|
51
|
+
const destinationLocale = runtime.contentLocaleForPath(postLoginPath);
|
|
52
|
+
const destinationAccessAuthoritative = await runtime.refreshAccess(
|
|
53
|
+
destinationLocale ?? runtime.contentLocale
|
|
54
|
+
);
|
|
55
|
+
if (
|
|
56
|
+
destinationAccessAuthoritative &&
|
|
57
|
+
runtime.collectionOperations[authCollection.slug]?.admin !== true
|
|
58
|
+
) {
|
|
59
|
+
try {
|
|
60
|
+
await runtime.client.logout();
|
|
61
|
+
} catch {
|
|
62
|
+
// The shell remains denied even if server cleanup fails.
|
|
63
|
+
}
|
|
64
|
+
throw new Error(runtime.i18n.t("auth:adminAccessDenied"));
|
|
65
|
+
}
|
|
66
|
+
// Keep this route mounted until access and the destination are ready.
|
|
67
|
+
// Assigning the session before the await swaps the route tree and leaves
|
|
68
|
+
// this component's navigate function inactive.
|
|
69
|
+
runtime.session = session;
|
|
70
|
+
await Promise.all([
|
|
71
|
+
runtime.loadTheme(),
|
|
72
|
+
runtime.loadContentLocale(destinationLocale),
|
|
73
|
+
runtime.i18n.loadPreferences(),
|
|
74
|
+
]);
|
|
75
|
+
const preferredAccessAuthoritative = await runtime.refreshAccess(runtime.contentLocale);
|
|
76
|
+
if (
|
|
77
|
+
preferredAccessAuthoritative &&
|
|
78
|
+
runtime.collectionOperations[authCollection.slug]?.admin !== true
|
|
79
|
+
) {
|
|
80
|
+
try {
|
|
81
|
+
await runtime.client.logout();
|
|
82
|
+
} catch {
|
|
83
|
+
// The shell remains denied even if server cleanup fails.
|
|
84
|
+
}
|
|
85
|
+
runtime.session = undefined;
|
|
86
|
+
throw new Error(runtime.i18n.t("auth:adminAccessDenied"));
|
|
87
|
+
}
|
|
88
|
+
// Assigning the authenticated session swaps this route for the authenticated
|
|
89
|
+
// route tree. Its /login route owns the deferred redirect so this component
|
|
90
|
+
// never reads route-owned derived state after it has been destroyed.
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function login() {
|
|
94
|
+
pending = true;
|
|
95
|
+
error = undefined;
|
|
96
|
+
try {
|
|
97
|
+
await signIn({ email, password });
|
|
98
|
+
} catch (cause) {
|
|
99
|
+
error =
|
|
100
|
+
cause instanceof RiduError || cause instanceof Error
|
|
101
|
+
? cause.message
|
|
102
|
+
: runtime.i18n.t("auth:loginFailed");
|
|
103
|
+
} finally {
|
|
104
|
+
pending = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleSubmit(event: SubmitEvent) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
await login();
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
{#snippet brand()}
|
|
115
|
+
{#if loginLogo !== undefined && runtime.manifest !== undefined}
|
|
116
|
+
<loginLogo.component
|
|
117
|
+
manifest={runtime.manifest}
|
|
118
|
+
user={runtime.session?.user}
|
|
119
|
+
surface="loginLogo"
|
|
120
|
+
i18n={runtime.i18n}
|
|
121
|
+
/>
|
|
122
|
+
{:else}
|
|
123
|
+
<AuthBrand />
|
|
124
|
+
{/if}
|
|
125
|
+
{/snippet}
|
|
126
|
+
|
|
127
|
+
{#snippet defaultView()}
|
|
128
|
+
<AuthFrame {brand}>
|
|
129
|
+
{#if runtime.manifest !== undefined}
|
|
130
|
+
{#each beforeComponents as component (component.key)}
|
|
131
|
+
<component.component manifest={runtime.manifest} {defaultView} {host} i18n={runtime.i18n} />
|
|
132
|
+
{/each}
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
<form class="grid gap-5" onsubmit={handleSubmit}>
|
|
136
|
+
<h1 class="sr-only">{runtime.i18n.t("auth:login")}</h1>
|
|
137
|
+
|
|
138
|
+
<label class="grid gap-2" for="ridu-login-email">
|
|
139
|
+
<span class="ridu-field-label text-foreground-body">{runtime.i18n.t("auth:email")}</span>
|
|
140
|
+
<Input
|
|
141
|
+
id="ridu-login-email"
|
|
142
|
+
type="email"
|
|
143
|
+
bind:value={email}
|
|
144
|
+
autocomplete="email"
|
|
145
|
+
placeholder={runtime.i18n.t("auth:emailPlaceholder")}
|
|
146
|
+
required
|
|
147
|
+
/>
|
|
148
|
+
</label>
|
|
149
|
+
|
|
150
|
+
<div class="grid gap-2">
|
|
151
|
+
<label class="ridu-field-label text-foreground-body" for="ridu-login-password">
|
|
152
|
+
{runtime.i18n.t("auth:password")}
|
|
153
|
+
</label>
|
|
154
|
+
<span class="relative block">
|
|
155
|
+
<Input
|
|
156
|
+
id="ridu-login-password"
|
|
157
|
+
class="pe-11"
|
|
158
|
+
type={passwordVisible ? "text" : "password"}
|
|
159
|
+
bind:value={password}
|
|
160
|
+
autocomplete="current-password"
|
|
161
|
+
placeholder={runtime.i18n.t("auth:passwordPlaceholder")}
|
|
162
|
+
required
|
|
163
|
+
/>
|
|
164
|
+
<Button
|
|
165
|
+
variant="ghost"
|
|
166
|
+
size="icon-sm"
|
|
167
|
+
class="absolute top-1/2 end-2.5 -translate-y-1/2"
|
|
168
|
+
onclick={() => (passwordVisible = !passwordVisible)}
|
|
169
|
+
aria-label={passwordVisible
|
|
170
|
+
? runtime.i18n.t("auth:hidePassword")
|
|
171
|
+
: runtime.i18n.t("auth:showPassword")}
|
|
172
|
+
>
|
|
173
|
+
{#if passwordVisible}
|
|
174
|
+
<EyeOffIcon class="size-3.5" />
|
|
175
|
+
{:else}
|
|
176
|
+
<EyeIcon class="size-3.5" />
|
|
177
|
+
{/if}
|
|
178
|
+
</Button>
|
|
179
|
+
</span>
|
|
180
|
+
{#if authCollection?.authSettings?.passwordReset}
|
|
181
|
+
<Link
|
|
182
|
+
class="w-fit text-[12px] text-foreground underline underline-offset-2 hover:text-foreground-muted"
|
|
183
|
+
to={adminRoutePatterns.forgotPassword}
|
|
184
|
+
>
|
|
185
|
+
{runtime.i18n.t("auth:forgotPassword")}
|
|
186
|
+
</Link>
|
|
187
|
+
{/if}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{#if error !== undefined}
|
|
191
|
+
<Banner tone="destructive">
|
|
192
|
+
<span class="size-2 shrink-0 rounded-full bg-destructive" aria-hidden="true"></span>
|
|
193
|
+
{error}
|
|
194
|
+
</Banner>
|
|
195
|
+
{/if}
|
|
196
|
+
|
|
197
|
+
<Button class="mt-1 h-10 w-full" type="submit" size="lg" disabled={pending}>
|
|
198
|
+
{pending ? runtime.i18n.t("auth:signingIn") : runtime.i18n.t("auth:login")}
|
|
199
|
+
</Button>
|
|
200
|
+
{#if authCollection?.authSettings?.verifyEmail}
|
|
201
|
+
<Link
|
|
202
|
+
class="text-center text-[11.5px] text-foreground-muted hover:text-primary"
|
|
203
|
+
to={adminRoutePatterns.requestVerification}
|
|
204
|
+
>
|
|
205
|
+
{runtime.i18n.t("auth:requestVerification")}
|
|
206
|
+
</Link>
|
|
207
|
+
{/if}
|
|
208
|
+
</form>
|
|
209
|
+
{#if runtime.manifest !== undefined}
|
|
210
|
+
{#each afterComponents as component (component.key)}
|
|
211
|
+
<component.component manifest={runtime.manifest} {defaultView} {host} i18n={runtime.i18n} />
|
|
212
|
+
{/each}
|
|
213
|
+
{/if}
|
|
214
|
+
</AuthFrame>
|
|
215
|
+
{/snippet}
|
|
216
|
+
|
|
217
|
+
{#if runtime.manifest !== undefined && replacement !== undefined}
|
|
218
|
+
<replacement.component manifest={runtime.manifest} {defaultView} {host} i18n={runtime.i18n} />
|
|
219
|
+
{:else}
|
|
220
|
+
{@render defaultView()}
|
|
221
|
+
{/if}
|