mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-08-17 21:25:52 +02:00
Merge branch 'main' of github.com:open-reception/appointment-booking-software into 153-book-appointment-allow-clients-to-select-if-they-want-email-notifications
This commit is contained in:
Generated
+3
-3
@@ -4214,9 +4214,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz",
|
||||
"integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dlv": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"welcome": "Willkommen bei OpenReception",
|
||||
"home": "Startseite",
|
||||
"poweredBy": "Betrieben mit",
|
||||
"i18n": {
|
||||
"label": "Sprache wählen",
|
||||
@@ -108,9 +109,13 @@
|
||||
"action": "Passkey setzen",
|
||||
"success": "Passkey eingerichtet. Logge Dich damit jetzt ein.",
|
||||
"successKeyPairSaved": "Schlüsselpaar erfolgreich gespeichert.",
|
||||
"confirmPrfRetrival": "Jetzt verwende den neu erstellten Passkey, um damit die Verschlüsselung zu aktivieren.",
|
||||
"error": "Passkey konnte nicht gesetzt werden",
|
||||
"errorKeyPairDataMissing": "Konnte Schlüsselpaar nicht generieren. Fehlende Daten.",
|
||||
"errorKeyPairNotSaved": "Schlüsselpaar konnte nicht gespeichert werden."
|
||||
"errorKeyPairNotSaved": "Schlüsselpaar konnte nicht gespeichert werden.",
|
||||
"errorPrfOutputNotTriggered": "Sie müssen der Verwendung des Passkeys zustimmen.",
|
||||
"errorAuthenticatorNotSupported": "Dieser Authentifikator unterstützt nicht die erforderliche Sicherheitserweiterung (PRF). Bitte verwenden Sie einen modernen Authentifikator wie YubiKey 5.2.3+, Apple Touch ID oder Android.",
|
||||
"errorGettingPrfOutput": "Fehler beim Abrufen der Sicherheitsdaten vom Passkey. Bitte verwenden Sie einen modernen Authentifikator, der die PRF-Erweiterung unterstützt."
|
||||
},
|
||||
"logout": {
|
||||
"title": "Erfolgreich abgemeldet",
|
||||
@@ -694,7 +699,8 @@
|
||||
"title": "Telefonnummer erforderlich",
|
||||
"label": "Kunden müssen eine Telefonnummer angeben, wenn sie einen Termin buchen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"action": "Speichern"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
@@ -901,6 +907,7 @@
|
||||
"today": "Heute",
|
||||
"loading": "Loading calendar",
|
||||
"decrypting": "wird entschlüsselt...",
|
||||
"decryptingError": "Entschlüsselung fehlgeschlagen. Bitte neu anmelden.",
|
||||
"shownAppointments": {
|
||||
"title": "Termine",
|
||||
"options": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"welcome": "Welcome to OpenReception",
|
||||
"home": "Home",
|
||||
"poweredBy": "Powered by",
|
||||
"i18n": {
|
||||
"label": "Select Language",
|
||||
@@ -41,9 +42,13 @@
|
||||
"action": "Set Passkey",
|
||||
"success": "Passkey set. Login with it now.",
|
||||
"successKeyPairSaved": "Key pair saved successfully.",
|
||||
"confirmPrfRetrival": "Now use your newly created passkey to enable encryption with it.",
|
||||
"error": "Passkey could not be set",
|
||||
"errorKeyPairDataMissing": "Could not generate key pair. Missing data.",
|
||||
"errorKeyPairNotSaved": "Key pair could not be saved."
|
||||
"errorKeyPairNotSaved": "Key pair could not be saved.",
|
||||
"errorPrfOutputNotTriggered": "You have to confirm using the passkey.",
|
||||
"errorAuthenticatorNotSupported": "This authenticator does not support the required security extension (PRF). Please use a modern authenticator like YubiKey 5.2.3+, Apple Touch ID or Android.",
|
||||
"errorGettingPrfOutput": "Failed to retrieve security data from passkey. Please use a modern authenticator that supports PRF extension."
|
||||
},
|
||||
"slogan": "End-to-End encrypted appointment booking platform.",
|
||||
"logo": "OpenReception Logo: A 3D pixel art with calendar that's also a lock",
|
||||
@@ -703,7 +708,8 @@
|
||||
"title": "Require Phone Number",
|
||||
"label": "Require clients to provide a phone number when booking an appointment."
|
||||
}
|
||||
}
|
||||
},
|
||||
"action": "Save"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
@@ -910,6 +916,7 @@
|
||||
"today": "Today",
|
||||
"loading": "Loading calendar",
|
||||
"decrypting": "decrypting...",
|
||||
"decryptingError": "Unable to decrypt data. Please log-in again",
|
||||
"shownAppointments": {
|
||||
"title": "Appointments",
|
||||
"options": {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if locales && options.length > 1}
|
||||
{#if options.length > 1}
|
||||
<div class={className}>
|
||||
<ComboBox
|
||||
{options}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { eq, and, not, count, or } from "drizzle-orm";
|
||||
import logger from "$lib/logger";
|
||||
import { z } from "zod";
|
||||
import { ValidationError, NotFoundError, ConflictError } from "../utils/errors";
|
||||
import { redactDbUrl } from "../utils/url";
|
||||
|
||||
if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set");
|
||||
|
||||
@@ -110,7 +111,7 @@ export class TenantAdminService {
|
||||
} catch (dbError) {
|
||||
log.error("Failed to initialize tenant database", {
|
||||
tenantId: tenant[0].id,
|
||||
databaseUrl: newTenant.databaseUrl,
|
||||
databaseUrl: redactDbUrl(newTenant.databaseUrl),
|
||||
error: String(dbError),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const redactDbUrl = (input: string) => {
|
||||
const url = new URL(input);
|
||||
url.username = "redacted-user";
|
||||
url.password = "redacted-pw";
|
||||
return url.toString();
|
||||
};
|
||||
+1
-1
@@ -85,7 +85,7 @@
|
||||
<li class="mb-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="flex h-auto w-full cursor-pointer items-start gap-9 px-3 py-2.5 text-left"
|
||||
class="hover:border-primary/30 flex h-auto w-full cursor-pointer items-start gap-9 px-3 py-2.5 text-left"
|
||||
onclick={() => selectAgent(null)}
|
||||
>
|
||||
<div class="relative size-10">
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
<li>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="flex h-auto w-full cursor-pointer flex-col items-start gap-1 px-3 py-2.5 text-left"
|
||||
class="hover:border-primary/30 flex h-auto w-full cursor-pointer flex-col items-start gap-1 px-3 py-2.5 text-left"
|
||||
onclick={() =>
|
||||
proceed({
|
||||
...appointment,
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
appointment.slot.duration,
|
||||
tenant.id,
|
||||
Boolean(appointment.isNewClient),
|
||||
getLocale() || "de",
|
||||
getLocale() || "en",
|
||||
)
|
||||
.then(() => {
|
||||
const isRequest = channel.requiresConfirmation;
|
||||
@@ -61,7 +61,7 @@
|
||||
{m["public.steps.summary.request.hint"]()}
|
||||
</Text>
|
||||
{/if}
|
||||
<Button isLoading={isSubmitting} onclick={onSubmit}>
|
||||
<Button isLoading={isSubmitting} disabled={isSubmitting} onclick={onSubmit}>
|
||||
{#if channel.requiresConfirmation}
|
||||
{m["public.steps.summary.request.action"]()}
|
||||
{:else}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
let passkeyId: string | undefined = $state();
|
||||
let prfOutput: ArrayBuffer | undefined = $state();
|
||||
let kyberKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array } | undefined = $state();
|
||||
let registrationChallenge: string | undefined = $state(); // Store first challenge for form submission
|
||||
let registrationChallenge: string | undefined = $state();
|
||||
|
||||
const form = superForm(data.form, {
|
||||
validators: zodClient(formSchema),
|
||||
@@ -111,9 +111,7 @@
|
||||
email: $formData.email,
|
||||
extensions: extensionResults,
|
||||
});
|
||||
toast.error(
|
||||
"This authenticator does not support the required security extension (PRF). Please use a modern authenticator like YubiKey 5.2.3+, Titan Gen2, Windows Hello, Touch ID, or Android.",
|
||||
);
|
||||
toast.warning(m["setupPasskey.errorAuthenticatorNotSupported"]());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,6 +119,15 @@
|
||||
const attestationObjectResp = passkeyResp.response.attestationObject;
|
||||
const clientDataJSONResp = passkeyResp.response.clientDataJSON;
|
||||
|
||||
// UX Primer for second passkey UI
|
||||
const isConfirmed = confirm(m["setupPasskey.confirmPrfRetrival"]());
|
||||
if (!isConfirmed) {
|
||||
$passkeyLoading = "error";
|
||||
toast.error(m["setupPasskey.errorPrfOutputNotTriggered"]());
|
||||
logger.warn("PRF challenge primer not confirmed");
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Get PRF output immediately after passkey creation
|
||||
// This is the only time we can retrieve the PRF output
|
||||
// Uses email as salt for multi-passkey support
|
||||
@@ -149,9 +156,7 @@
|
||||
passkeyId: passkeyResp.id,
|
||||
error,
|
||||
});
|
||||
toast.error(
|
||||
"Failed to retrieve security data from passkey. Please use a modern authenticator that supports PRF extension.",
|
||||
);
|
||||
toast.error(m["setupPasskey.errorGettingPrfOutput"]());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
};
|
||||
|
||||
const initStaffCrypto = async () => {
|
||||
if (data?.user?.id && data?.user?.tenantId) {
|
||||
if (data?.user?.id && data?.user?.tenantId && data?.user.role !== "GLOBAL_ADMIN") {
|
||||
// Try to initialize from session storage (from login)
|
||||
const success = await staffCrypto.initFromSession(data.user.id, data.user.tenantId);
|
||||
if (success) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from "$app/navigation";
|
||||
import { m } from "$i18n/messages";
|
||||
import { MaxPageWidth } from "$lib/components/layouts/max-page-width";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
@@ -61,10 +60,9 @@
|
||||
{/snippet}
|
||||
<AddAgentForm
|
||||
done={() => {
|
||||
tenants.reload();
|
||||
agents.load();
|
||||
channels.load();
|
||||
tenants.reload();
|
||||
invalidate(ROUTES.DASHBOARD.AGENTS);
|
||||
closeDialog("add");
|
||||
}}
|
||||
/>
|
||||
@@ -119,7 +117,6 @@
|
||||
curItem = null;
|
||||
agents.load();
|
||||
channels.load();
|
||||
invalidate(ROUTES.DASHBOARD.AGENTS);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -134,7 +131,6 @@
|
||||
agents.load();
|
||||
channels.load();
|
||||
tenants.reload();
|
||||
invalidate(ROUTES.DASHBOARD.AGENTS);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -84,7 +84,9 @@
|
||||
|
||||
{#if error}
|
||||
<div class="text-destructive flex h-full items-center gap-1 px-1">
|
||||
<Text style="xs" class="leading-none" title={error}>⚠️</Text>
|
||||
<Text style="xs" class="leading-none" title={error}>
|
||||
⚠️ {m["calendar.decryptingError"]()}
|
||||
</Text>
|
||||
</div>
|
||||
{:else if decrypted === undefined}
|
||||
<div class="flex h-full items-center gap-1 px-1">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from "$app/navigation";
|
||||
import { m } from "$i18n/messages";
|
||||
import { MaxPageWidth } from "$lib/components/layouts/max-page-width";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
@@ -23,6 +22,7 @@
|
||||
import DeleteChannelForm from "./(components)/delete-channel-form/delete-channel-form.svelte";
|
||||
import EditChannelForm from "./(components)/edit-channel-form/edit-channel-form.svelte";
|
||||
import { PauseChannelForm } from "./(components)/pause-channel-form";
|
||||
import { channels } from "$lib/stores/channels";
|
||||
|
||||
const { data } = $props();
|
||||
let curItem: TChannelWithFullAgents | null = $state(null);
|
||||
@@ -63,7 +63,7 @@
|
||||
<AddChannelForm
|
||||
done={() => {
|
||||
tenants.reload();
|
||||
invalidate(ROUTES.DASHBOARD.CHANNELS);
|
||||
channels.load();
|
||||
closeDialog("add");
|
||||
}}
|
||||
/>
|
||||
@@ -127,7 +127,7 @@
|
||||
done={() => {
|
||||
closeDialog("edit");
|
||||
curItem = null;
|
||||
invalidate(ROUTES.DASHBOARD.CHANNELS);
|
||||
channels.load();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -143,7 +143,7 @@
|
||||
done={() => {
|
||||
closeDialog("pause");
|
||||
curItem = null;
|
||||
invalidate(ROUTES.DASHBOARD.CHANNELS);
|
||||
channels.load();
|
||||
}}
|
||||
/>
|
||||
</ResponsiveDialog>
|
||||
@@ -156,7 +156,7 @@
|
||||
closeDialog("delete");
|
||||
curItem = null;
|
||||
tenants.reload();
|
||||
invalidate(ROUTES.DASHBOARD.CHANNELS);
|
||||
channels.load();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
+1
-1
@@ -333,7 +333,7 @@
|
||||
disabled={isSubmitting}
|
||||
class="sm:ml-auto"
|
||||
>
|
||||
{m["tenants.edit.action"]()}
|
||||
{m["settings.form.action"]()}
|
||||
</Form.Button>
|
||||
</div>
|
||||
</Form.Root>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { m } from "$i18n/messages";
|
||||
import { z } from "zod";
|
||||
|
||||
const optionalUrl = (errorMessage: string) =>
|
||||
z.union([z.literal(""), z.url({ message: errorMessage })]).optional();
|
||||
|
||||
export const formSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
@@ -18,9 +21,9 @@ export const formSchema = z
|
||||
city: z.string().min(1, { message: m["form.errors.city"]() }),
|
||||
}),
|
||||
links: z.object({
|
||||
website: z.string().url({ message: m["form.errors.url"]() }).optional().default(""),
|
||||
imprint: z.string().url({ message: m["form.errors.url"]() }).optional().default(""),
|
||||
privacyStatement: z.string().url({ message: m["form.errors.url"]() }).optional().default(""),
|
||||
website: optionalUrl(m["form.errors.url"]()),
|
||||
imprint: optionalUrl(m["form.errors.url"]()),
|
||||
privacyStatement: optionalUrl(m["form.errors.url"]()),
|
||||
}),
|
||||
settings: z.object({
|
||||
autoDeleteDays: z.number().min(30, { message: m["form.errors.deleteAfterDays"]() }),
|
||||
|
||||
+2
-6
@@ -4,7 +4,6 @@
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { supportedLocales, translatedLocales } from "$lib/const/locales";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import type { TStaff } from "$lib/types/users";
|
||||
import { toast } from "svelte-sonner";
|
||||
@@ -18,10 +17,7 @@
|
||||
let { done }: { done: () => void } = $props();
|
||||
|
||||
const tenantLocales = get(tenants).currentTenant?.languages ?? [];
|
||||
const userRole = $derived($auth.user?.role ?? "STAFF");
|
||||
const availableRolesForUser = $derived(
|
||||
userRole === "GLOBAL_ADMIN" ? roles : roles.filter((it) => it.value !== "GLOBAL_ADMIN"),
|
||||
);
|
||||
const availableRoles = $derived(roles.filter((it) => it.value !== "GLOBAL_ADMIN"));
|
||||
const form = superForm(
|
||||
{
|
||||
name: "",
|
||||
@@ -114,7 +110,7 @@
|
||||
{value ? value : m["staff.form.fields.role.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each availableRolesForUser as role (role.value)}
|
||||
{#each availableRoles as role (role.value)}
|
||||
<Select.Item value={role.value}>
|
||||
{role.label}
|
||||
</Select.Item>
|
||||
|
||||
+2
-6
@@ -3,7 +3,6 @@
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import type { TStaff } from "$lib/types/users";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
@@ -14,10 +13,7 @@
|
||||
|
||||
let { entity, done }: { entity: TStaff; done: () => void } = $props();
|
||||
|
||||
const userRole = $derived($auth.user?.role ?? "STAFF");
|
||||
const availableRolesForUser = $derived(
|
||||
userRole === "GLOBAL_ADMIN" ? roles : roles.filter((it) => it.value !== "GLOBAL_ADMIN"),
|
||||
);
|
||||
const availableRoles = $derived(roles.filter((it) => it.value !== "GLOBAL_ADMIN"));
|
||||
const form = superForm(
|
||||
{
|
||||
id: entity.id,
|
||||
@@ -80,7 +76,7 @@
|
||||
{value ? value : m["staff.form.fields.role.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each availableRolesForUser as role (role.value)}
|
||||
{#each availableRoles as role (role.value)}
|
||||
<Select.Item value={role.value}>
|
||||
{role.label}
|
||||
</Select.Item>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fail } from "@sveltejs/kit";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod4 as zod } from "sveltekit-superforms/adapters";
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import type { Actions } from "./$types";
|
||||
import { formSchema } from "./schema";
|
||||
import type { WebAuthnCredential } from "$lib/server/auth/webauthn-service";
|
||||
import type { TUser } from "$lib/types/user";
|
||||
@@ -9,12 +9,6 @@ import logger from "$lib/logger";
|
||||
|
||||
const log = logger.setContext(import.meta.filename);
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
return {
|
||||
form: await superValidate(zod(formSchema)),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event, zod(formSchema));
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
import { PageWithClaim } from "$lib/components/ui/page";
|
||||
import LoginForm from "./login-form.svelte";
|
||||
import type { EventReporter } from "$lib/components/ui/form/form-root.svelte";
|
||||
|
||||
const { data } = $props();
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
|
||||
const formId = "create-account-form";
|
||||
let isSubmitting = $state(false);
|
||||
@@ -26,6 +26,9 @@
|
||||
</svelte:head>
|
||||
|
||||
<PageWithClaim isWithLanguageSwitch>
|
||||
{#snippet left()}
|
||||
<Button href={ROUTES.MAIN} size="sm" variant="link">{m["home"]()}</Button>
|
||||
{/snippet}
|
||||
<CenteredCard.Root>
|
||||
<CenteredCard.Header>
|
||||
<CenteredCard.Title>
|
||||
@@ -36,7 +39,7 @@
|
||||
</CenteredCard.Description>
|
||||
</CenteredCard.Header>
|
||||
<CenteredCard.Main>
|
||||
<LoginForm {formId} {data} {onEvent} />
|
||||
<LoginForm {formId} {onEvent} />
|
||||
</CenteredCard.Main>
|
||||
<CenteredCard.Action>
|
||||
<Form.Button
|
||||
|
||||
@@ -20,31 +20,37 @@
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import logger from "$lib/logger";
|
||||
|
||||
let {
|
||||
data,
|
||||
formId,
|
||||
onEvent,
|
||||
}: { formId: string; onEvent: EventReporter; data: { form: SuperValidated<Infer<FormSchema>> } } =
|
||||
$props();
|
||||
let { formId, onEvent }: { formId: string; onEvent: EventReporter } = $props();
|
||||
|
||||
const form = superForm(data.form, {
|
||||
validators: zodClient(formSchema),
|
||||
onChange: (event) => {
|
||||
if (event.paths.includes("email")) {
|
||||
setProperPasskeyState();
|
||||
}
|
||||
const form = superForm(
|
||||
{
|
||||
email: "",
|
||||
type: "passkey",
|
||||
id: "",
|
||||
passphrase: "passphrase",
|
||||
clientDataBase64: "",
|
||||
authenticatorDataBase64: "",
|
||||
signatureBase64: "",
|
||||
},
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
auth.setUser(event.result.data?.user);
|
||||
await goto(ROUTES.DASHBOARD.MAIN);
|
||||
} else {
|
||||
toast.error(m["login.error"]());
|
||||
}
|
||||
onEvent({ isSubmitting: false });
|
||||
{
|
||||
validators: zodClient(formSchema),
|
||||
onChange: (event) => {
|
||||
if (event.paths.includes("email")) {
|
||||
setProperPasskeyState();
|
||||
}
|
||||
},
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
auth.setUser(event.result.data?.user);
|
||||
await goto(ROUTES.DASHBOARD.MAIN);
|
||||
} else {
|
||||
toast.error(m["login.error"]());
|
||||
}
|
||||
onEvent({ isSubmitting: false });
|
||||
},
|
||||
onSubmit: () => onEvent({ isSubmitting: true }),
|
||||
},
|
||||
onSubmit: () => onEvent({ isSubmitting: true }),
|
||||
});
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
$formData.type = "passkey";
|
||||
@@ -186,14 +192,13 @@
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["form.passphrase"]()}</Form.Label>
|
||||
<!-- prettier-ignore -->
|
||||
<Input
|
||||
{...props}
|
||||
bind:value={($formData as FormDataPassphrase).passphrase}
|
||||
type="password"
|
||||
minlength={30}
|
||||
maxlength={100}
|
||||
/>
|
||||
{...props}
|
||||
bind:value={$formData.passphrase}
|
||||
type="password"
|
||||
minlength={30}
|
||||
maxlength={100}
|
||||
/>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
@@ -210,48 +215,28 @@
|
||||
<Form.Field {form} name="id" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<!-- prettier-ignore -->
|
||||
<Input
|
||||
{...props}
|
||||
bind:value={($formData as FormDataPasskey).id}
|
||||
type="hidden"
|
||||
/>
|
||||
<Input {...props} bind:value={$formData.id} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="authenticatorDataBase64" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<!-- prettier-ignore -->
|
||||
<Input
|
||||
{...props}
|
||||
bind:value={($formData as FormDataPasskey).authenticatorDataBase64}
|
||||
type="hidden"
|
||||
/>
|
||||
<Input {...props} bind:value={$formData.authenticatorDataBase64} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="clientDataBase64" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<!-- prettier-ignore -->
|
||||
<Input
|
||||
{...props}
|
||||
bind:value={($formData as FormDataPasskey).clientDataBase64}
|
||||
type="hidden"
|
||||
/>
|
||||
<Input {...props} bind:value={$formData.clientDataBase64} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="signatureBase64" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<!-- prettier-ignore -->
|
||||
<Input
|
||||
{...props}
|
||||
bind:value={($formData as FormDataPasskey).signatureBase64}
|
||||
type="hidden"
|
||||
/>
|
||||
<Input {...props} bind:value={$formData.signatureBase64} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
|
||||
@@ -97,7 +97,7 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
maxAge: 60 * 15,
|
||||
});
|
||||
|
||||
logger.info("Tenant switched successfully", {
|
||||
|
||||
@@ -16,7 +16,7 @@ const inviteUserSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
role: z.enum(["TENANT_ADMIN", "STAFF"]),
|
||||
tenantId: z.string().uuid(),
|
||||
language: z.enum(["de", "en"]).optional().default("de"),
|
||||
language: z.enum(["de", "en"]).optional().default("en"),
|
||||
});
|
||||
|
||||
registerOpenAPIRoute("/auth/invite", "POST", {
|
||||
|
||||
@@ -279,7 +279,7 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
expires: sessionData.expiresAt,
|
||||
});
|
||||
|
||||
logger.info("Login successful", {
|
||||
|
||||
@@ -102,7 +102,7 @@ export const POST: RequestHandler = async ({ cookies }) => {
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
expires: result.expiresAt,
|
||||
});
|
||||
|
||||
return json({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { checkPermission } from "$lib/server/utils/permissions";
|
||||
|
||||
const requestSchema = z.object({
|
||||
clientEmail: z.string().email().optional(),
|
||||
clientLanguage: z.string().optional().default("de"),
|
||||
clientLanguage: z.string().optional().default("en"),
|
||||
});
|
||||
|
||||
// Register OpenAPI documentation for PUT
|
||||
|
||||
@@ -28,7 +28,7 @@ const requestSchema = z.object({
|
||||
appointmentDate: z.string(),
|
||||
duration: z.number().int().positive(),
|
||||
clientEmail: z.string().email(),
|
||||
clientLanguage: z.string().optional().default("de"),
|
||||
clientLanguage: z.string().optional().default("en"),
|
||||
encryptedAppointment: z.object({
|
||||
encryptedPayload: z.string(),
|
||||
iv: z.string(),
|
||||
|
||||
@@ -13,7 +13,7 @@ const requestSchema = z.object({
|
||||
duration: z.number().int().positive(),
|
||||
emailHash: z.string(),
|
||||
clientEmail: z.string().email(),
|
||||
clientLanguage: z.string().optional().default("de"),
|
||||
clientLanguage: z.string().optional().default("en"),
|
||||
clientPublicKey: z.string(),
|
||||
privateKeyShare: z.string(),
|
||||
encryptedAppointment: z.object({
|
||||
|
||||
@@ -122,7 +122,10 @@ export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
|
||||
try {
|
||||
const staff = await StaffService.getStaffMembers(tenantId);
|
||||
return json({ staff: staff });
|
||||
|
||||
const filteredStaff = staff.filter((member) => member.role !== "GLOBAL_ADMIN");
|
||||
|
||||
return json({ staff: filteredStaff });
|
||||
} catch (error) {
|
||||
logError(log)("Error fetching staff data", error, locals.user?.id, params.id);
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ ALTER TABLE "appointment" ADD COLUMN "auth_tag" text;--> statement-breakpoint
|
||||
ALTER TABLE "appointment" ADD COLUMN "created_at" timestamp DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "appointment" ADD COLUMN "updated_at" timestamp DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "appointment_key_share" ADD CONSTRAINT "appointment_key_share_appointment_id_appointment_id_fk" FOREIGN KEY ("appointment_id") REFERENCES "public"."appointment"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "client_tunnel_staff_key_share" ADD CONSTRAINT "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk" FOREIGN KEY ("tunnel_id") REFERENCES "public"."client_appointment_tunnel"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "client_tunnel_staff_key_share" ADD CONSTRAINT "client_tun_staff_keyShare_tId_client_app_tId_fk" FOREIGN KEY ("tunnel_id") REFERENCES "public"."client_appointment_tunnel"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "appointment" ADD CONSTRAINT "appointment_tunnel_id_client_appointment_tunnel_id_fk" FOREIGN KEY ("tunnel_id") REFERENCES "public"."client_appointment_tunnel"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "agent" DROP COLUMN "description";--> statement-breakpoint
|
||||
ALTER TABLE "appointment" DROP COLUMN "client_id";--> statement-breakpoint
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
CREATE TYPE "public"."notification_type" AS ENUM('APPOINTMENT_CONFIRMED', 'APPOINTMENT_CANCELED');--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "type" "notification_type" DEFAULT 'APPOINTMENT_CONFIRMED' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "meta_data" json;--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "created_at" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "notification" DROP COLUMN "title";--> statement-breakpoint
|
||||
ALTER TABLE "notification" DROP COLUMN "description";
|
||||
@@ -620,8 +620,8 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
|
||||
"name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
|
||||
"client_tun_staff_keyShare_tId_client_app_tId_fk": {
|
||||
"name": "client_tun_staff_keyShare_tId_client_app_tId_fk",
|
||||
"tableFrom": "client_tunnel_staff_key_share",
|
||||
"tableTo": "client_appointment_tunnel",
|
||||
"columnsFrom": ["tunnel_id"],
|
||||
|
||||
@@ -635,8 +635,8 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
|
||||
"name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
|
||||
"client_tun_staff_keyShare_tId_client_app_tId_fk": {
|
||||
"name": "client_tun_staff_keyShare_tId_client_app_tId_fk",
|
||||
"tableFrom": "client_tunnel_staff_key_share",
|
||||
"tableTo": "client_appointment_tunnel",
|
||||
"columnsFrom": ["tunnel_id"],
|
||||
|
||||
@@ -649,8 +649,8 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
|
||||
"name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
|
||||
"client_tun_staff_keyShare_tId_client_app_tId_fk": {
|
||||
"name": "client_tun_staff_keyShare_tId_client_app_tId_fk",
|
||||
"tableFrom": "client_tunnel_staff_key_share",
|
||||
"tableTo": "client_appointment_tunnel",
|
||||
"columnsFrom": ["tunnel_id"],
|
||||
|
||||
@@ -655,8 +655,8 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
|
||||
"name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
|
||||
"client_tun_staff_keyShare_tId_client_app_tId_fk": {
|
||||
"name": "client_tun_staff_keyShare_tId_client_app_tId_fk",
|
||||
"tableFrom": "client_tunnel_staff_key_share",
|
||||
"tableTo": "client_appointment_tunnel",
|
||||
"columnsFrom": ["tunnel_id"],
|
||||
|
||||
@@ -599,8 +599,8 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
|
||||
"name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
|
||||
"client_tun_staff_keyShare_tId_client_app_tId_fk": {
|
||||
"name": "client_tun_staff_keyShare_tId_client_app_tId_fk",
|
||||
"tableFrom": "client_tunnel_staff_key_share",
|
||||
"tableTo": "client_appointment_tunnel",
|
||||
"columnsFrom": ["tunnel_id"],
|
||||
|
||||
@@ -634,8 +634,8 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
|
||||
"name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
|
||||
"client_tun_staff_keyShare_tId_client_app_tId_fk": {
|
||||
"name": "client_tun_staff_keyShare_tId_client_app_tId_fk",
|
||||
"tableFrom": "client_tunnel_staff_key_share",
|
||||
"tableTo": "client_appointment_tunnel",
|
||||
"columnsFrom": ["tunnel_id"],
|
||||
|
||||
@@ -634,8 +634,8 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
|
||||
"name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
|
||||
"client_tun_staff_keyShare_tId_client_app_tId_fk": {
|
||||
"name": "client_tun_staff_keyShare_tId_client_app_tId_fk",
|
||||
"tableFrom": "client_tunnel_staff_key_share",
|
||||
"tableTo": "client_appointment_tunnel",
|
||||
"columnsFrom": ["tunnel_id"],
|
||||
|
||||
@@ -693,8 +693,8 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
|
||||
"name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
|
||||
"client_tun_staff_keyShare_tId_client_app_tId_fk": {
|
||||
"name": "client_tun_staff_keyShare_tId_client_app_tId_fk",
|
||||
"tableFrom": "client_tunnel_staff_key_share",
|
||||
"tableTo": "client_appointment_tunnel",
|
||||
"columnsFrom": ["tunnel_id"],
|
||||
|
||||
Reference in New Issue
Block a user