Implement user authentication with email and password, update forms and schemas
All checks were successful
main-branch-frovide/pipeline/head This commit looks good
All checks were successful
main-branch-frovide/pipeline/head This commit looks good
This commit is contained in:
parent
48c956dae4
commit
3a2b4ded8c
@ -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)
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
5
src/routes/register/+layout.svelte
Normal file
5
src/routes/register/+layout.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
67
src/routes/register/+page.server.ts
Normal file
67
src/routes/register/+page.server.ts
Normal 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');
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
10
src/routes/register/+page.svelte
Normal file
10
src/routes/register/+page.svelte
Normal 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>
|
||||
92
src/routes/register/register-form.svelte
Normal file
92
src/routes/register/register-form.svelte
Normal 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>
|
||||
25
src/routes/register/schema.ts
Normal file
25
src/routes/register/schema.ts
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user