feat: enhance user signup process with password hashing and session management

This commit is contained in:
2025-12-01 21:42:47 +01:00
parent 76041eb7f8
commit c61129c828
3 changed files with 89 additions and 15 deletions

View File

@@ -5,6 +5,7 @@ export const userTable = pgTable('user', {
name: text('name').notNull(), name: text('name').notNull(),
email: text('email').notNull().unique(), email: text('email').notNull().unique(),
password: text('password').notNull(), password: text('password').notNull(),
salt: text('salt').notNull(),
created_at: timestamp('created_at').defaultNow() created_at: timestamp('created_at').defaultNow()
}); });

View File

@@ -1,22 +1,86 @@
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { userTable } from '$lib/server/db/schema'; import { userTable, sessionTable } from '$lib/server/db/schema';
import { z } from 'zod';
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types'; import type { Actions } from './$types';
import { randomBytes, scrypt } from 'crypto';
const userSchema = z.object({
name: z.string().min(3, "Username must be at least 3 characters long").max(32, "Username must be at most 32 characters long"),
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters long")
});
export const _hash = async (password: string, keyLength = 32): Promise<{ hash: string; salt: string }> => {
return new Promise((resolve, reject) => {
const salt = randomBytes(16).toString("hex");
scrypt(password, salt, keyLength, (err, derivedKey) => {
if (err) reject(err);
resolve({ hash: derivedKey.toString("hex"), salt });
});
});
};
export const actions = { export const actions = {
default: async ({ request }) => { default: async ({ request, cookies }) => {
const formData = await request.formData(); const formData = await request.formData();
const name = formData.get('name'); const name = formData.get('name')?.toString();
const email = formData.get('email'); const email = formData.get('email')?.toString();
const password = formData.get('password'); const password = formData.get('password')?.toString();
// TODO: Implement data validation. const parseResult = userSchema.safeParse({ name, email, password });
const userRecord = await db.insert(userTable).values({ if (!parseResult.success) {
name: name as string, return fail(400, {
email: email as string, data: { name, email },
password: password as string errors: z.flattenError(parseResult.error).fieldErrors
}).returning(); });
}
// TODO: Handle post-signup logic (e.g., redirect, session creation). try {
} const hashedpw = await _hash(parseResult.data.password);
const newUser = await db.insert(userTable).values({
name: parseResult.data.name,
email: parseResult.data.email,
password: hashedpw.hash,
salt: hashedpw.salt
}).returning();
const sessionToken = randomBytes(32).toString("hex");
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
await db.insert(sessionTable).values({
user_id: newUser[0].id,
token: sessionToken,
expires_at: expiresAt
});
cookies.set('session', sessionToken, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
expires: expiresAt
});
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === '23505') {
return fail(400, {
data: { name, email },
errors: {
email: ["Email is already in use"]
}
});
}
console.error("Unexpected error during user registration:", error);
return fail(500, {
message: "An unexpected error occurred."
});
}
throw redirect(303, '/dashboard');
}
} satisfies Actions; } satisfies Actions;

View File

@@ -1,3 +1,12 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageProps } from './$types';
let { form }: PageProps = $props();
/* ... */
</script>
<form method="post"> <form method="post">
<input type="text" name="name" placeholder="Name" required /> <input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required /> <input type="email" name="email" placeholder="Email" required />