@vonall/auth 0.2.0 → 0.3.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/CHANGELOG.md +12 -0
- package/README.md +24 -1
- package/dist/components/ui/avatar/avatar-fallback.svelte +17 -0
- package/dist/components/ui/avatar/avatar-fallback.svelte.d.ts +4 -0
- package/dist/components/ui/avatar/avatar-image.svelte +17 -0
- package/dist/components/ui/avatar/avatar-image.svelte.d.ts +4 -0
- package/dist/components/ui/avatar/avatar.svelte +17 -0
- package/dist/components/ui/avatar/avatar.svelte.d.ts +4 -0
- package/dist/components/ui/avatar/index.d.ts +4 -0
- package/dist/components/ui/avatar/index.js +6 -0
- package/dist/components/ui/button/button.svelte +82 -0
- package/dist/components/ui/button/button.svelte.d.ts +70 -0
- package/dist/components/ui/button/index.d.ts +2 -0
- package/dist/components/ui/button/index.js +2 -0
- package/dist/components/ui/card/card-action.svelte +20 -0
- package/dist/components/ui/card/card-action.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-content.svelte +15 -0
- package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-description.svelte +20 -0
- package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-footer.svelte +20 -0
- package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-header.svelte +23 -0
- package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-title.svelte +20 -0
- package/dist/components/ui/card/card-title.svelte.d.ts +5 -0
- package/dist/components/ui/card/card.svelte +23 -0
- package/dist/components/ui/card/card.svelte.d.ts +5 -0
- package/dist/components/ui/card/index.d.ts +8 -0
- package/dist/components/ui/card/index.js +10 -0
- package/dist/components/ui/input/index.d.ts +2 -0
- package/dist/components/ui/input/index.js +4 -0
- package/dist/components/ui/input/input.svelte +48 -0
- package/dist/components/ui/input/input.svelte.d.ts +13 -0
- package/dist/components/ui/label/index.d.ts +2 -0
- package/dist/components/ui/label/index.js +4 -0
- package/dist/components/ui/label/label.svelte +20 -0
- package/dist/components/ui/label/label.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/index.d.ts +5 -0
- package/dist/components/ui/tabs/index.js +7 -0
- package/dist/components/ui/tabs/tabs-content.svelte +17 -0
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-list.svelte +20 -0
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte +20 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs.svelte +19 -0
- package/dist/components/ui/tabs/tabs.svelte.d.ts +4 -0
- package/dist/messages.d.ts +196 -0
- package/dist/messages.js +404 -0
- package/dist/ui/AdminView.svelte +91 -0
- package/dist/ui/AdminView.svelte.d.ts +6 -0
- package/dist/ui/ChangePasswordCard.svelte +62 -0
- package/dist/ui/ChangePasswordCard.svelte.d.ts +3 -0
- package/dist/ui/DeletionTab.svelte +76 -0
- package/dist/ui/DeletionTab.svelte.d.ts +6 -0
- package/dist/ui/EinladungErstellen.svelte +74 -0
- package/dist/ui/EinladungErstellen.svelte.d.ts +3 -0
- package/dist/ui/EinladungPage.svelte +42 -0
- package/dist/ui/EinladungPage.svelte.d.ts +13 -0
- package/dist/ui/IconGithub.svelte +23 -0
- package/dist/ui/IconGithub.svelte.d.ts +4 -0
- package/dist/ui/IconGoogle.svelte +16 -0
- package/dist/ui/IconGoogle.svelte.d.ts +4 -0
- package/dist/ui/InvitationsTab.svelte +46 -0
- package/dist/ui/InvitationsTab.svelte.d.ts +3 -0
- package/dist/ui/LinksTab.svelte +91 -0
- package/dist/ui/LinksTab.svelte.d.ts +3 -0
- package/dist/ui/LoginForm.svelte +220 -0
- package/dist/ui/LoginForm.svelte.d.ts +8 -0
- package/dist/ui/MobileSettingsMenu.svelte +37 -0
- package/dist/ui/MobileSettingsMenu.svelte.d.ts +11 -0
- package/dist/ui/ProfileManagementTab.svelte +56 -0
- package/dist/ui/ProfileManagementTab.svelte.d.ts +7 -0
- package/dist/ui/RegisterForm.svelte +143 -0
- package/dist/ui/RegisterForm.svelte.d.ts +6 -0
- package/dist/ui/ResetPasswordForm.svelte +91 -0
- package/dist/ui/ResetPasswordForm.svelte.d.ts +3 -0
- package/dist/ui/SecurityTab.svelte +25 -0
- package/dist/ui/SecurityTab.svelte.d.ts +3 -0
- package/dist/ui/SessionsTab.svelte +98 -0
- package/dist/ui/SessionsTab.svelte.d.ts +3 -0
- package/dist/ui/SetPasswordCard.svelte +56 -0
- package/dist/ui/SetPasswordCard.svelte.d.ts +6 -0
- package/dist/ui/SettingsHeader.svelte +19 -0
- package/dist/ui/SettingsHeader.svelte.d.ts +7 -0
- package/dist/ui/SettingsPage.svelte +96 -0
- package/dist/ui/SettingsPage.svelte.d.ts +9 -0
- package/dist/ui/SettingsTabsList.svelte +17 -0
- package/dist/ui/SettingsTabsList.svelte.d.ts +7 -0
- package/dist/ui/SocialButton.svelte +21 -0
- package/dist/ui/SocialButton.svelte.d.ts +7 -0
- package/dist/ui/TwoFactorAuthenticationCard.svelte +216 -0
- package/dist/ui/TwoFactorAuthenticationCard.svelte.d.ts +3 -0
- package/dist/ui/Verify2FAForm.svelte +63 -0
- package/dist/ui/Verify2FAForm.svelte.d.ts +11 -0
- package/dist/ui/context.d.ts +93 -0
- package/dist/ui/context.js +17 -0
- package/dist/ui/href.d.ts +2 -0
- package/dist/ui/href.js +5 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +11 -0
- package/dist/ui/tabs.d.ts +17 -0
- package/dist/ui/tabs.js +21 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +6 -0
- package/package.json +15 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Copy, Share2 } from "@lucide/svelte";
|
|
3
|
+
import { toast } from "svelte-sonner";
|
|
4
|
+
import { Button } from "../components/ui/button/index.js";
|
|
5
|
+
import { Input } from "../components/ui/input/index.js";
|
|
6
|
+
import { m } from "../messages.js";
|
|
7
|
+
import { createEinladung, getOffeneEinladungen } from "../remote/einladung.remote.js";
|
|
8
|
+
import { authUi } from "./context.js";
|
|
9
|
+
import { messageOf } from "../util/messageOf.js";
|
|
10
|
+
|
|
11
|
+
/** Creates an Einladung (a link, optionally sent by email) and shows the link once; in the settings and for the admin. */
|
|
12
|
+
const { appName } = authUi();
|
|
13
|
+
const mail = $derived((await getOffeneEinladungen()).mail);
|
|
14
|
+
|
|
15
|
+
let email = $state("");
|
|
16
|
+
let busy = $state(false);
|
|
17
|
+
let link = $state<string | null>(null);
|
|
18
|
+
|
|
19
|
+
const text = $derived(m().invitationText(appName, link ?? ""));
|
|
20
|
+
|
|
21
|
+
async function create(event: SubmitEvent) {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
busy = true;
|
|
24
|
+
try {
|
|
25
|
+
const result = await createEinladung({ email: email.trim() || null });
|
|
26
|
+
link = result.link;
|
|
27
|
+
if (result.mailFehler) toast.error(m().shareYourself(result.mailFehler));
|
|
28
|
+
else if (email.trim()) toast.success(m().invitationSentTo(email.trim()));
|
|
29
|
+
email = "";
|
|
30
|
+
} catch (e) {
|
|
31
|
+
toast.error(messageOf(e, m().invitationNotCreated));
|
|
32
|
+
} finally {
|
|
33
|
+
busy = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function copy() {
|
|
38
|
+
if (!link) return;
|
|
39
|
+
try {
|
|
40
|
+
await navigator.clipboard.writeText(text);
|
|
41
|
+
toast.success(m().linkCopied);
|
|
42
|
+
} catch {
|
|
43
|
+
toast.error(m().copyLinkFailed);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function share() {
|
|
48
|
+
try {
|
|
49
|
+
await navigator.share({ title: appName, text });
|
|
50
|
+
} catch {
|
|
51
|
+
// cancelled or not supported: the copy button remains
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<form onsubmit={create} class="flex flex-wrap gap-2">
|
|
57
|
+
{#if mail}
|
|
58
|
+
<Input type="email" bind:value={email} placeholder={m().emailOptional} autocomplete="off" class="min-w-56 flex-1" />
|
|
59
|
+
{/if}
|
|
60
|
+
<Button type="submit" disabled={busy} class="cursor-pointer">{busy ? m().creating : m().createInvitation}</Button>
|
|
61
|
+
</form>
|
|
62
|
+
|
|
63
|
+
{#if link}
|
|
64
|
+
<div class="grid gap-2 rounded-lg bg-muted p-3">
|
|
65
|
+
<code class="text-xs break-all">{link}</code>
|
|
66
|
+
<div class="flex gap-2">
|
|
67
|
+
<Button size="sm" variant="secondary" onclick={copy} class="cursor-pointer"><Copy size={14} /> {m().copy}</Button>
|
|
68
|
+
{#if typeof navigator !== "undefined" && "share" in navigator}
|
|
69
|
+
<Button size="sm" variant="secondary" onclick={share} class="cursor-pointer"><Share2 size={14} /> {m().share}</Button>
|
|
70
|
+
{/if}
|
|
71
|
+
</div>
|
|
72
|
+
<p class="text-xs text-muted-foreground">{m().linkShownOnce}</p>
|
|
73
|
+
</div>
|
|
74
|
+
{/if}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from "$app/state";
|
|
3
|
+
import { Button } from "../components/ui/button/index.js";
|
|
4
|
+
import * as Card from "../components/ui/card/index.js";
|
|
5
|
+
import { m } from "../messages.js";
|
|
6
|
+
import { authUi } from "./context.js";
|
|
7
|
+
import { href } from "./href.js";
|
|
8
|
+
|
|
9
|
+
/** The page an Einladung link opens (`einladungLoad` gives `data`); `tagline` is the Product's one sentence about itself. */
|
|
10
|
+
let { data, tagline }: { data: { fehler: string | null; einladung: { inviterName: string } | null; eingeloggt: boolean }; tagline: string } = $props();
|
|
11
|
+
const { appName } = authUi();
|
|
12
|
+
|
|
13
|
+
const loginLink = $derived(`${href("/auth/login")}?redirectTo=${encodeURIComponent(page.url.pathname)}`);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<svelte:head><title>{m().invitationTitle} · {appName}</title></svelte:head>
|
|
17
|
+
|
|
18
|
+
<div class="flex min-h-svh flex-col items-center justify-center bg-muted p-6">
|
|
19
|
+
<Card.Root class="w-full max-w-sm">
|
|
20
|
+
<Card.Header class="text-center">
|
|
21
|
+
<Card.Title class="text-xl">{m().invitationTo(appName)}</Card.Title>
|
|
22
|
+
{#if data.einladung}
|
|
23
|
+
<Card.Description>{m().invitesYou(data.einladung.inviterName)}</Card.Description>
|
|
24
|
+
{/if}
|
|
25
|
+
</Card.Header>
|
|
26
|
+
<Card.Content class="grid gap-4 text-sm">
|
|
27
|
+
{#if data.fehler}
|
|
28
|
+
<p class="text-center">{data.fehler}. {m().askForNewLink}</p>
|
|
29
|
+
<Button href={href("/")} variant="outline">{m().toStartPage}</Button>
|
|
30
|
+
{:else if data.eingeloggt}
|
|
31
|
+
<p class="text-center">{m().alreadyHaveAccountNoInvite}</p>
|
|
32
|
+
<Button href={href("/home")}>{m().openApp(appName)}</Button>
|
|
33
|
+
{:else}
|
|
34
|
+
<p>{tagline}</p>
|
|
35
|
+
<div class="grid gap-2">
|
|
36
|
+
<Button href={href("/auth/signup")}>{m().createAccount}</Button>
|
|
37
|
+
<Button href={loginLink} variant="outline">{m().iHaveAnAccount}</Button>
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
</Card.Content>
|
|
41
|
+
</Card.Root>
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
data: {
|
|
3
|
+
fehler: string | null;
|
|
4
|
+
einladung: {
|
|
5
|
+
inviterName: string;
|
|
6
|
+
} | null;
|
|
7
|
+
eingeloggt: boolean;
|
|
8
|
+
};
|
|
9
|
+
tagline: string;
|
|
10
|
+
};
|
|
11
|
+
declare const EinladungPage: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type EinladungPage = ReturnType<typeof EinladungPage>;
|
|
13
|
+
export default EinladungPage;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SVGAttributes } from "svelte/elements";
|
|
3
|
+
let { class: className = "size-5 mr-2", ...restProps }: SVGAttributes<SVGElement> = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<svg
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
viewBox="0 0 24 24"
|
|
9
|
+
class={className}
|
|
10
|
+
fill="currentColor"
|
|
11
|
+
{...restProps}
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
d="M12 0.297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.387.6.113.82-.258.82-.577
|
|
15
|
+
0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.745.083-.729.083-.729
|
|
16
|
+
1.205.084 1.84 1.236 1.84 1.236 1.07 1.835 2.807 1.305 3.495.997.108-.776.418-1.305.76-1.605-2.665-.3-5.467-1.334-5.467-5.93
|
|
17
|
+
0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.522.105-3.176 0 0 1.005-.322 3.3 1.23a11.5 11.5 0 0 1 3-.405c1.02.005
|
|
18
|
+
2.045.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.654.24 2.873.12 3.176.765.84
|
|
19
|
+
1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.429.372.81 1.103.81 2.222
|
|
20
|
+
0 1.606-.015 2.896-.015 3.286 0 .315.21.694.825.576C20.565 22.092
|
|
21
|
+
24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SVGAttributes } from "svelte/elements";
|
|
3
|
+
let { class: className = "size-5 mr-2", ...restProps }: SVGAttributes<SVGElement> = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<svg
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
viewBox="0 0 24 24"
|
|
9
|
+
class={className}
|
|
10
|
+
{...restProps}
|
|
11
|
+
>
|
|
12
|
+
<path
|
|
13
|
+
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
|
14
|
+
fill="currentColor"
|
|
15
|
+
/>
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X } from "@lucide/svelte";
|
|
3
|
+
import { toast } from "svelte-sonner";
|
|
4
|
+
import * as Tabs from "../components/ui/tabs/index.js";
|
|
5
|
+
import { m } from "../messages.js";
|
|
6
|
+
import { getOffeneEinladungen, withdrawEinladung } from "../remote/einladung.remote.js";
|
|
7
|
+
import { authUi } from "./context.js";
|
|
8
|
+
import EinladungErstellen from "./EinladungErstellen.svelte";
|
|
9
|
+
import { messageOf } from "../util/messageOf.js";
|
|
10
|
+
|
|
11
|
+
/** Signup is invite only: here a user creates Einladungen and sees the open ones. */
|
|
12
|
+
const { locale } = authUi();
|
|
13
|
+
const offen = $derived(await getOffeneEinladungen());
|
|
14
|
+
const datum = new Intl.DateTimeFormat(locale, { day: "numeric", month: "long" });
|
|
15
|
+
|
|
16
|
+
async function withdraw(einladungId: string) {
|
|
17
|
+
try {
|
|
18
|
+
await withdrawEinladung({ einladungId });
|
|
19
|
+
toast.success(m().invitationWithdrawn);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
toast.error(messageOf(e, m().invitationNotWithdrawn));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<Tabs.Content value="invitations" class="panel mt-6 grid gap-4 p-6 md:p-8">
|
|
27
|
+
<div class="space-y-1">
|
|
28
|
+
<h3 class="text-lg font-medium">{m().inviteSomeone}</h3>
|
|
29
|
+
<p class="text-sm text-muted-foreground">{m().inviteIntro}</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<EinladungErstellen />
|
|
33
|
+
|
|
34
|
+
{#if offen.einladungen.length > 0}
|
|
35
|
+
<ul class="grid gap-2 text-sm">
|
|
36
|
+
{#each offen.einladungen as e (e.id)}
|
|
37
|
+
<li class="flex items-center justify-between gap-2">
|
|
38
|
+
<span class="text-muted-foreground">{e.email ?? m().linkWord} · {m().validUntil(datum.format(new Date(e.expiresAt)))}</span>
|
|
39
|
+
<button type="button" class="text-muted-foreground hover:text-destructive" aria-label={m().withdrawInvitation} onclick={() => withdraw(e.id)}>
|
|
40
|
+
<X size={14} />
|
|
41
|
+
</button>
|
|
42
|
+
</li>
|
|
43
|
+
{/each}
|
|
44
|
+
</ul>
|
|
45
|
+
{/if}
|
|
46
|
+
</Tabs.Content>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Link2, Unlink } from "@lucide/svelte";
|
|
3
|
+
import { goto } from "$app/navigation";
|
|
4
|
+
import { page } from "$app/state";
|
|
5
|
+
import { toast } from "svelte-sonner";
|
|
6
|
+
import { Button } from "../components/ui/button/index.js";
|
|
7
|
+
import * as Tabs from "../components/ui/tabs/index.js";
|
|
8
|
+
import { m } from "../messages.js";
|
|
9
|
+
import { getLinkedAccounts } from "../remote/app_user.remote.js";
|
|
10
|
+
import { authUi } from "./context.js";
|
|
11
|
+
import IconGithub from "./IconGithub.svelte";
|
|
12
|
+
import IconGoogle from "./IconGoogle.svelte";
|
|
13
|
+
import { isSessionNotFresh, reauthPath } from "../util/fresh.js";
|
|
14
|
+
|
|
15
|
+
const { authClient } = authUi();
|
|
16
|
+
const linkedAccounts = $derived(getLinkedAccounts());
|
|
17
|
+
const providersData = $derived(page.data.providers as { github?: boolean; google?: boolean } | undefined);
|
|
18
|
+
|
|
19
|
+
const providers = $derived([
|
|
20
|
+
...(providersData?.github ? [{ id: "github", name: "GitHub", component: IconGithub }] : []),
|
|
21
|
+
...(providersData?.google ? [{ id: "google", name: "Google", component: IconGoogle }] : []),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
async function link(provider: string) {
|
|
25
|
+
const res = await authClient.linkSocial({ provider, callbackURL: window.location.href });
|
|
26
|
+
if (res?.error) toast.error(res.error.message || m().providerNotLinked(provider));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function unlink(provider: string) {
|
|
30
|
+
const accountId = linkedAccounts.current?.find((acc) => acc.providerId === provider)?.id;
|
|
31
|
+
if (!accountId) return;
|
|
32
|
+
const res = await authClient.unlinkAccount({ accountId, providerId: provider });
|
|
33
|
+
|
|
34
|
+
if (isSessionNotFresh(res?.error)) {
|
|
35
|
+
// better-auth lets only a login from the last day unlink an account
|
|
36
|
+
await goto(reauthPath(`${page.url.pathname}?tab=links`));
|
|
37
|
+
} else if (res?.error) {
|
|
38
|
+
toast.error(res.error.message || m().providerNotUnlinked(provider));
|
|
39
|
+
} else {
|
|
40
|
+
toast.success(m().providerUnlinked(provider));
|
|
41
|
+
linkedAccounts.refresh();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isLinked(providerId: string) {
|
|
46
|
+
return linkedAccounts.current?.some((acc) => acc.providerId === providerId);
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<Tabs.Content value="links" class="panel mt-6 p-6 md:p-8">
|
|
51
|
+
<div class="space-y-6">
|
|
52
|
+
<div class="space-y-1">
|
|
53
|
+
<h3 class="text-lg font-medium">{m().linkedAccounts}</h3>
|
|
54
|
+
<p class="text-sm text-muted-foreground">{m().linkedIntro}</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="grid gap-4">
|
|
58
|
+
{#each providers as provider (provider.id)}
|
|
59
|
+
{@const linked = isLinked(provider.id)}
|
|
60
|
+
<div class="flex items-center justify-between rounded-lg border bg-muted/30 p-4">
|
|
61
|
+
<div class="flex items-center gap-4">
|
|
62
|
+
<div class="flex items-center justify-center rounded-md border bg-background p-2">
|
|
63
|
+
<provider.component class="size-5" />
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<p class="font-medium">{provider.name}</p>
|
|
67
|
+
<p class="text-xs text-muted-foreground">{linked ? m().linked : m().notLinked}</p>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{#if linked}
|
|
72
|
+
<Button variant="outline" size="sm" class="cursor-pointer text-destructive hover:text-destructive" onclick={() => unlink(provider.id)}>
|
|
73
|
+
<Unlink class="mr-2 size-4" />
|
|
74
|
+
{m().unlink}
|
|
75
|
+
</Button>
|
|
76
|
+
{:else}
|
|
77
|
+
<Button class="cursor-pointer" variant="outline" size="sm" onclick={() => link(provider.id)}>
|
|
78
|
+
<Link2 class="mr-2 size-4" />
|
|
79
|
+
{m().link}
|
|
80
|
+
</Button>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
{/each}
|
|
84
|
+
|
|
85
|
+
{#if providers.length === 0}
|
|
86
|
+
<p class="text-sm text-muted-foreground">{m().noSocialLogin}</p>
|
|
87
|
+
{/if}
|
|
88
|
+
<p class="text-xs text-muted-foreground">{m().autoLinkNote}</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</Tabs.Content>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { LoaderCircle } from "@lucide/svelte";
|
|
4
|
+
import { goto } from "$app/navigation";
|
|
5
|
+
import { page } from "$app/state";
|
|
6
|
+
import { toast } from "svelte-sonner";
|
|
7
|
+
import { Button } from "../components/ui/button/index.js";
|
|
8
|
+
import * as Card from "../components/ui/card/index.js";
|
|
9
|
+
import { Input } from "../components/ui/input/index.js";
|
|
10
|
+
import { Label } from "../components/ui/label/index.js";
|
|
11
|
+
import { m } from "../messages.js";
|
|
12
|
+
import { authUi } from "./context.js";
|
|
13
|
+
import { href } from "./href.js";
|
|
14
|
+
import SocialButton from "./SocialButton.svelte";
|
|
15
|
+
import Verify2FAForm from "./Verify2FAForm.svelte";
|
|
16
|
+
import { cn } from "../utils.js";
|
|
17
|
+
import { messageOf } from "../util/messageOf.js";
|
|
18
|
+
import { sameSitePath } from "../util/sameSite.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The login page: email and password (with two-factor and the forgotten password), the social logins that have keys
|
|
22
|
+
* (`authLayoutLoad`). `footer` is the Product's line under the card (e.g. its privacy policy).
|
|
23
|
+
*/
|
|
24
|
+
const { class: className, footer, ...restProps }: { class?: string; footer?: Snippet } = $props();
|
|
25
|
+
const { authClient, appName, signupMode } = authUi();
|
|
26
|
+
|
|
27
|
+
let email = $state("");
|
|
28
|
+
let password = $state("");
|
|
29
|
+
let loading = $state(false);
|
|
30
|
+
let errorMessage = $state("");
|
|
31
|
+
|
|
32
|
+
let show2FAInput = $state(false);
|
|
33
|
+
let showForgotPassword = $state(false);
|
|
34
|
+
let forgotPasswordEmail = $state("");
|
|
35
|
+
let sendingResetLink = $state(false);
|
|
36
|
+
|
|
37
|
+
const providers = $derived(page.data.providers as { github?: boolean; google?: boolean } | undefined);
|
|
38
|
+
const mail = $derived(page.data.mail as boolean);
|
|
39
|
+
|
|
40
|
+
const urlError = $derived(page.url.searchParams.get("error"));
|
|
41
|
+
// A new account through GitHub or Google was refused: why depends on the signup mode
|
|
42
|
+
const noNewAccount = $derived(signupMode === "open" ? m().noNewAccountOpen : m().noNewAccountInvite);
|
|
43
|
+
const URL_ERRORS = $derived<Record<string, string>>({
|
|
44
|
+
account_not_linked: m().accountNotLinked,
|
|
45
|
+
unable_to_create_user: noNewAccount,
|
|
46
|
+
forbidden: noNewAccount,
|
|
47
|
+
session_not_fresh: m().loginTooOld,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const displayError = $derived(urlError ? (URL_ERRORS[urlError.toLowerCase()] ?? urlError) : errorMessage);
|
|
51
|
+
|
|
52
|
+
async function requestPasswordReset(e: SubmitEvent) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
if (!forgotPasswordEmail || !forgotPasswordEmail.includes("@")) {
|
|
55
|
+
toast.error(m().invalidEmailDot);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
sendingResetLink = true;
|
|
59
|
+
try {
|
|
60
|
+
const res = await authClient.requestPasswordReset({ email: forgotPasswordEmail, redirectTo: `${window.location.origin}/auth/reset-password` });
|
|
61
|
+
if (res.error) {
|
|
62
|
+
toast.error(res.error.message || m().resetLinkNotSent);
|
|
63
|
+
} else {
|
|
64
|
+
toast.success(m().resetLinkSent);
|
|
65
|
+
showForgotPassword = false;
|
|
66
|
+
forgotPasswordEmail = "";
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
toast.error(messageOf(err, m().resetLinkNotRequested));
|
|
70
|
+
} finally {
|
|
71
|
+
sendingResetLink = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// An AI assistant (MCP client) sent the user here to authorise it; the query is signed by the OAuth provider.
|
|
76
|
+
// Sign-in then carries it along (the OAuth provider client plugin) and the server answers with the next step's URL
|
|
77
|
+
// (consent page or the client's callback), which the auth client follows by itself.
|
|
78
|
+
const oauthRequest = $derived(page.url.searchParams.has("sig") && page.url.searchParams.has("client_id"));
|
|
79
|
+
// Only same-site paths, e.g. back to an Einladung after logging in
|
|
80
|
+
const redirectTo = $derived(sameSitePath(page.url.searchParams.get("redirectTo"), page.url.origin));
|
|
81
|
+
|
|
82
|
+
async function next(data?: { redirect?: boolean; url?: string } | null) {
|
|
83
|
+
if (data?.redirect && data.url) return; // the auth client navigates to data.url
|
|
84
|
+
if (oauthRequest) {
|
|
85
|
+
// Fallback: with a session, the authorize endpoint continues the flow in the browser
|
|
86
|
+
const query = [...page.url.searchParams].filter(([key]) => key !== "sig" && key !== "exp");
|
|
87
|
+
window.location.href = `/api/auth/oauth2/authorize?${query.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")}`;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// A new login: queries cached before it (one that sent here to log in again resolved empty) load again
|
|
91
|
+
await goto(redirectTo ?? href("/home"), { replaceState: true, refreshAll: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function signIn(e: SubmitEvent) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
await authClient.signIn.email(
|
|
97
|
+
{ email, password },
|
|
98
|
+
{
|
|
99
|
+
onRequest: () => {
|
|
100
|
+
loading = true;
|
|
101
|
+
errorMessage = "";
|
|
102
|
+
},
|
|
103
|
+
onSuccess: async (ctx) => {
|
|
104
|
+
loading = false;
|
|
105
|
+
const data = ctx.data as { twoFactorRedirect?: boolean; redirect?: boolean; url?: string } | null;
|
|
106
|
+
if (data?.twoFactorRedirect) show2FAInput = true;
|
|
107
|
+
else await next(data);
|
|
108
|
+
},
|
|
109
|
+
onError: async (ctx) => {
|
|
110
|
+
loading = false;
|
|
111
|
+
// Signup mode open: the address isn't confirmed yet; better-auth has just sent a new link
|
|
112
|
+
errorMessage = ctx.error.code === "EMAIL_NOT_VERIFIED" ? m().confirmEmailFirst : (ctx.error.message ?? "");
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<div class={cn("flex flex-col gap-6", className)} {...restProps}>
|
|
120
|
+
<Card.Root>
|
|
121
|
+
<Card.Header class="text-center">
|
|
122
|
+
<Card.Title class="text-xl">{m().welcomeBack}</Card.Title>
|
|
123
|
+
<Card.Description>{m().logInTo(appName)}</Card.Description>
|
|
124
|
+
</Card.Header>
|
|
125
|
+
<Card.Content>
|
|
126
|
+
{#if show2FAInput}
|
|
127
|
+
<Verify2FAForm
|
|
128
|
+
onSuccess={async (data) => {
|
|
129
|
+
await next(data);
|
|
130
|
+
show2FAInput = false;
|
|
131
|
+
}}
|
|
132
|
+
onCancel={() => {
|
|
133
|
+
show2FAInput = false;
|
|
134
|
+
errorMessage = "";
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
{:else if showForgotPassword}
|
|
138
|
+
<form onsubmit={requestPasswordReset}>
|
|
139
|
+
<div class="grid gap-6">
|
|
140
|
+
<div class="space-y-2 text-center">
|
|
141
|
+
<h3 class="text-lg font-medium">{m().forgotPassword}</h3>
|
|
142
|
+
<p class="text-sm text-muted-foreground">{m().forgotIntro}</p>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="grid gap-3">
|
|
145
|
+
<Label for="resetEmail">{m().email}</Label>
|
|
146
|
+
<Input id="resetEmail" type="email" bind:value={forgotPasswordEmail} placeholder={m().emailPlaceholder} required />
|
|
147
|
+
</div>
|
|
148
|
+
<div class="flex flex-col gap-2">
|
|
149
|
+
<Button type="submit" class="w-full cursor-pointer" disabled={sendingResetLink}>
|
|
150
|
+
{#if sendingResetLink}<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />{/if}
|
|
151
|
+
{m().sendTheLink}
|
|
152
|
+
</Button>
|
|
153
|
+
<Button
|
|
154
|
+
type="button"
|
|
155
|
+
variant="outline"
|
|
156
|
+
class="w-full cursor-pointer"
|
|
157
|
+
onclick={() => {
|
|
158
|
+
showForgotPassword = false;
|
|
159
|
+
errorMessage = "";
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{m().backToLogin}
|
|
163
|
+
</Button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</form>
|
|
167
|
+
{:else}
|
|
168
|
+
<form onsubmit={signIn}>
|
|
169
|
+
<div class="grid gap-6">
|
|
170
|
+
{#if providers?.github || providers?.google}
|
|
171
|
+
<div class="flex flex-col gap-4">
|
|
172
|
+
{#if providers.github}<SocialButton provider="github" callbackURL={redirectTo ?? "/home"} />{/if}
|
|
173
|
+
{#if providers.google}<SocialButton provider="google" callbackURL={redirectTo ?? "/home"} />{/if}
|
|
174
|
+
</div>
|
|
175
|
+
<div class="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
|
176
|
+
<span class="bg-card text-muted-foreground relative z-10 px-2">{m().orWithEmail}</span>
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
<div class="grid gap-6">
|
|
180
|
+
<div class="grid gap-3">
|
|
181
|
+
<Label for="email">{m().email}</Label>
|
|
182
|
+
<Input id="email" name="email" bind:value={email} type="email" placeholder={m().emailPlaceholder} required autocomplete="username" />
|
|
183
|
+
</div>
|
|
184
|
+
<div class="grid gap-3">
|
|
185
|
+
<div class="flex items-center">
|
|
186
|
+
<Label for="password">{m().password}</Label>
|
|
187
|
+
{#if mail}
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onclick={() => {
|
|
191
|
+
showForgotPassword = true;
|
|
192
|
+
errorMessage = "";
|
|
193
|
+
}}
|
|
194
|
+
class="ml-auto cursor-pointer border-0 bg-transparent p-0 text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
|
|
195
|
+
>
|
|
196
|
+
{m().forgotPassword}
|
|
197
|
+
</button>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
<Input id="password" name="password" type="password" bind:value={password} required autocomplete="current-password" />
|
|
201
|
+
</div>
|
|
202
|
+
{#if displayError}
|
|
203
|
+
<div class="rounded-md text-center text-sm text-destructive">{displayError}</div>
|
|
204
|
+
{/if}
|
|
205
|
+
<Button type="submit" class="w-full cursor-pointer" disabled={loading}>
|
|
206
|
+
{#if loading}<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />{/if}
|
|
207
|
+
{m().logIn}
|
|
208
|
+
</Button>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="text-center text-sm">
|
|
211
|
+
{m().noAccountYet}
|
|
212
|
+
<a href={href("/auth/signup")} class="underline underline-offset-4">{m().signUp}</a>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</form>
|
|
216
|
+
{/if}
|
|
217
|
+
</Card.Content>
|
|
218
|
+
</Card.Root>
|
|
219
|
+
{@render footer?.()}
|
|
220
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
class?: string;
|
|
4
|
+
footer?: Snippet;
|
|
5
|
+
};
|
|
6
|
+
declare const LoginForm: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type LoginForm = ReturnType<typeof LoginForm>;
|
|
8
|
+
export default LoginForm;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronRight } from "@lucide/svelte";
|
|
3
|
+
import * as Avatar from "../components/ui/avatar/index.js";
|
|
4
|
+
import { m } from "../messages.js";
|
|
5
|
+
import type { AppUserClient } from "../types.js";
|
|
6
|
+
import type { SettingsTabDef } from "./tabs.js";
|
|
7
|
+
import { displayInitials } from "../util/names.js";
|
|
8
|
+
|
|
9
|
+
let { user, memberSince, tabs, onSelectTab }: { user: AppUserClient; memberSince: string; tabs: SettingsTabDef[]; onSelectTab: (tabName: string) => void } = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<section class="panel flex items-center gap-4 p-5">
|
|
13
|
+
<Avatar.Root class="size-14">
|
|
14
|
+
{#if user.image}<Avatar.Image src={user.image} alt="" />{/if}
|
|
15
|
+
<Avatar.Fallback class="bg-primary/10 text-xl font-semibold text-primary">{displayInitials(user)}</Avatar.Fallback>
|
|
16
|
+
</Avatar.Root>
|
|
17
|
+
<div class="min-w-0">
|
|
18
|
+
<span class="section-kicker">{m().profile}</span>
|
|
19
|
+
<h1 class="truncate text-xl font-semibold">{user.name}</h1>
|
|
20
|
+
<p class="mt-0.5 truncate text-xs text-muted-foreground">{#if memberSince}{m().memberSince(memberSince)}{:else}{user.email}{/if}</p>
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<section class="panel divide-y px-4">
|
|
25
|
+
{#each tabs as tab (tab.id)}
|
|
26
|
+
<button onclick={() => onSelectTab(tab.id)} class="flex w-full cursor-pointer items-center gap-4 py-3.5 text-left">
|
|
27
|
+
<span class="grid size-9 place-items-center rounded-lg {tab.id === 'deletion' ? 'bg-destructive/10 text-destructive' : 'bg-primary/10 text-primary'}">
|
|
28
|
+
<tab.icon size={20} />
|
|
29
|
+
</span>
|
|
30
|
+
<span class="grid min-w-0 flex-1">
|
|
31
|
+
<strong class="text-sm font-medium">{tab.label}</strong>
|
|
32
|
+
<small class="text-xs text-muted-foreground">{tab.hint}</small>
|
|
33
|
+
</span>
|
|
34
|
+
<ChevronRight size={18} class="text-muted-foreground" />
|
|
35
|
+
</button>
|
|
36
|
+
{/each}
|
|
37
|
+
</section>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AppUserClient } from "../types.js";
|
|
2
|
+
import type { SettingsTabDef } from "./tabs.js";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
user: AppUserClient;
|
|
5
|
+
memberSince: string;
|
|
6
|
+
tabs: SettingsTabDef[];
|
|
7
|
+
onSelectTab: (tabName: string) => void;
|
|
8
|
+
};
|
|
9
|
+
declare const MobileSettingsMenu: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type MobileSettingsMenu = ReturnType<typeof MobileSettingsMenu>;
|
|
11
|
+
export default MobileSettingsMenu;
|