login now username+password based
This commit is contained in:
@@ -2,7 +2,6 @@ import {
|
||||
adminClient,
|
||||
customSessionClient,
|
||||
inferAdditionalFields,
|
||||
magicLinkClient,
|
||||
multiSessionClient,
|
||||
usernameClient,
|
||||
} from "better-auth/client/plugins";
|
||||
@@ -13,7 +12,6 @@ export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
usernameClient(),
|
||||
adminClient(),
|
||||
magicLinkClient(),
|
||||
multiSessionClient(),
|
||||
customSessionClient<typeof auth>(),
|
||||
inferAdditionalFields<typeof auth>(),
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { authVM } from "$lib/domains/security/auth.vm.svelte";
|
||||
import GoogleIcon from "~icons/devicon/google";
|
||||
import Icon from "../atoms/icon.svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
</script>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
onclick={() => authVM.handleGoogleOAuthLogin()}
|
||||
disabled={authVM.loggingIn}
|
||||
>
|
||||
<Icon icon={GoogleIcon} cls="h-6 w-6" />
|
||||
<p class="">Sign in with Google</p>
|
||||
</Button>
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { User } from "@pkg/logic/domains/user/data";
|
||||
import { apiClient, user as userStore } from "$lib/global.stores";
|
||||
import { authClient } from "$lib/auth.client";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { get } from "svelte/store";
|
||||
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
||||
import type { Err } from "@pkg/result";
|
||||
|
||||
class AccountViewModel {
|
||||
loading = $state(false);
|
||||
emailLoading = $state(false);
|
||||
passwordLoading = $state(false);
|
||||
errorMessage = $state<string | null>(null);
|
||||
|
||||
async updateProfilePicture(imagePath: string): Promise<boolean> {
|
||||
@@ -104,44 +106,71 @@ class AccountViewModel {
|
||||
return user;
|
||||
}
|
||||
|
||||
async changeEmail(email: string): Promise<boolean> {
|
||||
this.emailLoading = true;
|
||||
async changePassword(password: string): Promise<boolean> {
|
||||
this.passwordLoading = true;
|
||||
this.errorMessage = null;
|
||||
|
||||
const currentUser = get(userStore);
|
||||
if (!currentUser?.id) {
|
||||
this.passwordLoading = false;
|
||||
this.errorMessage = "User not found";
|
||||
toast.error(this.errorMessage);
|
||||
return false;
|
||||
}
|
||||
const client = get(apiClient);
|
||||
if (!client) {
|
||||
this.passwordLoading = false;
|
||||
this.errorMessage = "API client not initialized";
|
||||
toast.error(this.errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await ResultAsync.fromPromise(
|
||||
authClient.changeEmail({ newEmail: email }),
|
||||
client.users["rotate-password"].$put({
|
||||
json: { userId: currentUser.id, password },
|
||||
}),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to change email",
|
||||
message: "Failed to change password",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (response.error) {
|
||||
if (!response.ok) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message:
|
||||
response.error.message ?? "Failed to change email",
|
||||
description:
|
||||
response.error.statusText ??
|
||||
"Please try again later",
|
||||
detail: response.error.statusText ?? "Unknown error",
|
||||
message: "Failed to change password",
|
||||
description: `Response failed with status ${response.status}`,
|
||||
detail: `HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
return okAsync(response.data);
|
||||
return ResultAsync.fromPromise(
|
||||
response.json(),
|
||||
(error): Err => ({
|
||||
code: "PARSING_ERROR",
|
||||
message: "Failed to parse response",
|
||||
description: "Invalid response format",
|
||||
detail:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: String(error),
|
||||
}),
|
||||
).andThen((apiResult: any) => {
|
||||
if (apiResult.error) {
|
||||
return errAsync(apiResult.error);
|
||||
}
|
||||
return okAsync(apiResult.data);
|
||||
});
|
||||
});
|
||||
|
||||
const success = result.match(
|
||||
() => {
|
||||
toast.success("Verification email sent!", {
|
||||
description:
|
||||
"Please check your inbox and verify your new email address.",
|
||||
});
|
||||
toast.success("Password updated successfully");
|
||||
return true;
|
||||
},
|
||||
(error) => {
|
||||
this.errorMessage = error.message ?? "Failed to change email";
|
||||
this.errorMessage = error.message ?? "Failed to change password";
|
||||
toast.error(this.errorMessage, {
|
||||
description: error.description,
|
||||
});
|
||||
@@ -149,7 +178,7 @@ class AccountViewModel {
|
||||
},
|
||||
);
|
||||
|
||||
this.emailLoading = false;
|
||||
this.passwordLoading = false;
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,32 @@
|
||||
import { apiClient } from "$lib/global.stores";
|
||||
import { authClient } from "$lib/auth.client";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { get } from "svelte/store";
|
||||
import { page } from "$app/state";
|
||||
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
||||
import type { Err } from "@pkg/result";
|
||||
|
||||
class AuthViewModel {
|
||||
loggingIn = $state(false);
|
||||
loginError = $state<string | null>(null);
|
||||
|
||||
async handleGoogleOAuthLogin() {
|
||||
const result = await ResultAsync.fromPromise(
|
||||
authClient.signIn.social({ provider: "google" }),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to initiate Google OAuth login",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
async loginWithCredentials(data: FormData): Promise<boolean> {
|
||||
const username = data.get("username")?.toString().trim();
|
||||
const password = data.get("password")?.toString();
|
||||
|
||||
result.match(
|
||||
() => {
|
||||
// OAuth flow will redirect, no action needed
|
||||
},
|
||||
(error) => {
|
||||
toast.error("Failed to initiate Google login", {
|
||||
description: error.description,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!username || username.length < 3) {
|
||||
toast.error("Please enter a valid username");
|
||||
return false;
|
||||
}
|
||||
|
||||
async loginWithEmail(data: FormData): Promise<boolean> {
|
||||
const email = data.get("email")?.toString();
|
||||
|
||||
if (!email || email.length < 5) {
|
||||
toast.error("Please enter a valid email");
|
||||
if (!password || password.length < 6) {
|
||||
toast.error("Please enter a valid password");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loggingIn = true;
|
||||
this.loginError = null;
|
||||
|
||||
const result = await ResultAsync.fromPromise(
|
||||
authClient.signIn.magicLink({ email }),
|
||||
authClient.signIn.username({ username, password }),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to send magic link",
|
||||
message: "Failed to login",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
@@ -57,10 +35,10 @@ class AuthViewModel {
|
||||
if (response.error) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: response.error.message ?? "Something went wrong",
|
||||
message: response.error.message ?? "Invalid credentials",
|
||||
description:
|
||||
response.error.statusText ??
|
||||
"Please try another login method or try again later",
|
||||
"Please check your username and password",
|
||||
detail: response.error.statusText ?? "Unknown error",
|
||||
});
|
||||
}
|
||||
@@ -69,26 +47,18 @@ class AuthViewModel {
|
||||
|
||||
const success = result.match(
|
||||
() => {
|
||||
this.loginError = null;
|
||||
toast.success("Login request sent", {
|
||||
description: "Check your email for a magic link to log in",
|
||||
toast.success("Login successful", {
|
||||
description: "Redirecting...",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 500);
|
||||
return true;
|
||||
},
|
||||
(error) => {
|
||||
// Set error message for UI display
|
||||
const errorMessage = error.message ?? "Something went wrong";
|
||||
this.loginError = errorMessage;
|
||||
|
||||
// Don't show toast for known error cases that we handle in the UI
|
||||
if (
|
||||
!errorMessage.includes("not valid to login") &&
|
||||
!errorMessage.includes("not allowed")
|
||||
) {
|
||||
toast.error(errorMessage, {
|
||||
description: error.description,
|
||||
});
|
||||
}
|
||||
toast.error(error.message ?? "Invalid credentials", {
|
||||
description: error.description,
|
||||
});
|
||||
return false;
|
||||
},
|
||||
);
|
||||
@@ -96,138 +66,6 @@ class AuthViewModel {
|
||||
this.loggingIn = false;
|
||||
return success;
|
||||
}
|
||||
|
||||
clearLoginError() {
|
||||
this.loginError = null;
|
||||
}
|
||||
|
||||
async verifyMagicLink() {
|
||||
const token = page.url.searchParams.get("token");
|
||||
if (!token) {
|
||||
throw new Error("No token provided");
|
||||
}
|
||||
|
||||
const result = await ResultAsync.fromPromise(
|
||||
authClient.magicLink.verify({ query: { token } }),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to verify magic link",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (response.error) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Could not verify magic link",
|
||||
description: response.error.message ?? "Unknown error",
|
||||
detail: response.error.statusText ?? "Unknown error",
|
||||
});
|
||||
}
|
||||
if (!response.data?.user?.id) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Invalid verification result",
|
||||
description: "User ID not found in verification response",
|
||||
detail: "Missing user.id in response",
|
||||
});
|
||||
}
|
||||
return okAsync(response.data);
|
||||
})
|
||||
.andThen((verificationResult) => {
|
||||
return ResultAsync.fromPromise(
|
||||
get(apiClient).users["ensure-account-exists"].$put({
|
||||
json: { userId: verificationResult.user.id },
|
||||
}),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to ensure account exists",
|
||||
description: "Network request failed",
|
||||
detail:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (!response.ok) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Failed to ensure account exists",
|
||||
description: `Response failed with status ${response.status}`,
|
||||
detail: `HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
return ResultAsync.fromPromise(
|
||||
response.json(),
|
||||
(error): Err => ({
|
||||
code: "PARSING_ERROR",
|
||||
message: "Failed to parse response",
|
||||
description: "Invalid response format",
|
||||
detail:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: String(error),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.andThen((apiResult: any) => {
|
||||
if (apiResult.error) {
|
||||
return errAsync(apiResult.error);
|
||||
}
|
||||
return okAsync(apiResult.data);
|
||||
});
|
||||
});
|
||||
|
||||
result.match(
|
||||
() => {
|
||||
// Success - account ensured
|
||||
},
|
||||
(error: Err) => {
|
||||
throw new Error(error.message ?? "Failed to verify magic link");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async verifyEmailChange() {
|
||||
const token = page.url.searchParams.get("token");
|
||||
if (!token) {
|
||||
throw new Error("No verification token provided");
|
||||
}
|
||||
|
||||
const result = await ResultAsync.fromPromise(
|
||||
authClient.verifyEmail({
|
||||
query: { token },
|
||||
}),
|
||||
(error): Err => ({
|
||||
code: "NETWORK_ERROR",
|
||||
message: "Failed to verify email change",
|
||||
description: "Network request failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
)
|
||||
.andThen((response) => {
|
||||
if (response.error) {
|
||||
return errAsync({
|
||||
code: "API_ERROR",
|
||||
message: "Could not verify email change",
|
||||
description: response.error.message ?? "Unknown error",
|
||||
detail: response.error.statusText ?? "Unknown error",
|
||||
});
|
||||
}
|
||||
return okAsync(response.data);
|
||||
});
|
||||
|
||||
result.match(
|
||||
() => {
|
||||
// Success - email verified
|
||||
},
|
||||
(error: Err) => {
|
||||
throw new Error(error.message ?? "Could not verify email change");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const authVM = new AuthViewModel();
|
||||
|
||||
@@ -3,152 +3,76 @@
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import { authVM } from "$lib/domains/security/auth.vm.svelte";
|
||||
import { AlertCircle, AtSign, CheckCircle, Loader2 } from "@lucide/svelte";
|
||||
import { AtSign, KeyRound, Loader2 } from "@lucide/svelte";
|
||||
|
||||
let status = $state<"idle" | "sending" | "sent" | "failed">("idle");
|
||||
let submittedEmail = $state<string>("");
|
||||
let email = $state<string>("");
|
||||
let emailFocused = $state<boolean>(false);
|
||||
let isEmailValid = $state<boolean>(false);
|
||||
let username = $state<string>("");
|
||||
let password = $state<string>("");
|
||||
|
||||
// Validate email as user types
|
||||
$effect(() => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
isEmailValid = emailRegex.test(email) && email.length >= 5;
|
||||
});
|
||||
|
||||
// Clear API error when user starts typing
|
||||
$effect(() => {
|
||||
if (email && authVM.loginError) {
|
||||
authVM.clearLoginError();
|
||||
}
|
||||
});
|
||||
|
||||
function onInput(e: Event & { currentTarget: HTMLInputElement }) {
|
||||
email = e.currentTarget.value;
|
||||
}
|
||||
const canSubmit = $derived(
|
||||
username.trim().length >= 3 && password.length >= 6 && !authVM.loggingIn,
|
||||
);
|
||||
|
||||
async function onSubmit(
|
||||
e: SubmitEvent & { currentTarget: HTMLFormElement },
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (!isEmailValid || authVM.loggingIn) return;
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
submittedEmail = (formData.get("email")?.toString() || "").trim();
|
||||
|
||||
status = "sending";
|
||||
try {
|
||||
const ok = await authVM.loginWithEmail(formData);
|
||||
status = ok ? "sent" : "failed";
|
||||
} catch {
|
||||
status = "failed";
|
||||
}
|
||||
}
|
||||
|
||||
function resetFormState() {
|
||||
status = "idle";
|
||||
submittedEmail = "";
|
||||
authVM.clearLoginError();
|
||||
if (!canSubmit) return;
|
||||
await authVM.loginWithCredentials(new FormData(e.currentTarget));
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="space-y-4" onsubmit={onSubmit} aria-describedby="login-status">
|
||||
<!-- Email -->
|
||||
<form class="space-y-4" onsubmit={onSubmit}>
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
for="email"
|
||||
for="username"
|
||||
class="text-foreground flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<Icon icon={AtSign} cls="h-4 w-4 text-muted-foreground" />
|
||||
<span>Email address</span>
|
||||
<span>Username</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
minlength={5}
|
||||
placeholder="you@example.com"
|
||||
bind:value={email}
|
||||
oninput={onInput}
|
||||
onfocus={() => (emailFocused = true)}
|
||||
onblur={() => (emailFocused = false)}
|
||||
class="h-12 pr-12"
|
||||
aria-invalid={!isEmailValid && email.length > 0}
|
||||
/>
|
||||
<!-- Right adornment: validation / loader -->
|
||||
<div class="absolute top-1/2 right-3 -translate-y-1/2">
|
||||
{#if authVM.loggingIn}
|
||||
<Icon
|
||||
icon={Loader2}
|
||||
cls="h-5 w-5 animate-spin text-muted-foreground"
|
||||
/>
|
||||
{:else if isEmailValid && email.length > 0}
|
||||
<Icon icon={CheckCircle} cls="h-5 w-5 text-emerald-500" />
|
||||
{:else if email.length > 0}
|
||||
<!-- keep space balanced with a subtle icon state -->
|
||||
<Icon
|
||||
icon={AtSign}
|
||||
cls="h-5 w-5 text-muted-foreground/50"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if emailFocused && email.length > 0 && !isEmailValid}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Please enter a valid email address.
|
||||
</p>
|
||||
{/if}
|
||||
<Input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
minlength={3}
|
||||
maxlength={64}
|
||||
placeholder="your-username"
|
||||
bind:value={username}
|
||||
class="h-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
for="password"
|
||||
class="text-foreground flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<Icon icon={KeyRound} cls="h-4 w-4 text-muted-foreground" />
|
||||
<span>Password</span>
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
minlength={6}
|
||||
placeholder="Your password"
|
||||
bind:value={password}
|
||||
class="h-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<Button
|
||||
type="submit"
|
||||
class="from-primary to-primary/90 hover:from-primary/90 hover:to-primary h-12 w-full bg-gradient-to-r text-base font-medium shadow-lg transition-all duration-200 hover:shadow-xl"
|
||||
disabled={authVM.loggingIn || !isEmailValid}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{#if authVM.loggingIn}
|
||||
<Icon icon={Loader2} cls="mr-2 h-5 w-5 animate-spin" />
|
||||
Sending Magic Link...
|
||||
Signing In...
|
||||
{:else}
|
||||
Send Magic Link
|
||||
Sign In
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
<!-- Inline status messages (minimal) -->
|
||||
<div id="login-status" class="space-y-2" aria-live="polite" role="status">
|
||||
{#if status === "sent"}
|
||||
<div
|
||||
class="bg-emerald-50 dark:bg-emerald-950/20 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800 rounded-md px-3 py-2 text-sm"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<Icon icon={CheckCircle} cls="h-4 w-4 mt-0.5" />
|
||||
<p>
|
||||
We’ve sent a magic link to <span class="font-medium"
|
||||
>{submittedEmail}</span
|
||||
>. Check your inbox.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if status === "failed" && authVM.loginError}
|
||||
<div
|
||||
class="bg-red-50 dark:bg-red-950/30 text-red-700 dark:text-red-300 border border-red-200 dark:border-red-800 rounded-md px-3 py-2 text-sm"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<Icon icon={AlertCircle} cls="h-4 w-4 mt-0.5" />
|
||||
<p>{authVM.loginError}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-2 text-xs underline underline-offset-2 hover:opacity-80"
|
||||
onclick={resetFormState}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { sessionsVM } from "$lib/domains/account/sessions/sessions.vm.svelte";
|
||||
import { authVM } from "$lib/domains/security/auth.vm.svelte";
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
Mail,
|
||||
Shield,
|
||||
XCircle,
|
||||
} from "@lucide/svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let verificationState = $state<"loading" | "success" | "error">("loading");
|
||||
let errorMessage = $state<string>("");
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
verificationState = "loading";
|
||||
await authVM.verifyMagicLink();
|
||||
verificationState = "success";
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
verificationState = "error";
|
||||
errorMessage =
|
||||
error instanceof Error ? error.message : "Verification failed";
|
||||
setTimeout(() => {
|
||||
sessionsVM.logout().then();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
function handleRetry() {
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="bg-card/95 border shadow-2xl backdrop-blur-sm">
|
||||
<Card.Content class="p-8">
|
||||
<div class="flex flex-col items-center space-y-6 text-center">
|
||||
{#if verificationState === "loading"}
|
||||
<!-- Header Icon -->
|
||||
<div class="relative">
|
||||
<div
|
||||
class="bg-primary/20 absolute inset-0 animate-pulse rounded-full blur-xl"
|
||||
></div>
|
||||
<div
|
||||
class="bg-primary/10 border-primary/20 relative rounded-full border p-4"
|
||||
>
|
||||
<Icon icon={Shield} cls="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Dynamic Content Based on State -->
|
||||
{#if verificationState === "loading"}
|
||||
<div class="space-y-4">
|
||||
<Title
|
||||
size="h4"
|
||||
weight="semibold"
|
||||
color="theme"
|
||||
center
|
||||
>
|
||||
Verifying Your Login
|
||||
</Title>
|
||||
|
||||
<div
|
||||
class="text-muted-foreground flex items-center justify-center space-x-2"
|
||||
>
|
||||
<Icon icon={Loader2} cls="h-5 w-5 animate-spin" />
|
||||
<span class="text-sm"
|
||||
>Please wait while we verify your magic
|
||||
link...</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Progress Animation -->
|
||||
<div
|
||||
class="bg-muted/30 h-2 w-full overflow-hidden rounded-full"
|
||||
>
|
||||
<div
|
||||
class="bg-primary from-primary/50 to-primary h-full animate-pulse rounded-full bg-gradient-to-r transition-all duration-1000 ease-in-out"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Security Features -->
|
||||
<div class="space-y-3 pt-4">
|
||||
<div
|
||||
class="text-muted-foreground flex items-center space-x-3 text-sm"
|
||||
>
|
||||
<div
|
||||
class="rounded-full border border-emerald-500/20 bg-emerald-500/10 p-1"
|
||||
>
|
||||
<CheckCircle
|
||||
class="h-4 w-4 text-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
<span>Secure encrypted connection</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-muted-foreground flex items-center space-x-3 text-sm"
|
||||
>
|
||||
<div
|
||||
class="rounded-full border border-emerald-500/20 bg-emerald-500/10 p-1"
|
||||
>
|
||||
<CheckCircle
|
||||
class="h-4 w-4 text-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
<span>Verifying your identity</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-muted-foreground flex items-center space-x-3 text-sm"
|
||||
>
|
||||
<div
|
||||
class="bg-primary/10 border-primary/20 rounded-full border p-1"
|
||||
>
|
||||
<Loader2
|
||||
class="text-primary h-4 w-4 animate-spin"
|
||||
/>
|
||||
</div>
|
||||
<span>Processing authentication</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if verificationState === "success"}
|
||||
<div class="grid place-items-center space-y-4">
|
||||
<div class="relative w-max">
|
||||
<div
|
||||
class="absolute inset-0 animate-pulse rounded-full bg-emerald-500/20 blur-lg"
|
||||
></div>
|
||||
<div
|
||||
class="relative rounded-full border border-emerald-500/20 bg-emerald-500/10 p-4"
|
||||
>
|
||||
<Icon
|
||||
icon={CheckCircle}
|
||||
cls="h-8 w-8 text-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Title
|
||||
size="h4"
|
||||
weight="semibold"
|
||||
color="theme"
|
||||
center
|
||||
>
|
||||
Login Successful!
|
||||
</Title>
|
||||
|
||||
<p class="text-muted-foreground text-sm">
|
||||
You've been successfully authenticated.
|
||||
Redirecting you now...
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-center space-x-2 text-sm text-emerald-500"
|
||||
>
|
||||
<span>Redirecting</span>
|
||||
<Icon
|
||||
icon={ArrowRight}
|
||||
cls="h-4 w-4 animate-bounce"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if verificationState === "error"}
|
||||
<div class="grid place-items-center space-y-4">
|
||||
<div class="relative w-max">
|
||||
<div
|
||||
class="bg-destructive/20 absolute inset-0 rounded-full blur-lg"
|
||||
></div>
|
||||
<div
|
||||
class="bg-destructive/10 border-destructive/20 relative rounded-full border p-4"
|
||||
>
|
||||
<Icon
|
||||
icon={XCircle}
|
||||
cls="h-8 w-8 text-destructive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Title
|
||||
size="h4"
|
||||
weight="semibold"
|
||||
color="theme"
|
||||
center
|
||||
>
|
||||
Verification Failed
|
||||
</Title>
|
||||
|
||||
<p class="text-muted-foreground text-center text-sm">
|
||||
{errorMessage ||
|
||||
"We couldn't verify your magic link. It may have expired or been used already."}
|
||||
</p>
|
||||
|
||||
<div class="space-y-3 pt-2">
|
||||
<Button onclick={handleRetry} class="w-full">
|
||||
<Icon icon={Mail} cls="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
|
||||
<p
|
||||
class="text-muted-foreground text-center text-xs"
|
||||
>
|
||||
Need help? Contact our support team if this
|
||||
problem persists.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Footer Note -->
|
||||
<div class="border-border w-full border-t pt-4">
|
||||
<p class="text-muted-foreground text-center text-xs">
|
||||
Magic links are secure, one-time use authentication
|
||||
tokens that expire after 10 minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -10,7 +10,7 @@
|
||||
import { accountVM } from "$lib/domains/account/account.vm.svelte";
|
||||
import { breadcrumbs } from "$lib/global.stores";
|
||||
import AtSign from "@lucide/svelte/icons/at-sign";
|
||||
import Mail from "@lucide/svelte/icons/mail";
|
||||
import KeyRound from "@lucide/svelte/icons/key-round";
|
||||
import Save from "@lucide/svelte/icons/save";
|
||||
import Upload from "@lucide/svelte/icons/upload";
|
||||
import User from "@lucide/svelte/icons/user";
|
||||
@@ -22,14 +22,15 @@
|
||||
|
||||
const user = $state(data.user!);
|
||||
|
||||
// Separate form state for profile and email
|
||||
// Separate form state for profile and password
|
||||
let profileData = $state({
|
||||
name: user.name ?? "",
|
||||
username: user.username ?? "",
|
||||
});
|
||||
|
||||
let emailData = $state({
|
||||
email: user.email,
|
||||
let passwordData = $state({
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
// Handle profile form submission (name, username)
|
||||
@@ -38,11 +39,20 @@
|
||||
await accountVM.updateProfile(profileData);
|
||||
}
|
||||
|
||||
// Handle email form submission
|
||||
async function handleEmailSubmit(e: SubmitEvent) {
|
||||
// Handle password form submission
|
||||
async function handlePasswordSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (user.email !== emailData.email) {
|
||||
await accountVM.changeEmail(emailData.email);
|
||||
if (
|
||||
passwordData.password.length >= 6 &&
|
||||
passwordData.password === passwordData.confirmPassword
|
||||
) {
|
||||
const didChange = await accountVM.changePassword(
|
||||
passwordData.password,
|
||||
);
|
||||
if (didChange) {
|
||||
passwordData.password = "";
|
||||
passwordData.confirmPassword = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,67 +181,72 @@
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Email Settings -->
|
||||
<!-- Password Settings -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Email Settings</Card.Title>
|
||||
<Card.Title>Password Settings</Card.Title>
|
||||
<Card.Description>
|
||||
Manage your email address and verification status.
|
||||
Update your account password.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form onsubmit={handleEmailSubmit} class="space-y-4">
|
||||
<!-- Email -->
|
||||
<form onsubmit={handlePasswordSubmit} class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-1.5">
|
||||
<Label for="email" class="flex items-center gap-1.5">
|
||||
<Label for="password" class="flex items-center gap-1.5">
|
||||
<Icon
|
||||
icon={Mail}
|
||||
icon={KeyRound}
|
||||
cls="h-3.5 w-3.5 text-muted-foreground"
|
||||
/>
|
||||
Email Address
|
||||
New Password
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={emailData.email}
|
||||
placeholder="your.email@example.com"
|
||||
id="password"
|
||||
type="password"
|
||||
bind:value={passwordData.password}
|
||||
placeholder="Enter new password"
|
||||
minlength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-1.5">
|
||||
<Label
|
||||
for="confirm-password"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<Icon
|
||||
icon={KeyRound}
|
||||
cls="h-3.5 w-3.5 text-muted-foreground"
|
||||
/>
|
||||
Confirm Password
|
||||
</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
bind:value={passwordData.confirmPassword}
|
||||
placeholder="Re-enter new password"
|
||||
minlength={6}
|
||||
maxlength={128}
|
||||
/>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
class={user.emailVerified
|
||||
? "text-xs text-emerald-600 dark:text-emerald-400"
|
||||
: "text-xs text-amber-600"}
|
||||
>
|
||||
{user.emailVerified
|
||||
? "Email verified"
|
||||
: "Email not verified"}
|
||||
</span>
|
||||
{#if !user.emailVerified}
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="h-auto p-0 text-xs">Verify now</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={accountVM.emailLoading ||
|
||||
user.email === emailData.email}
|
||||
disabled={accountVM.passwordLoading ||
|
||||
passwordData.password.length < 6 ||
|
||||
passwordData.password !==
|
||||
passwordData.confirmPassword}
|
||||
variant="outline"
|
||||
class="w-full sm:w-auto"
|
||||
>
|
||||
{#if accountVM.emailLoading}
|
||||
<Icon icon={Mail} cls="h-4 w-4 mr-2 animate-spin" />
|
||||
Sending...
|
||||
{#if accountVM.passwordLoading}
|
||||
<Icon
|
||||
icon={KeyRound}
|
||||
cls="h-4 w-4 mr-2 animate-spin"
|
||||
/>
|
||||
Updating...
|
||||
{:else}
|
||||
<Icon icon={Mail} cls="h-4 w-4 mr-2" />
|
||||
Update Email
|
||||
<Icon icon={KeyRound} cls="h-4 w-4 mr-2" />
|
||||
Update Password
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -239,8 +254,7 @@
|
||||
</Card.Content>
|
||||
<Card.Footer>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
Changing your email will require verification of the new
|
||||
address.
|
||||
Choose a strong password with at least 6 characters.
|
||||
</p>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { authVM } from "$lib/domains/security/auth.vm.svelte";
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
Mail,
|
||||
User,
|
||||
XCircle,
|
||||
} from "@lucide/svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let verificationState = $state<"loading" | "success" | "error">("loading");
|
||||
let errorMessage = $state<string>("");
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
verificationState = "loading";
|
||||
await authVM.verifyEmailChange();
|
||||
verificationState = "success";
|
||||
setTimeout(() => {
|
||||
window.location.href = "/account";
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
verificationState = "error";
|
||||
errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Email verification failed";
|
||||
}
|
||||
});
|
||||
|
||||
function handleReturnToAccount() {
|
||||
window.location.href = "/account";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="from-muted/20 via-background to-accent/10 flex min-h-screen items-center justify-center bg-linear-to-br p-4"
|
||||
>
|
||||
<div class="w-full max-w-md">
|
||||
<Card.Root class="bg-card/95 border shadow-2xl backdrop-blur-sm">
|
||||
<Card.Content class="p-8">
|
||||
<div class="flex flex-col items-center space-y-6 text-center">
|
||||
{#if verificationState === "loading"}
|
||||
<!-- Header Icon -->
|
||||
<div class="relative">
|
||||
<div
|
||||
class="bg-primary/20 absolute inset-0 animate-pulse rounded-full blur-xl"
|
||||
></div>
|
||||
<div
|
||||
class="bg-primary/10 border-primary/20 relative rounded-full border p-4"
|
||||
>
|
||||
<Icon icon={Mail} cls="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Dynamic Content Based on State -->
|
||||
{#if verificationState === "loading"}
|
||||
<div class="space-y-4">
|
||||
<Title
|
||||
size="h4"
|
||||
weight="semibold"
|
||||
color="theme"
|
||||
center
|
||||
>
|
||||
Verifying Your Email
|
||||
</Title>
|
||||
|
||||
<div
|
||||
class="text-muted-foreground flex items-center justify-center space-x-2"
|
||||
>
|
||||
<Icon
|
||||
icon={Loader2}
|
||||
cls="h-5 w-5 animate-spin"
|
||||
/>
|
||||
<span class="text-sm"
|
||||
>Please wait while we verify your email
|
||||
change...</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Progress Animation -->
|
||||
<div
|
||||
class="bg-muted/30 h-2 w-full overflow-hidden rounded-full"
|
||||
>
|
||||
<div
|
||||
class="bg-primary from-primary/50 to-primary h-full animate-pulse rounded-full bg-linear-to-r transition-all duration-1000 ease-in-out"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Security Features -->
|
||||
<div class="space-y-3 pt-4">
|
||||
<div
|
||||
class="text-muted-foreground flex items-center space-x-3 text-sm"
|
||||
>
|
||||
<div
|
||||
class="rounded-full border border-emerald-500/20 bg-emerald-500/10 p-1"
|
||||
>
|
||||
<CheckCircle
|
||||
class="h-4 w-4 text-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
<span>Secure encrypted connection</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-muted-foreground flex items-center space-x-3 text-sm"
|
||||
>
|
||||
<div
|
||||
class="rounded-full border border-emerald-500/20 bg-emerald-500/10 p-1"
|
||||
>
|
||||
<CheckCircle
|
||||
class="h-4 w-4 text-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
<span>Validating email ownership</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-muted-foreground flex items-center space-x-3 text-sm"
|
||||
>
|
||||
<div
|
||||
class="bg-primary/10 border-primary/20 rounded-full border p-1"
|
||||
>
|
||||
<Loader2
|
||||
class="text-primary h-4 w-4 animate-spin"
|
||||
/>
|
||||
</div>
|
||||
<span>Updating account settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if verificationState === "success"}
|
||||
<div class="grid place-items-center space-y-4">
|
||||
<div class="relative w-max">
|
||||
<div
|
||||
class="absolute inset-0 animate-pulse rounded-full bg-emerald-500/20 blur-lg"
|
||||
></div>
|
||||
<div
|
||||
class="relative rounded-full border border-emerald-500/20 bg-emerald-500/10 p-4"
|
||||
>
|
||||
<Icon
|
||||
icon={CheckCircle}
|
||||
cls="h-8 w-8 text-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Title
|
||||
size="h4"
|
||||
weight="semibold"
|
||||
color="theme"
|
||||
center
|
||||
>
|
||||
Email Verified Successfully!
|
||||
</Title>
|
||||
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Your email address has been updated and
|
||||
verified. Redirecting you to your account...
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-center space-x-2 text-sm text-emerald-500"
|
||||
>
|
||||
<span>Redirecting</span>
|
||||
<Icon
|
||||
icon={ArrowRight}
|
||||
cls="h-4 w-4 animate-bounce"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if verificationState === "error"}
|
||||
<div class="grid place-items-center space-y-4">
|
||||
<div class="relative w-max">
|
||||
<div
|
||||
class="bg-destructive/20 absolute inset-0 rounded-full blur-lg"
|
||||
></div>
|
||||
<div
|
||||
class="bg-destructive/10 border-destructive/20 relative rounded-full border p-4"
|
||||
>
|
||||
<Icon
|
||||
icon={XCircle}
|
||||
cls="h-8 w-8 text-destructive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Title
|
||||
size="h4"
|
||||
weight="semibold"
|
||||
color="theme"
|
||||
center
|
||||
>
|
||||
Email Verification Failed
|
||||
</Title>
|
||||
|
||||
<p
|
||||
class="text-muted-foreground text-center text-sm"
|
||||
>
|
||||
{errorMessage ||
|
||||
"We couldn't verify your email change. The verification link may have expired or been used already."}
|
||||
</p>
|
||||
|
||||
<div class="space-y-3 pt-2">
|
||||
<Button
|
||||
onclick={handleReturnToAccount}
|
||||
class="w-full"
|
||||
>
|
||||
<Icon icon={User} cls="h-4 w-4 mr-2" />
|
||||
Return to Account
|
||||
</Button>
|
||||
|
||||
<p
|
||||
class="text-muted-foreground text-center text-xs"
|
||||
>
|
||||
You can try changing your email again from
|
||||
your account settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Footer Note -->
|
||||
<div class="border-border w-full border-t pt-4">
|
||||
<p class="text-muted-foreground text-center text-xs">
|
||||
Email verification links are secure, one-time use
|
||||
tokens that expire after 10 minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,10 @@
|
||||
import { UserRoleMap } from "@pkg/logic/domains/user/data";
|
||||
import { user } from "@pkg/db/schema/better.auth.schema";
|
||||
import { auth } from "@pkg/logic/domains/auth/config.base";
|
||||
import { account, user } from "@pkg/db/schema/better.auth.schema";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { settings } from "@pkg/settings";
|
||||
import { logger } from "@pkg/logger";
|
||||
import { db, eq } from "@pkg/db";
|
||||
import { db, eq, or } from "@pkg/db";
|
||||
import { nanoid } from "nanoid";
|
||||
import * as v from "valibot";
|
||||
|
||||
@@ -48,45 +49,81 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
const data = await request.json();
|
||||
const _schema = v.object({
|
||||
username: v.string(),
|
||||
email: v.string(),
|
||||
usertype: v.enum(UserRoleMap),
|
||||
password: v.pipe(v.string(), v.minLength(6)),
|
||||
email: v.optional(v.string()),
|
||||
usertype: v.optional(v.enum(UserRoleMap)),
|
||||
});
|
||||
const res = v.safeParse(_schema, data);
|
||||
if (!res.success) {
|
||||
return new Response("Invalid data", { status: 400 });
|
||||
}
|
||||
const resData = res.output;
|
||||
const email = resData.email ?? `${resData.username}@debug.local`;
|
||||
const usertype = resData.usertype ?? UserRoleMap.admin;
|
||||
|
||||
if (
|
||||
!!(
|
||||
await db.query.user
|
||||
.findFirst({
|
||||
where: eq(user.role, UserRoleMap.user),
|
||||
columns: { id: true },
|
||||
})
|
||||
.execute()
|
||||
)?.id
|
||||
) {
|
||||
return new Response("Admin already exists", { status: 400 });
|
||||
const existingUser = await db.query.user
|
||||
.findFirst({
|
||||
where: or(eq(user.username, resData.username), eq(user.email, email)),
|
||||
columns: { id: true },
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (existingUser?.id) {
|
||||
return new Response("User already exists", { status: 409 });
|
||||
}
|
||||
|
||||
const resData = res.output;
|
||||
|
||||
logger.info(
|
||||
`Creating ${resData.username} | ${resData.email} (${resData.usertype})`,
|
||||
`Creating debug user ${resData.username} | ${email} (${usertype})`,
|
||||
);
|
||||
|
||||
const out = await db.insert(user).values({
|
||||
id: nanoid(),
|
||||
username: resData.username,
|
||||
email: resData.email,
|
||||
emailVerified: false,
|
||||
name: resData.username,
|
||||
role: resData.usertype,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
const userId = nanoid();
|
||||
const accountId = nanoid();
|
||||
const hashedPassword = await auth.$context.then((ctx) =>
|
||||
ctx.password.hash(resData.password),
|
||||
);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.insert(user)
|
||||
.values({
|
||||
id: userId,
|
||||
username: resData.username,
|
||||
email,
|
||||
emailVerified: true,
|
||||
name: resData.username,
|
||||
role: usertype,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.execute();
|
||||
|
||||
await tx
|
||||
.insert(account)
|
||||
.values({
|
||||
id: accountId,
|
||||
accountId: userId,
|
||||
providerId: "credential",
|
||||
userId,
|
||||
password: hashedPassword,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
|
||||
logger.debug(out);
|
||||
logger.info("Debug user created with credential account", {
|
||||
userId,
|
||||
username: resData.username,
|
||||
email,
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ ...out }), { status: 200 });
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: userId,
|
||||
username: resData.username,
|
||||
email,
|
||||
role: usertype,
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</Title>
|
||||
</Card.Title>
|
||||
<Card.Description class="text-muted-foreground">
|
||||
Sign in to your account to continue
|
||||
Sign in with your username and password
|
||||
</Card.Description>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,12 +54,6 @@
|
||||
|
||||
<Card.Content class="px-8 pb-8">
|
||||
<EmailLoginForm />
|
||||
|
||||
<div class="w-full flex gap-4 items-center justify-center py-4">
|
||||
<div class="h-0.5 w-full bg-muted-foreground"></div>
|
||||
<p class="text-lg">OR</p>
|
||||
<div class="h-0.5 w-full bg-muted-foreground"></div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
|
||||
<Card.Footer class="flex items-center justify-between">
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MagicLinkVerification from "$lib/domains/security/magic-link-verification.svelte";
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="from-muted/20 via-background to-accent/10 flex min-h-screen items-center justify-center bg-gradient-to-br p-4"
|
||||
>
|
||||
<div class="w-full max-w-md">
|
||||
<MagicLinkVerification />
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user