login now username+password based
This commit is contained in:
@@ -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