@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +24 -1
  3. package/dist/components/ui/avatar/avatar-fallback.svelte +17 -0
  4. package/dist/components/ui/avatar/avatar-fallback.svelte.d.ts +4 -0
  5. package/dist/components/ui/avatar/avatar-image.svelte +17 -0
  6. package/dist/components/ui/avatar/avatar-image.svelte.d.ts +4 -0
  7. package/dist/components/ui/avatar/avatar.svelte +17 -0
  8. package/dist/components/ui/avatar/avatar.svelte.d.ts +4 -0
  9. package/dist/components/ui/avatar/index.d.ts +4 -0
  10. package/dist/components/ui/avatar/index.js +6 -0
  11. package/dist/components/ui/button/button.svelte +82 -0
  12. package/dist/components/ui/button/button.svelte.d.ts +70 -0
  13. package/dist/components/ui/button/index.d.ts +2 -0
  14. package/dist/components/ui/button/index.js +2 -0
  15. package/dist/components/ui/card/card-action.svelte +20 -0
  16. package/dist/components/ui/card/card-action.svelte.d.ts +5 -0
  17. package/dist/components/ui/card/card-content.svelte +15 -0
  18. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  19. package/dist/components/ui/card/card-description.svelte +20 -0
  20. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  21. package/dist/components/ui/card/card-footer.svelte +20 -0
  22. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  23. package/dist/components/ui/card/card-header.svelte +23 -0
  24. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  25. package/dist/components/ui/card/card-title.svelte +20 -0
  26. package/dist/components/ui/card/card-title.svelte.d.ts +5 -0
  27. package/dist/components/ui/card/card.svelte +23 -0
  28. package/dist/components/ui/card/card.svelte.d.ts +5 -0
  29. package/dist/components/ui/card/index.d.ts +8 -0
  30. package/dist/components/ui/card/index.js +10 -0
  31. package/dist/components/ui/input/index.d.ts +2 -0
  32. package/dist/components/ui/input/index.js +4 -0
  33. package/dist/components/ui/input/input.svelte +48 -0
  34. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  35. package/dist/components/ui/label/index.d.ts +2 -0
  36. package/dist/components/ui/label/index.js +4 -0
  37. package/dist/components/ui/label/label.svelte +20 -0
  38. package/dist/components/ui/label/label.svelte.d.ts +4 -0
  39. package/dist/components/ui/tabs/index.d.ts +5 -0
  40. package/dist/components/ui/tabs/index.js +7 -0
  41. package/dist/components/ui/tabs/tabs-content.svelte +17 -0
  42. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  43. package/dist/components/ui/tabs/tabs-list.svelte +20 -0
  44. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  45. package/dist/components/ui/tabs/tabs-trigger.svelte +20 -0
  46. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  47. package/dist/components/ui/tabs/tabs.svelte +19 -0
  48. package/dist/components/ui/tabs/tabs.svelte.d.ts +4 -0
  49. package/dist/messages.d.ts +196 -0
  50. package/dist/messages.js +404 -0
  51. package/dist/ui/AdminView.svelte +91 -0
  52. package/dist/ui/AdminView.svelte.d.ts +6 -0
  53. package/dist/ui/ChangePasswordCard.svelte +62 -0
  54. package/dist/ui/ChangePasswordCard.svelte.d.ts +3 -0
  55. package/dist/ui/DeletionTab.svelte +76 -0
  56. package/dist/ui/DeletionTab.svelte.d.ts +6 -0
  57. package/dist/ui/EinladungErstellen.svelte +74 -0
  58. package/dist/ui/EinladungErstellen.svelte.d.ts +3 -0
  59. package/dist/ui/EinladungPage.svelte +42 -0
  60. package/dist/ui/EinladungPage.svelte.d.ts +13 -0
  61. package/dist/ui/IconGithub.svelte +23 -0
  62. package/dist/ui/IconGithub.svelte.d.ts +4 -0
  63. package/dist/ui/IconGoogle.svelte +16 -0
  64. package/dist/ui/IconGoogle.svelte.d.ts +4 -0
  65. package/dist/ui/InvitationsTab.svelte +46 -0
  66. package/dist/ui/InvitationsTab.svelte.d.ts +3 -0
  67. package/dist/ui/LinksTab.svelte +91 -0
  68. package/dist/ui/LinksTab.svelte.d.ts +3 -0
  69. package/dist/ui/LoginForm.svelte +220 -0
  70. package/dist/ui/LoginForm.svelte.d.ts +8 -0
  71. package/dist/ui/MobileSettingsMenu.svelte +37 -0
  72. package/dist/ui/MobileSettingsMenu.svelte.d.ts +11 -0
  73. package/dist/ui/ProfileManagementTab.svelte +56 -0
  74. package/dist/ui/ProfileManagementTab.svelte.d.ts +7 -0
  75. package/dist/ui/RegisterForm.svelte +143 -0
  76. package/dist/ui/RegisterForm.svelte.d.ts +6 -0
  77. package/dist/ui/ResetPasswordForm.svelte +91 -0
  78. package/dist/ui/ResetPasswordForm.svelte.d.ts +3 -0
  79. package/dist/ui/SecurityTab.svelte +25 -0
  80. package/dist/ui/SecurityTab.svelte.d.ts +3 -0
  81. package/dist/ui/SessionsTab.svelte +98 -0
  82. package/dist/ui/SessionsTab.svelte.d.ts +3 -0
  83. package/dist/ui/SetPasswordCard.svelte +56 -0
  84. package/dist/ui/SetPasswordCard.svelte.d.ts +6 -0
  85. package/dist/ui/SettingsHeader.svelte +19 -0
  86. package/dist/ui/SettingsHeader.svelte.d.ts +7 -0
  87. package/dist/ui/SettingsPage.svelte +96 -0
  88. package/dist/ui/SettingsPage.svelte.d.ts +9 -0
  89. package/dist/ui/SettingsTabsList.svelte +17 -0
  90. package/dist/ui/SettingsTabsList.svelte.d.ts +7 -0
  91. package/dist/ui/SocialButton.svelte +21 -0
  92. package/dist/ui/SocialButton.svelte.d.ts +7 -0
  93. package/dist/ui/TwoFactorAuthenticationCard.svelte +216 -0
  94. package/dist/ui/TwoFactorAuthenticationCard.svelte.d.ts +3 -0
  95. package/dist/ui/Verify2FAForm.svelte +63 -0
  96. package/dist/ui/Verify2FAForm.svelte.d.ts +11 -0
  97. package/dist/ui/context.d.ts +93 -0
  98. package/dist/ui/context.js +17 -0
  99. package/dist/ui/href.d.ts +2 -0
  100. package/dist/ui/href.js +5 -0
  101. package/dist/ui/index.d.ts +10 -0
  102. package/dist/ui/index.js +11 -0
  103. package/dist/ui/tabs.d.ts +17 -0
  104. package/dist/ui/tabs.js +21 -0
  105. package/dist/utils.d.ts +12 -0
  106. package/dist/utils.js +6 -0
  107. package/package.json +15 -2
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ agents?: boolean;
3
+ };
4
+ declare const DeletionTab: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type DeletionTab = ReturnType<typeof DeletionTab>;
6
+ export default DeletionTab;
@@ -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,3 @@
1
+ declare const EinladungErstellen: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type EinladungErstellen = ReturnType<typeof EinladungErstellen>;
3
+ export default EinladungErstellen;
@@ -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,4 @@
1
+ import type { SVGAttributes } from "svelte/elements";
2
+ declare const IconGithub: import("svelte").Component<SVGAttributes<SVGElement>, {}, "">;
3
+ type IconGithub = ReturnType<typeof IconGithub>;
4
+ export default IconGithub;
@@ -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,4 @@
1
+ import type { SVGAttributes } from "svelte/elements";
2
+ declare const IconGoogle: import("svelte").Component<SVGAttributes<SVGElement>, {}, "">;
3
+ type IconGoogle = ReturnType<typeof IconGoogle>;
4
+ export default IconGoogle;
@@ -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,3 @@
1
+ declare const InvitationsTab: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type InvitationsTab = ReturnType<typeof InvitationsTab>;
3
+ export default InvitationsTab;
@@ -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,3 @@
1
+ declare const LinksTab: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type LinksTab = ReturnType<typeof LinksTab>;
3
+ export default LinksTab;
@@ -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;