login now username+password based

This commit is contained in:
user
2026-02-28 19:39:21 +02:00
parent 0265d6e774
commit cf8d72cac3
14 changed files with 245 additions and 1128 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 },
);
};

View File

@@ -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">

View File

@@ -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>