-
-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
523 additions
and
456 deletions.
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
apps/frontend/src/lib/components/blocks/auth/login-or-signup.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<script lang="ts"> | ||
import * as Tabs from "$lib/components/ui/tabs" | ||
import Login from "./login.svelte" | ||
import Signup from "./signup.svelte" | ||
export let registrationEnabled: boolean | ||
export let redirect: string | null | ||
export let onSuccess: () => void = () => {} | ||
export let githubEnabled: boolean | ||
export let googleEnabled: boolean | ||
export let invitationId: string | null | ||
export let email: string | null | ||
</script> | ||
|
||
<Tabs.Root value="login" class="w-full"> | ||
<Tabs.List class="w-full"> | ||
<Tabs.Trigger value="login" class="flex-1">Login</Tabs.Trigger> | ||
<Tabs.Trigger value="signup" class="flex-1">Signup</Tabs.Trigger> | ||
</Tabs.List> | ||
<Tabs.Content value="login"> | ||
<Login {registrationEnabled} {redirect} {onSuccess} {githubEnabled} {googleEnabled} /> | ||
</Tabs.Content> | ||
<Tabs.Content value="signup"> | ||
<Signup {redirect} {onSuccess} {githubEnabled} {googleEnabled} {invitationId} {email} /> | ||
</Tabs.Content> | ||
</Tabs.Root> |
176 changes: 176 additions & 0 deletions
176
apps/frontend/src/lib/components/blocks/auth/login.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
<script lang="ts"> | ||
import { goto } from "$app/navigation" | ||
import { Input } from "$lib/components/ui/input/index.js" | ||
import { Label } from "$lib/components/ui/label/index.js" | ||
import Github from "$lib/images/github.svg" | ||
import Google from "$lib/images/Google.svg" | ||
import { createMutation } from "@tanstack/svelte-query" | ||
import { z } from "@undb/zod" | ||
import { defaults, superForm } from "sveltekit-superforms" | ||
import { zodClient } from "sveltekit-superforms/adapters" | ||
import * as Form from "$lib/components/ui/form" | ||
import { Button } from "$lib/components/ui/button" | ||
import { Separator } from "$lib/components/ui/separator" | ||
import PasswordInput from "$lib/components/ui/input/password-input.svelte" | ||
import * as Alert from "$lib/components/ui/alert/index.js" | ||
import autoAnimate from "@formkit/auto-animate" | ||
import { LoaderCircleIcon } from "lucide-svelte" | ||
import ResetPassword from "$lib/components/blocks/auth/reset-password.svelte" | ||
import { page } from "$app/stores" | ||
export let registrationEnabled: boolean | ||
export let redirect: string | null | ||
export let onSuccess: () => void = () => {} | ||
export let githubEnabled: boolean | ||
export let googleEnabled: boolean | ||
const schema = z.object({ | ||
email: z.string().email(), | ||
password: z.string(), | ||
}) | ||
type LoginSchema = z.infer<typeof schema> | ||
let loginError = false | ||
const loginMutation = createMutation({ | ||
mutationFn: async (input: LoginSchema) => { | ||
try { | ||
const { ok } = await fetch("/api/login", { method: "POST", body: JSON.stringify(input) }) | ||
if (!ok) { | ||
throw new Error("Failed to login") | ||
} | ||
return | ||
} catch (error) { | ||
loginError = true | ||
} | ||
}, | ||
onMutate(variables) { | ||
loginError = false | ||
}, | ||
async onSuccess(data, variables, context) { | ||
onSuccess() | ||
}, | ||
async onError(error, variables, context) { | ||
loginError = true | ||
}, | ||
}) | ||
const form = superForm( | ||
defaults( | ||
{ | ||
email: $page.url.searchParams.get("email") ?? "", | ||
password: "", | ||
}, | ||
zodClient(schema), | ||
), | ||
{ | ||
SPA: true, | ||
dataType: "json", | ||
validators: zodClient(schema), | ||
resetForm: false, | ||
invalidateAll: false, | ||
async onUpdate(event) { | ||
if (!event.form.valid) { | ||
console.log(event.form.errors) | ||
return | ||
} | ||
await $loginMutation.mutateAsync(event.form.data) | ||
}, | ||
}, | ||
) | ||
const { enhance, form: formData } = form | ||
let resetPassword = false | ||
</script> | ||
|
||
{#if resetPassword} | ||
<ResetPassword /> | ||
{:else} | ||
<form method="POST" use:enhance> | ||
<div class="grid gap-4"> | ||
<div class="grid gap-2"> | ||
<Form.Field {form} name="email"> | ||
<Form.Control let:attrs> | ||
<Form.Label for="email">Email</Form.Label> | ||
<Input | ||
{...attrs} | ||
id="email" | ||
type="email" | ||
placeholder="Enter your email to login" | ||
bind:value={$formData.email} | ||
/> | ||
</Form.Control> | ||
<Form.Description /> | ||
<Form.FieldErrors /> | ||
</Form.Field> | ||
</div> | ||
<div class="grid gap-2"> | ||
<Form.Field {form} name="password"> | ||
<Form.Control let:attrs> | ||
<div class="flex justify-between"> | ||
<Label for="password">Password</Label> | ||
<Button | ||
tabindex={-1} | ||
variant="link" | ||
class="ml-auto h-auto p-0 text-sm" | ||
on:click={() => { | ||
resetPassword = true | ||
}}>Forgot your password?</Button | ||
> | ||
</div> | ||
<PasswordInput {...attrs} id="password" placeholder="*****" bind:value={$formData.password} /> | ||
</Form.Control> | ||
<Form.Description /> | ||
<Form.FieldErrors /> | ||
</Form.Field> | ||
</div> | ||
<Form.Button type="submit" class="w-full" disabled={$loginMutation.isPending}> | ||
{#if $loginMutation.isPending} | ||
<LoaderCircleIcon class="mr-2 h-5 w-5 animate-spin" /> | ||
{/if} | ||
Login | ||
</Form.Button> | ||
</div> | ||
<div class="mt-4" use:autoAnimate> | ||
{#if loginError} | ||
<Alert.Root variant="destructive"> | ||
<Alert.Title>Error</Alert.Title> | ||
<Alert.Description>Invalid email or password.</Alert.Description> | ||
</Alert.Root> | ||
{/if} | ||
</div> | ||
{#if registrationEnabled} | ||
<div class="mt-4 text-center text-sm"> | ||
Don't have an account? | ||
{#if redirect} | ||
<a href="/signup?redirect={encodeURIComponent(redirect)}" class="underline"> Sign up </a> | ||
{:else} | ||
<a href="/signup" class="underline"> Sign up </a> | ||
{/if} | ||
</div> | ||
{:else} | ||
<p class="text-muted-foreground mt-4 text-center text-xs"> | ||
Registration is disabled. <br /> Contact your administrator to request access. | ||
</p> | ||
{/if} | ||
{#if githubEnabled || googleEnabled} | ||
<Separator class="my-6" /> | ||
<div class="space-y-2"> | ||
{#if githubEnabled} | ||
<Button href="/login/github" variant="secondary" class="w-full"> | ||
<img class="mr-2 h-4 w-4" src={Github} alt="github" /> | ||
Login with Github | ||
</Button> | ||
{/if} | ||
{#if googleEnabled} | ||
<Button href="/login/google" variant="secondary" class="w-full"> | ||
<img class="mr-2 h-4 w-4" src={Google} alt="google" /> | ||
Login with Google | ||
</Button> | ||
{/if} | ||
</div> | ||
{/if} | ||
</form> | ||
{/if} |
193 changes: 193 additions & 0 deletions
193
apps/frontend/src/lib/components/blocks/auth/signup.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
<script lang="ts"> | ||
import { goto } from "$app/navigation" | ||
import { Button } from "$lib/components/ui/button/index.js" | ||
import { Input } from "$lib/components/ui/input/index.js" | ||
import { Label } from "$lib/components/ui/label/index.js" | ||
import Logo from "$lib/images/logo.svg" | ||
import Github from "$lib/images/github.svg" | ||
import Google from "$lib/images/Google.svg" | ||
import { createMutation } from "@tanstack/svelte-query" | ||
import { z } from "@undb/zod" | ||
import { defaults, superForm } from "sveltekit-superforms" | ||
import { zodClient } from "sveltekit-superforms/adapters" | ||
import * as Form from "$lib/components/ui/form" | ||
import { Separator } from "$lib/components/ui/separator" | ||
import PasswordInput from "$lib/components/ui/input/password-input.svelte" | ||
import { LoaderCircleIcon } from "lucide-svelte" | ||
export let redirect: string | null | ||
export let invitationId: string | null | ||
export let email: string | null | ||
export let githubEnabled: boolean | ||
export let googleEnabled: boolean | ||
export let onSuccess: () => void = () => {} | ||
const schema = z.object({ | ||
email: z.string().email(), | ||
password: z.string().min(6), | ||
username: z.string().min(2).optional(), | ||
}) | ||
const formSchema = schema | ||
.merge( | ||
z.object({ | ||
confirmPassword: z.string(), | ||
}), | ||
) | ||
.refine((data) => data.password === data.confirmPassword, { | ||
message: "Passwords do not match", | ||
}) | ||
type SignupSchema = z.infer<typeof schema> | ||
let signupError = false | ||
const signupMutation = createMutation({ | ||
mutationFn: async (input: SignupSchema) => { | ||
try { | ||
const { ok } = await fetch("/api/signup", { | ||
method: "POST", | ||
body: JSON.stringify({ ...input, invitationId }), | ||
}) | ||
if (!ok) { | ||
throw new Error("Failed to signup") | ||
} | ||
} catch (error) { | ||
signupError = true | ||
} | ||
}, | ||
onMutate(variables) { | ||
signupError = false | ||
}, | ||
async onSuccess(data, variables, context) { | ||
onSuccess() | ||
}, | ||
onError(error, variables, context) { | ||
signupError = true | ||
}, | ||
}) | ||
const form = superForm( | ||
defaults( | ||
{ | ||
email: email || "", | ||
password: "", | ||
confirmPassword: "", | ||
username: "", | ||
}, | ||
zodClient(formSchema), | ||
), | ||
{ | ||
SPA: true, | ||
dataType: "json", | ||
validators: zodClient(formSchema), | ||
resetForm: false, | ||
invalidateAll: false, | ||
onUpdate(event) { | ||
if (!event.form.valid) { | ||
console.log(event.form.errors) | ||
return | ||
} | ||
const { confirmPassword, ...data } = event.form.data | ||
$signupMutation.mutate(data) | ||
}, | ||
}, | ||
) | ||
const { enhance, form: formData, allErrors } = form | ||
$: disabled = $allErrors.length > 0 || $signupMutation.isPending | ||
</script> | ||
|
||
<form method="POST" use:enhance> | ||
<div class="grid gap-2"> | ||
<div class="grid gap-2"> | ||
<Form.Field {form} name="username"> | ||
<Form.Control let:attrs> | ||
<Form.Label for="username">Username</Form.Label> | ||
<Input {...attrs} placeholder="Enter your display username" id="username" bind:value={$formData.username} /> | ||
</Form.Control> | ||
<Form.Description /> | ||
<Form.FieldErrors /> | ||
</Form.Field> | ||
</div> | ||
<div class="grid gap-2"> | ||
<Form.Field {form} name="email"> | ||
<Form.Control let:attrs> | ||
<Form.Label for="email">Email</Form.Label> | ||
<Input {...attrs} id="email" type="email" placeholder="Enter your work email" bind:value={$formData.email} /> | ||
</Form.Control> | ||
<Form.Description /> | ||
<Form.FieldErrors /> | ||
</Form.Field> | ||
</div> | ||
<div class="grid gap-2"> | ||
<Form.Field {form} name="password"> | ||
<Form.Control let:attrs> | ||
<div class="flex justify-between"> | ||
<Label for="password">Password</Label> | ||
</div> | ||
<PasswordInput | ||
{...attrs} | ||
id="password" | ||
type="password" | ||
placeholder="******" | ||
bind:value={$formData.password} | ||
/> | ||
</Form.Control> | ||
<Form.Description /> | ||
<Form.FieldErrors /> | ||
</Form.Field> | ||
</div> | ||
<div class="grid gap-2"> | ||
<Form.Field {form} name="confirmPassword"> | ||
<Form.Control let:attrs> | ||
<div class="flex justify-between"> | ||
<Label for="confirmPassword">Confirm Password</Label> | ||
</div> | ||
<PasswordInput | ||
{...attrs} | ||
id="confirmPassword" | ||
type="password" | ||
placeholder="******" | ||
bind:value={$formData.confirmPassword} | ||
/> | ||
</Form.Control> | ||
<Form.Description /> | ||
<Form.FieldErrors /> | ||
</Form.Field> | ||
</div> | ||
<Button {disabled} type="submit" class="w-full"> | ||
{#if $signupMutation.isPending} | ||
<LoaderCircleIcon class="mr-2 h-5 w-5 animate-spin" /> | ||
{/if} | ||
Create an account | ||
</Button> | ||
</div> | ||
<div class="mt-4 text-center text-sm"> | ||
Already have an account? | ||
{#if invitationId} | ||
<a href={`/login?invitationId=${invitationId}`} class="underline"> Sign in </a> | ||
{:else if redirect} | ||
<a href={`/login?redirect=${encodeURIComponent(redirect)}`} class="underline"> Sign in </a> | ||
{:else} | ||
<a href="/login" class="underline"> Sign in </a> | ||
{/if} | ||
</div> | ||
{#if !invitationId && (githubEnabled || googleEnabled)} | ||
<Separator class="my-6" /> | ||
<div class="space-y-2"> | ||
{#if githubEnabled} | ||
<Button href="/login/github" variant="secondary" class="w-full"> | ||
<img class="mr-2 h-4 w-4" src={Github} alt="github" /> | ||
Login with Github | ||
</Button> | ||
{/if} | ||
{#if googleEnabled} | ||
<Button href="/login/google" variant="secondary" class="w-full"> | ||
<img class="mr-2 h-4 w-4" src={Google} alt="google" /> | ||
Login with Google | ||
</Button> | ||
{/if} | ||
</div> | ||
{/if} | ||
</form> |
Oops, something went wrong.