feat: allow sign in with email (#100)

This commit is contained in:
Elias Schneider
2025-01-19 15:30:31 +01:00
committed by GitHub
parent e284e352e2
commit 06b90eddd6
42 changed files with 422 additions and 145 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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 });
}
}

View File

@@ -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;

View File

@@ -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);
}