Implement user authentication with email and password, update forms and schemas
All checks were successful
main-branch-frovide/pipeline/head This commit looks good

This commit is contained in:
pd0a6847 2025-06-13 12:00:21 +09:00
parent 48c956dae4
commit 3a2b4ded8c
12 changed files with 252 additions and 54 deletions

View File

@ -33,7 +33,7 @@ export async function validateSessionToken(token: string) {
const [result] = await db
.select({
// Adjust user table here to tweak returned data
user: { id: table.user.id, username: table.user.username },
user: { id: table.user.id, email: table.user.email },
session: table.session
})
.from(table.session)

View File

@ -1,13 +1,13 @@
import { pgTable, serial, integer, text, timestamp } from 'drizzle-orm/pg-core';
import {pgTable, serial, integer, text, timestamp} from 'drizzle-orm/pg-core';
export const user = pgTable('user', {
id: text('id').primaryKey(),
age: integer('age'), //나이
username: text('username').notNull().unique(), // username 이 사용자가 회원가입 할때 id 임
username: text('username'),
nickname: text('nickname'), // 별칭
helloword: text('helloword'), // 인사말, 소개말
avatar: text('avatar'), // 아바타 파일경로
email: text('email'), // 이메일주소
email: text('email').notNull().unique(), // 이메일주소 이것을 아이디로 사용하도록 변경하자
gender: text('gender'), // 성별
passwordHash: text('password_hash').notNull(),
createdAt: timestamp(),
@ -17,7 +17,7 @@ export const user = pgTable('user', {
export const session = pgTable('session', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => user.id),
expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'date' }).notNull(),
expiresAt: timestamp('expires_at', {withTimezone: true, mode: 'date'}).notNull(),
createdAt: timestamp(),
updatedAt: timestamp(),
});

View File

@ -1,7 +1,7 @@
<script lang="ts">
import '../app.css';
import { Toaster } from "$lib/components/ui/sonner/index.js";
let { children } = $props();
</script>
<Toaster />
{@render children()}

View File

@ -30,17 +30,17 @@ export const actions: Actions = {
});
}
const userId = form.data.userId;
const email = form.data.email;
const password = form.data.password;
const results = await db
.select()
.from(table.user)
.where(eq(table.user.username, userId));
.where(eq(table.user.email, email));
const existingUser = results.at(0);
if (!existingUser) {
return setError(form, 'userId', '존재하지 않는 아이디 입니다.');
return setError(form, 'email', '등록된 이메일이 아닙니다.');
}
const validPassword = await verify(existingUser.passwordHash, password, {

View File

@ -1,10 +1,11 @@
<script lang="ts">
import type { PageData } from "../../../.svelte-kit/types/src/routes";
import type {PageData} from "./$types.js";
import LoginForm from "./login-form.svelte";
let { data }: { data: PageData } = $props();
let {data}: { data: PageData } = $props();
</script>
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-6">
<LoginForm {data} />
<LoginForm {data}/>
</div>
</div>

View File

@ -26,7 +26,7 @@
</script>
<div class={cn("flex flex-col gap-6")}>
<div class="flex flex-col gap-6">
<Card.Root>
<Card.Header class="text-center">
<Card.Title class="text-xl font-semibold">LOGIN</Card.Title>
@ -36,11 +36,11 @@
<div class="grid gap-6">
<div class="grid gap-6">
<div class="grid gap-3">
<Form.Field {form} name="userId">
<Form.Field {form} name="email">
<Form.Control>
{#snippet children({props})}
<Form.Label>UserId</Form.Label>
<Input {...props} bind:value={$formData.userId}/>
<Form.Label>Email</Form.Label>
<Input {...props} bind:value={$formData.email}/>
{/snippet}
</Form.Control>
<Form.FieldErrors/>
@ -50,7 +50,7 @@
<Form.Field {form} name="password">
<Form.Control>
{#snippet children({props})}
<Form.Label>password</Form.Label>
<Form.Label>Password</Form.Label>
<Input
{...props}
bind:value={$formData.password}
@ -69,7 +69,7 @@
</div>
<div class="text-center text-sm">
이메일 인증을 통한
<a href="##" class="underline underline-offset-4"> 회원가입</a>
<a href="/register" class="underline underline-offset-4"> 회원가입</a>
</div>
</div>
</form>

View File

@ -2,10 +2,8 @@ import { z } from "zod";
export const formSchema = z.object({
// User ID (formerly 'user')
userId: z.string()
.min(3, "최소 3자 이상이어야 합니다.") // A common minimum for IDs
.max(50, "최대 50자 이하이어야 합니다.") // Reasonable maximum length
.regex(/^[a-zA-Z0-9_.-]+$/, "영문, 숫자, '_', '.', '-'만 포함할 수 있습니다.") // Restrict characters
email: z.string()
.email("이메일주소 형식이 아닙니다.")
.trim(), // Remove leading/trailing whitespace
// Password

View File

@ -0,0 +1,5 @@
<script lang="ts">
let { children } = $props();
</script>
{@render children()}

View File

@ -0,0 +1,67 @@
import {hash, verify} from '@node-rs/argon2';
import {encodeBase32LowerCase} from '@oslojs/encoding';
import {redirect} from '@sveltejs/kit';
import {eq} from 'drizzle-orm';
import * as auth from '$lib/server/auth';
import {db} from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import type {Actions, PageServerLoad} from './$types';
import { setError, superValidate , fail} from 'sveltekit-superforms';
import {zod} from "sveltekit-superforms/adapters";
import {formSchema} from "./schema";
export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
return redirect(302, '/demo/lucia');
}
return {
form: await superValidate(zod(formSchema)),
};
};
export const actions: Actions = {
login: async (event) => {
const form = await superValidate(event, zod(formSchema));
if (!form.valid) {
return fail(400, {
form,
});
}
const email = form.data.email;
const password = form.data.password;
const results = await db
.select()
.from(table.user)
.where(eq(table.user.email, email));
const existingUser = results.at(0);
if (!existingUser) {
return setError(form, 'email', '등록된 이메일이 아닙니다.');
}
const validPassword = await verify(existingUser.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
if (!validPassword) {
return setError(form, 'password', '비밀번호가 일치하지 않습니다.');
}
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, existingUser.id);
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
return redirect(302, '/demo/lucia');
},
};

View File

@ -0,0 +1,10 @@
<script lang="ts">
import type { PageData } from "./$types.js";
import RegisterForm from "./register-form.svelte";
let { data }: { data: PageData } = $props();
</script>
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-6">
<RegisterForm {data} />
</div>
</div>

View File

@ -0,0 +1,92 @@
<script lang="ts">
import * as Form from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import * as Card from "@/components/ui/card";
import {formSchema, type FormSchema} from "./schema";
import {
type SuperValidated,
type Infer,
superForm,
} from "sveltekit-superforms";
import {zodClient} from "sveltekit-superforms/adapters";
import { dev } from '$app/environment';
import {Label} from "@/components/ui/label";
import {Switch} from "@/components/ui/switch";
import {cn} from "@/utils.js";
import SuperDebug from 'sveltekit-superforms';
let {data}: { data: { form: SuperValidated<Infer<FormSchema>> } } =
$props();
const form = superForm(data.form, {
validators: zodClient(formSchema),
});
const {form: formData, enhance} = form;
</script>
<div class="flex flex-col gap-6">
<Card.Root>
<Card.Header class="text-center">
<Card.Title class="text-xl font-semibold">회원가입</Card.Title>
<Card.Description class="text-sm text-muted-foreground">
Register a new account
</Card.Description>
</Card.Header>
<Card.Content>
<form method="POST" action="?/register" use:enhance>
<div class="grid gap-6">
<div class="grid gap-6">
<div class="grid gap-3">
<Form.Field {form} name="email">
<Form.Control>
{#snippet children({props})}
<Form.Label>이메일</Form.Label>
<Input {...props} bind:value={$formData.email}/>
{/snippet}
</Form.Control>
<Form.FieldErrors/>
</Form.Field>
</div>
<div class="grid gap-3">
<Form.Field {form} name="password">
<Form.Control>
{#snippet children({props})}
<Form.Label>비밀번호</Form.Label>
<Input
{...props}
bind:value={$formData.password}
type="password"
/>
{/snippet}
</Form.Control>
<Form.FieldErrors/>
</Form.Field>
<Form.Field {form} name="passwordConfirm">
<Form.Control>
{#snippet children({props})}
<Form.Label>비밀번호 확인</Form.Label>
<Input
{...props}
bind:value={$formData.passwordConfirm}
type="password"
/>
{/snippet}
</Form.Control>
<Form.FieldErrors/>
</Form.Field>
</div>
<Form.Button class="w-full">회원가입</Form.Button>
</div>
<div class="text-center text-sm">
<a href="/login" class="underline underline-offset-4"> 로그인</a>
</div>
</div>
</form>
</Card.Content>
</Card.Root>
{#if dev}
<SuperDebug data={formData}/>
{/if}
</div>

View File

@ -0,0 +1,25 @@
import { z } from "zod";
import {integer, text, timestamp} from "drizzle-orm/pg-core";
export const formSchema = z.object({
// User ID (formerly 'user')
email: z.string()
.email("이메일주소 형식이 아닙니다.")
.trim(), // Remove leading/trailing whitespace
// Password
password: z.string()
.min(8, "최소 8자 이상이어야 합니다.")
.max(100, "최대 100자 이하이어야 합니다.") // Reasonable maximum length
.regex(/[a-z]/, "최소 하나의 소문자를 포함해야 합니다.")
.regex(/[A-Z]/, "최소 하나의 대문자를 포함해야 합니다.")
.regex(/[0-9]/, "최소 하나의 숫자를 포함해야 합니다.")
.regex(/[^a-zA-Z0-9]/, "최소 하나의 특수문자를 포함해야 합니다.") // For common special characters
.trim(),
passwordConfirm: z.string(),
}).refine((data) => data.password === data.passwordConfirm, {
message: "비밀번호가 일치하지 않습니다",
path: ["passwordConfirm"], // passwordConfirm 필드에 에러를 표시
});
export type FormSchema = typeof formSchema;