mirror of
https://github.com/nikdoof/pocket-id.git
synced 2025-12-13 23:02:17 +00:00
feat: allow sign in with email (#100)
This commit is contained in:
@@ -2,22 +2,38 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Button } from './ui/button';
|
||||
import * as Card from './ui/card';
|
||||
import WebAuthnUnsupported from './web-authn-unsupported.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let {
|
||||
children
|
||||
children,
|
||||
showEmailOneTimeAccessButton = false
|
||||
}: {
|
||||
children: Snippet;
|
||||
showEmailOneTimeAccessButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="hidden h-screen items-center text-center lg:flex">
|
||||
<div class="min-w-[650px] p-16">
|
||||
<div class="h-full min-w-[650px] p-16 {showEmailOneTimeAccessButton ? 'pb-0' : ''}">
|
||||
{#if browser && !browserSupportsWebAuthn()}
|
||||
<WebAuthnUnsupported />
|
||||
{:else}
|
||||
{@render children()}
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex flex-grow flex-col items-center justify-center">
|
||||
{@render children()}
|
||||
</div>
|
||||
{#if showEmailOneTimeAccessButton}
|
||||
<div class="mb-4 flex justify-center">
|
||||
<Button href="/login/email?redirect={encodeURIComponent($page.url.pathname + $page.url.search)}" variant="link" class="text-muted-foreground text-xs">
|
||||
Don't have access to your passkey?
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<img
|
||||
@@ -27,15 +43,25 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mobile -->
|
||||
<div
|
||||
class="flex h-screen items-center justify-center bg-[url('/api/application-configuration/background-image')] bg-cover bg-center text-center lg:hidden"
|
||||
>
|
||||
<Card.Root class="mx-3">
|
||||
<Card.CardContent class="px-4 py-10 sm:p-10">
|
||||
<Card.CardContent
|
||||
class="px-4 py-10 sm:p-10 {showEmailOneTimeAccessButton ? 'pb-3 sm:pb-3' : ''}"
|
||||
>
|
||||
{#if browser && !browserSupportsWebAuthn()}
|
||||
<WebAuthnUnsupported />
|
||||
{:else}
|
||||
{@render children()}
|
||||
{#if showEmailOneTimeAccessButton}
|
||||
<div class="mt-5">
|
||||
<Button href="/login/email?redirect={encodeURIComponent($page.url.pathname + $page.url.search)}" variant="link" class="text-muted-foreground text-xs">
|
||||
Don't have access to your passkey?
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</Card.CardContent>
|
||||
</Card.Root>
|
||||
|
||||
@@ -11,13 +11,7 @@ export default class AppConfigService extends APIService {
|
||||
}
|
||||
|
||||
const { data } = await this.api.get<AppConfigRawResponse>(url);
|
||||
|
||||
const appConfig: Partial<AllAppConfig> = {};
|
||||
data.forEach(({ key, value }) => {
|
||||
(appConfig as any)[key] = this.parseValue(value);
|
||||
});
|
||||
|
||||
return appConfig as AllAppConfig;
|
||||
return this.parseConfigList(data);
|
||||
}
|
||||
|
||||
async update(appConfig: AllAppConfig) {
|
||||
@@ -27,7 +21,7 @@ export default class AppConfigService extends APIService {
|
||||
(appConfigConvertedToString as any)[key] = (appConfig as any)[key].toString();
|
||||
}
|
||||
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
|
||||
return res.data as AllAppConfig;
|
||||
return this.parseConfigList(res.data);
|
||||
}
|
||||
|
||||
async updateFavicon(favicon: File) {
|
||||
@@ -76,6 +70,15 @@ export default class AppConfigService extends APIService {
|
||||
};
|
||||
}
|
||||
|
||||
private parseConfigList(data: AppConfigRawResponse) {
|
||||
const appConfig: Partial<AllAppConfig> = {};
|
||||
data.forEach(({ key, value }) => {
|
||||
(appConfig as any)[key] = this.parseValue(value);
|
||||
});
|
||||
|
||||
return appConfig as AllAppConfig;
|
||||
}
|
||||
|
||||
private parseValue(value: string) {
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
|
||||
@@ -51,4 +51,8 @@ export default class UserService extends APIService {
|
||||
const res = await this.api.post(`/one-time-access-token/${token}`);
|
||||
return res.data as User;
|
||||
}
|
||||
|
||||
async requestOneTimeAccessEmail(email: string, redirectPath?: string) {
|
||||
await this.api.post('/one-time-access-email', { email, redirectPath });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type AppConfig = {
|
||||
appName: string;
|
||||
allowOwnAccountEdit: boolean;
|
||||
emailOneTimeAccessEnabled: boolean
|
||||
};
|
||||
|
||||
export type AllAppConfig = AppConfig & {
|
||||
@@ -8,7 +9,6 @@ export type AllAppConfig = AppConfig & {
|
||||
sessionDuration: number;
|
||||
emailsVerified: boolean;
|
||||
// Email
|
||||
emailEnabled: boolean;
|
||||
smtpHost: string;
|
||||
smtpPort: number;
|
||||
smtpFrom: string;
|
||||
@@ -16,6 +16,7 @@ export type AllAppConfig = AppConfig & {
|
||||
smtpPassword: string;
|
||||
smtpTls: boolean;
|
||||
smtpSkipCertVerify: boolean;
|
||||
emailLoginNotificationEnabled: boolean;
|
||||
// LDAP
|
||||
ldapEnabled: boolean;
|
||||
ldapUrl: string;
|
||||
|
||||
@@ -2,10 +2,19 @@ import { WebAuthnError } from '@simplewebauthn/browser';
|
||||
import { AxiosError } from 'axios';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export function axiosErrorToast(e: unknown, message: string = 'An unknown error occurred') {
|
||||
export function getAxiosErrorMessage(
|
||||
e: unknown,
|
||||
defaultMessage: string = 'An unknown error occurred'
|
||||
) {
|
||||
let message = defaultMessage;
|
||||
if (e instanceof AxiosError) {
|
||||
message = e.response?.data.error || message;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
export function axiosErrorToast(e: unknown, defaultMessage: string = 'An unknown error occurred') {
|
||||
const message = getAxiosErrorMessage(e, defaultMessage);
|
||||
toast.error(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
{#if client == null}
|
||||
<p>Client not found</p>
|
||||
{:else}
|
||||
<SignInWrapper>
|
||||
<SignInWrapper showEmailOneTimeAccessButton={$appConfigStore.emailOneTimeAccessEnabled}>
|
||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Sign in to {client.name}</h1>
|
||||
{#if errorMessage}
|
||||
@@ -136,7 +136,7 @@
|
||||
</Card.Root>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex justify-center gap-2">
|
||||
<div class="flex w-full justify-stretch gap-2">
|
||||
<Button onclick={() => history.back()} class="w-full" variant="secondary">Cancel</Button>
|
||||
{#if !errorMessage}
|
||||
<Button
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||
import { startAuthentication } from '@simplewebauthn/browser';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoginLogoErrorIndicator from './components/login-logo-error-indicator.svelte';
|
||||
import LoginLogoErrorSuccessIndicator from './components/login-logo-error-success-indicator.svelte';
|
||||
const webauthnService = new WebAuthnService();
|
||||
|
||||
let isLoading = $state(false);
|
||||
@@ -35,9 +35,9 @@
|
||||
<title>Sign In</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
<SignInWrapper showEmailOneTimeAccessButton={$appConfigStore.emailOneTimeAccessEnabled}>
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorIndicator error={!!error} />
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||
Sign in to {$appConfigStore.appName}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
export const load: PageServerLoad = async ({ params, url }) => {
|
||||
return {
|
||||
token: params.token
|
||||
token: params.token,
|
||||
redirect: url.searchParams.get('redirect') || '/settings'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,51 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store.js';
|
||||
import userStore from '$lib/stores/user-store.js';
|
||||
import type { User } from '$lib/types/user.type.js';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { getAxiosErrorMessage } from '$lib/utils/error-util';
|
||||
import { onMount } from 'svelte';
|
||||
import LoginLogoErrorSuccessIndicator from '../components/login-logo-error-success-indicator.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let isLoading = $state(false);
|
||||
let error: string | undefined = $state();
|
||||
const skipPage = data.redirect !== '/settings';
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
async function authenticate() {
|
||||
isLoading = true;
|
||||
userService
|
||||
.exchangeOneTimeAccessToken(data.token)
|
||||
.then((user: User) => {
|
||||
userStore.setUser(user);
|
||||
goto('/settings');
|
||||
})
|
||||
.catch(axiosErrorToast);
|
||||
try {
|
||||
const user = await userService.exchangeOneTimeAccessToken(data.token);
|
||||
userStore.setUser(user);
|
||||
|
||||
try {
|
||||
goto(data.redirect);
|
||||
} catch (e) {
|
||||
error = 'Invalid redirect URL';
|
||||
}
|
||||
} catch (e) {
|
||||
error = getAxiosErrorMessage(e);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (skipPage) {
|
||||
authenticate();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<SignInWrapper>
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-muted rounded-2xl p-3">
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">
|
||||
{data.token === 'setup' ? `${$appConfigStore.appName} Setup` : 'One Time Access'}
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
{#if data.token === 'setup'}
|
||||
You're about to sign in to the initial admin account. Anyone with this link can access the
|
||||
account until a passkey is added. Please set up a passkey as soon as possible to prevent
|
||||
unauthorized access.
|
||||
{:else}
|
||||
You've been granted one-time access to your {$appConfigStore.appName} account. Please note that
|
||||
if you continue, this link will become invalid. To avoid this, make sure to add a passkey. Otherwise,
|
||||
you'll need to request a new link.
|
||||
{/if}
|
||||
</p>
|
||||
<Button class="mt-5" {isLoading} on:click={authenticate}>Continue</Button>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
{error}. Please try again.
|
||||
</p>
|
||||
{:else if !skipPage}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
{#if data.token === 'setup'}
|
||||
You're about to sign in to the initial admin account. Anyone with this link can access the
|
||||
account until a passkey is added. Please set up a passkey as soon as possible to prevent
|
||||
unauthorized access.
|
||||
{:else}
|
||||
You've been granted one-time access to your {$appConfigStore.appName} account. Please note that
|
||||
if you continue, this link will become invalid. To avoid this, make sure to add a passkey. Otherwise,
|
||||
you'll need to request a new link.
|
||||
{/if}
|
||||
</p>
|
||||
<Button class="mt-5" {isLoading} on:click={authenticate}>Continue</Button>
|
||||
{/if}
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import CheckmarkAnimated from '$lib/icons/checkmark-animated.svelte';
|
||||
import CrossAnimated from '$lib/icons/cross-animated.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const {
|
||||
error
|
||||
error,
|
||||
success
|
||||
}: {
|
||||
error: boolean;
|
||||
error?: boolean;
|
||||
success?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="rounded-2xl p-3 transition-[background-color] duration-300
|
||||
{error ? 'bg-red-200' : 'bg-muted'}"
|
||||
{error ? 'bg-red-200' : success ? 'bg-green-200' : 'bg-muted'}"
|
||||
>
|
||||
{#if error}
|
||||
{#if error || success}
|
||||
<div class="flex h-10 w-10 items-center justify-center">
|
||||
<CrossAnimated class="h-5 w-5" />
|
||||
{#if error}
|
||||
<CrossAnimated class="h-5 w-5" />
|
||||
{:else}
|
||||
<CheckmarkAnimated class="h-5 w-5" />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div in:fade={{ duration: 300 }}>
|
||||
7
frontend/src/routes/login/email/+page.server.ts
Normal file
7
frontend/src/routes/login/email/+page.server.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
return {
|
||||
redirect: url.searchParams.get('redirect') || undefined
|
||||
};
|
||||
};
|
||||
62
frontend/src/routes/login/email/+page.svelte
Normal file
62
frontend/src/routes/login/email/+page.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoginLogoErrorSuccessIndicator from '../components/login-logo-error-success-indicator.svelte';
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
let email = $state('');
|
||||
let isLoading = $state(false);
|
||||
let error: string | undefined = $state(undefined);
|
||||
let success = $state(false);
|
||||
|
||||
async function requestEmail() {
|
||||
isLoading = true;
|
||||
await userService
|
||||
.requestOneTimeAccessEmail(email, data.redirect)
|
||||
.then(() => (success = true))
|
||||
.catch((e) => (error = e.response?.data.error || 'An unknown error occured'));
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Email One Time Access</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorSuccessIndicator {success} error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Email One Time Access</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
{error}. Please try again.
|
||||
</p>
|
||||
<div class="mt-10 flex w-full justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href="/">Go back</Button>
|
||||
<Button class="w-full" onclick={() => (error = undefined)}>Try again</Button>
|
||||
</div>
|
||||
{:else if success}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
An email has been sent to the provided email, if it exists in the system.
|
||||
</p>
|
||||
{:else}
|
||||
<form onsubmit={requestEmail}>
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
Enter your email to receive an email with a one time access link.
|
||||
</p>
|
||||
<Input id="Email" class="mt-7" placeholder="Your email" bind:value={email} />
|
||||
<div class="mt-8 flex justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href="/">Go back</Button>
|
||||
<Button class="w-full" type="submit" {isLoading}>Submit</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</SignInWrapper>
|
||||
@@ -16,7 +16,7 @@
|
||||
const appConfigService = new AppConfigService();
|
||||
|
||||
async function updateAppConfig(updatedAppConfig: Partial<AllAppConfig>) {
|
||||
await appConfigService
|
||||
appConfig = await appConfigService
|
||||
.update({
|
||||
...appConfig,
|
||||
...updatedAppConfig
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
const appConfigService = new AppConfigService();
|
||||
|
||||
let isSendingTestEmail = $state(false);
|
||||
let emailEnabled = $state(appConfig.emailEnabled);
|
||||
|
||||
const updatedAppConfig = {
|
||||
emailEnabled: appConfig.emailEnabled,
|
||||
smtpHost: appConfig.smtpHost,
|
||||
smtpPort: appConfig.smtpPort,
|
||||
smtpUser: appConfig.smtpUser,
|
||||
smtpPassword: appConfig.smtpPassword,
|
||||
smtpFrom: appConfig.smtpFrom,
|
||||
smtpTls: appConfig.smtpTls,
|
||||
smtpSkipCertVerify: appConfig.smtpSkipCertVerify
|
||||
smtpSkipCertVerify: appConfig.smtpSkipCertVerify,
|
||||
emailOneTimeAccessEnabled: appConfig.emailOneTimeAccessEnabled,
|
||||
emailLoginNotificationEnabled: appConfig.emailLoginNotificationEnabled
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -39,7 +39,9 @@
|
||||
smtpPassword: z.string(),
|
||||
smtpFrom: z.string().email(),
|
||||
smtpTls: z.boolean(),
|
||||
smtpSkipCertVerify: z.boolean()
|
||||
smtpSkipCertVerify: z.boolean(),
|
||||
emailOneTimeAccessEnabled: z.boolean(),
|
||||
emailLoginNotificationEnabled: z.boolean()
|
||||
});
|
||||
|
||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
||||
@@ -47,26 +49,11 @@
|
||||
async function onSubmit() {
|
||||
const data = form.validate();
|
||||
if (!data) return false;
|
||||
await callback({
|
||||
...data,
|
||||
emailEnabled: true
|
||||
});
|
||||
await callback(data);
|
||||
toast.success('Email configuration updated successfully');
|
||||
return true;
|
||||
}
|
||||
|
||||
async function onDisable() {
|
||||
emailEnabled = false;
|
||||
await callback({ emailEnabled });
|
||||
toast.success('Email disabled successfully');
|
||||
}
|
||||
|
||||
async function onEnable() {
|
||||
if (await onSubmit()) {
|
||||
emailEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function onTestEmail() {
|
||||
isSendingTestEmail = true;
|
||||
await appConfigService
|
||||
@@ -80,7 +67,8 @@
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="mt-5 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||
<h4 class="text-lg font-semibold">SMTP Configuration</h4>
|
||||
<div class="mt-4 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||
<FormInput label="SMTP Host" bind:input={$inputs.smtpHost} />
|
||||
<FormInput label="SMTP Port" type="number" bind:input={$inputs.smtpPort} />
|
||||
<FormInput label="SMTP User" bind:input={$inputs.smtpUser} />
|
||||
@@ -99,15 +87,26 @@
|
||||
bind:checked={$inputs.smtpSkipCertVerify.value}
|
||||
/>
|
||||
</div>
|
||||
<h4 class="mt-10 text-lg font-semibold">Enabled Emails</h4>
|
||||
<div class="mt-4 flex flex-col gap-3">
|
||||
<CheckboxWithLabel
|
||||
id="email-login-notification"
|
||||
label="Email Login Notification"
|
||||
description="Send an email to the user when they log in from a new device."
|
||||
bind:checked={$inputs.emailLoginNotificationEnabled.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="email-one-time-access"
|
||||
label="Email One Time Access"
|
||||
description="Allows users to sign in with a link sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry."
|
||||
bind:checked={$inputs.emailOneTimeAccessEnabled.value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-wrap justify-end gap-3">
|
||||
{#if emailEnabled}
|
||||
<Button variant="secondary" onclick={onDisable}>Disable</Button>
|
||||
<Button isLoading={isSendingTestEmail} variant="secondary" onclick={onTestEmail}
|
||||
>Send Test Email</Button
|
||||
>
|
||||
<Button type="submit">Save</Button>
|
||||
{:else}
|
||||
<Button onclick={onEnable}>Enable</Button>
|
||||
{/if}
|
||||
<Button isLoading={isSendingTestEmail} variant="secondary" onclick={onTestEmail}
|
||||
>Send Test Email</Button
|
||||
>
|
||||
<Button type="submit">Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
||||
import FormInput from '$lib/components/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -23,14 +21,14 @@
|
||||
appName: appConfig.appName,
|
||||
sessionDuration: appConfig.sessionDuration,
|
||||
emailsVerified: appConfig.emailsVerified,
|
||||
allowOwnAccountEdit: appConfig.allowOwnAccountEdit
|
||||
allowOwnAccountEdit: appConfig.allowOwnAccountEdit,
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
appName: z.string().min(2).max(30),
|
||||
sessionDuration: z.number().min(1).max(43200),
|
||||
emailsVerified: z.boolean(),
|
||||
allowOwnAccountEdit: z.boolean()
|
||||
allowOwnAccountEdit: z.boolean(),
|
||||
});
|
||||
|
||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
||||
@@ -63,7 +61,7 @@
|
||||
label="Emails Verified"
|
||||
description="Whether the user's email should be marked as verified for the OIDC clients."
|
||||
bind:checked={$inputs.emailsVerified.value}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
label="Public Client"
|
||||
description="Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app."
|
||||
onCheckedChange={(v) => {
|
||||
console.log(v)
|
||||
if (v == true) form.setValue('pkceEnabled', true);
|
||||
}}
|
||||
bind:checked={$inputs.isPublic.value}
|
||||
|
||||
@@ -29,11 +29,12 @@ test('Update email configuration', async ({ page }) => {
|
||||
await page.getByLabel('SMTP User').fill('test@gmail.com');
|
||||
await page.getByLabel('SMTP Password').fill('password');
|
||||
await page.getByLabel('SMTP From').fill('test@gmail.com');
|
||||
await page.getByRole('button', { name: 'Enable' }).nth(0).click();
|
||||
await page.getByRole('status').click();
|
||||
await page.getByLabel('Email Login Notification').click();
|
||||
await page.getByLabel('Email One Time Access').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
|
||||
await expect(page.getByRole('status')).toHaveText('Email configuration updated successfully');
|
||||
await expect(page.getByRole('button', { name: 'Disable' })).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -42,10 +43,8 @@ test('Update email configuration', async ({ page }) => {
|
||||
await expect(page.getByLabel('SMTP User')).toHaveValue('test@gmail.com');
|
||||
await expect(page.getByLabel('SMTP Password')).toHaveValue('password');
|
||||
await expect(page.getByLabel('SMTP From')).toHaveValue('test@gmail.com');
|
||||
|
||||
await page.getByRole('button', { name: 'Disable' }).click();
|
||||
|
||||
await expect(page.getByRole('status')).toHaveText('Email disabled successfully');
|
||||
await expect(page.getByLabel('Email Login Notification')).toBeChecked();
|
||||
await expect(page.getByLabel('Email One Time Access')).toBeChecked();
|
||||
});
|
||||
|
||||
test('Update application images', async ({ page }) => {
|
||||
@@ -55,7 +54,7 @@ test('Update application images', async ({ page }) => {
|
||||
await page.getByLabel('Light Mode Logo').setInputFiles('tests/assets/pingvin-share-logo.png');
|
||||
await page.getByLabel('Dark Mode Logo').setInputFiles('tests/assets/nextcloud-logo.png');
|
||||
await page.getByLabel('Background Image').setInputFiles('tests/assets/clouds.jpg');
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Save' }).nth(2).click();
|
||||
|
||||
await expect(page.getByRole('status')).toHaveText('Images updated successfully');
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ test('Sign in with expired one time access token fails', async ({ page }) => {
|
||||
await page.goto(`/login/${token.token}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('status')).toHaveText('Token is invalid or expired');
|
||||
await expect(page.getByRole('paragraph')).toHaveText('Token is invalid or expired. Please try again.');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user