oops i forgot to commit as I go again

This commit is contained in:
2025-01-24 17:18:04 -06:00
parent a09e8245c0
commit 567aa4cb71
35 changed files with 1455 additions and 37 deletions
+10
View File
@@ -0,0 +1,10 @@
<script>
import { page } from '$app/stores';
</script>
<div class="flex flex-riow gap-4">
{#if $page.error}
<span class="text-red-500 font-bold">{$page.status}:</span>
<p>{$page.error.message}</p>
{/if}
</div>
+6 -1
View File
@@ -1,6 +1,11 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
import { ModeWatcher } from 'mode-watcher';
</script>
{@render children()}
<ModeWatcher />
<div class="w-screen h-screen flex flex-col items-center justify-center">
{@render children()}
</div>
+77
View File
@@ -0,0 +1,77 @@
// src/routes/api/login/+server.ts
import { prisma } from '$lib/prisma';
import bcrypt from 'bcryptjs';
import { randomBytes } from 'crypto'; // For generating a random session ID
import { z } from 'zod';
import { json, type RequestEvent } from '@sveltejs/kit';
import { serialize } from 'cookie'; // For cookie serialization
// Define the schema for validation using Zod
const loginSchema = z.object({
email: z.string().email().min(5, 'Email is required'),
password: z.string().min(8, 'Password must be at least 8 characters')
});
export const POST = async ({ request }: RequestEvent) => {
try {
// Parse the request body
const formData = await request.formData();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
// Validate the incoming data
const parsed = loginSchema.safeParse({ email, password });
if (!parsed.success) {
return json({ message: parsed.error.errors[0].message }, { status: 400 });
}
// Find the user in the database by email
const user = await prisma.user.findUnique({
where: { email }
});
if (!user) {
return json({ message: 'Invalid email or password' }, { status: 400 });
}
// Compare the provided password with the hashed password in the database
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
if (!isPasswordValid) {
return json({ message: 'Invalid email or password' }, { status: 400 });
}
// Generate a secure random session ID using crypto
const sessionId = randomBytes(16).toString('hex'); // 64-byte hex session ID
// Create a new session in the database
await prisma.session.create({
data: {
id: sessionId, // Session ID is generated randomly
userId: user.id // Associate session with user
}
});
// Set the session ID in an HttpOnly cookie
const cookie = serialize('session_id', sessionId, {
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
secure: process.env.NODE_ENV === 'production', // Set secure flag in production
maxAge: 60 * 60, // 1 hour expiry
path: '/' // Make the cookie available throughout the site
});
// Respond with the session token
return json(
{ message: 'Login successful' },
{
status: 200,
headers: { 'Set-Cookie': cookie } // Set cookie header
}
);
} catch (error) {
console.error(error);
return json({ message: 'Internal server error' }, { status: 500 });
}
};
+42
View File
@@ -0,0 +1,42 @@
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const GET = async ({ cookies }: RequestEvent) => {
// Retrieve the session_id cookie
const sessionId = cookies.get('session_id');
// If there's no session_id cookie, return an empty object
if (!sessionId) {
return json({ message: 'No session cookie!' }, { status: 500 });
}
try {
// Find the session and include the associated user
const session = await prisma.session.findUnique({
where: { id: sessionId },
include: { user: { include: { roles: true } } }
});
// If no session is found, return an empty object
if (!session) {
return json({ message: 'Session doesnt exist!' }, { status: 500 });
}
// Sanitize the user data by removing the passwordHash and sanitizing the session
const sanitizedUser = {
...session.user,
passwordHash: undefined, // Remove the password hash
sessions: undefined // Remove sessions if included in the model
};
// Return the sanitized user data
return json(sanitizedUser);
} catch (error) {
// Log the error and return an empty object
console.error('Error fetching session:', error);
return json({ message: 'Internal server error' }, { status: 500 });
}
};
+74
View File
@@ -0,0 +1,74 @@
import { prisma } from '$lib/prisma'; // Your Prisma client instance
import bcrypt from 'bcryptjs';
import { z } from 'zod';
import { type RequestHandler } from '@sveltejs/kit';
// Define the schema for validation using Zod
const registerSchema = z.object({
email: z.string().email().min(5, 'Email is required'),
password: z.string().min(8, 'Password must be at least 8 characters'),
firstName: z.string().optional(),
lastName: z.string().optional()
});
// Define the POST handler with explicit typing for the request
export const POST: RequestHandler = async ({ request }) => {
try {
// Parse form data
const formData = await request.formData();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
// Validate the incoming data
const parsed = registerSchema.safeParse({
email,
password,
firstName: formData.get('firstName') as string | null,
lastName: formData.get('lastName') as string | null
});
if (!parsed.success) {
return new Response(JSON.stringify({ message: parsed.error.errors[0].message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Check if the email already exists
const existingUser = await prisma.user.findUnique({
where: { email }
});
if (existingUser) {
return new Response(JSON.stringify({ message: 'Email already registered' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Hash the password before storing it
const hashedPassword = await bcrypt.hash(password, 10);
// Create the new user in the database
await prisma.user.create({
data: {
email,
passwordHash: hashedPassword,
firstName: parsed.data.firstName || undefined, // Store firstName if provided
lastName: parsed.data.lastName || undefined // Store lastName if provided
}
});
// Redirect to the login page
return new Response(JSON.stringify({ message: 'Successfully created an account' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
// Handle unexpected errors
return new Response(JSON.stringify({ message: 'Internal server error', error: error }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};
+56
View File
@@ -0,0 +1,56 @@
import { prisma } from '$lib/prisma';
import { parse } from 'cookie';
import { redirect, error } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
export const load: ServerLoad = async ({ request, url }) => {
const cookies = parse(request.headers.get('cookie') || '');
const sessionId = cookies['session_id'];
const redirectURL = url.searchParams.get('redirect');
if (!redirectURL) throw error(400, 'Invalid redirect domain');
if (sessionId) {
// Validate the session by checking if the session ID exists in the database
const session = await prisma.session.findUnique({
where: { id: sessionId },
include: { user: true }
});
if (session) {
// Session is valid, check if there's a redirectURL query parameter
if (redirectURL) {
let redirectWithToken;
try {
redirectWithToken = new URL(redirectURL);
// Extract the domain from the redirect URL
const redirectDomain = redirectWithToken.hostname;
// Check if an application exists with the provided domain
const app = await prisma.app.findFirst({
where: { domain: redirectDomain }
});
if (!app) {
// If no application with the domain exists, throw an error
throw error(400, 'Invalid redirect domain');
}
// Append the session ID or token to the redirect URL
redirectWithToken.searchParams.append('token', session.id);
// Perform the redirect with the session token attached
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
// Handle malformed URL or other errors
throw error(400, 'Invalid redirect domain!');
}
redirect(302, redirectWithToken.toString());
}
}
}
// If no session exists, allow the user to stay on the login page or show the login form
return {};
};
+72
View File
@@ -0,0 +1,72 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
let redirectUrl: string | null = page.url.searchParams.get('redirect');
let email = '';
let password = '';
let successMessage: string | null = null;
let errorMessage: string | null = null;
const login = async (event: Event) => {
event.preventDefault();
successMessage = null;
errorMessage = null;
try {
// Send login request
const response = await fetch('/api/login', {
method: 'POST',
body: new URLSearchParams({ email, password })
});
if (response.ok) {
successMessage = 'Login successful! Redirecting...';
setTimeout(() => {
location.reload(); // Reload the page to reflect the logged-in state
}, 2000); // Add slight delay to show success message
} else {
errorMessage = 'Login failed: Invalid email or password.';
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
errorMessage = 'An error occurred while trying to log in. Please try again later.';
}
};
</script>
<Card.Root class="w-[350px]">
<form on:submit={login}>
<Card.Header>
<Card.Title>Login to Account</Card.Title>
<Card.Description>Please enter your credentials below to login.</Card.Description>
<Card.Description>You will be redirected to .</Card.Description>
</Card.Header>
<Card.Content>
<!-- Display success or error message -->
{#if successMessage}
<p class="text-green-500 mb-4">{successMessage}</p>
{:else if errorMessage}
<p class="text-red-500 mb-4">{errorMessage}</p>
{/if}
<Label for="email">Email:</Label>
<Input id="email" type="email" bind:value={email} required />
<Label for="password">Password:</Label>
<Input id="password" type="password" bind:value={password} required />
<!-- Submit button within the form -->
</Card.Content>
<Card.Footer class="flex justify-between">
<Button variant="outline" on:click={() => goto(`/register?redirect=${redirectUrl}`)}
>Register</Button
>
<Button type="submit">Login</Button>
</Card.Footer>
</form>
</Card.Root>
+13
View File
@@ -0,0 +1,13 @@
import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
export const GET = async ({ cookies, url }: RequestEvent) => {
// Delete the cookie
cookies.delete('session_id', { path: '/' });
// Get the redirect URL from the query parameter
const redirectUrl = url.searchParams.get('redirect') || 'https://rummage.cc';
// Redirect to the specified URL
throw redirect(302, redirectUrl);
};
+84
View File
@@ -0,0 +1,84 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index';
import * as Card from '$lib/components/ui/card/index';
import { Input } from '$lib/components/ui/input/index';
import { Label } from '$lib/components/ui/label/index';
import { page } from '$app/state';
import { goto } from '$app/navigation'; // Import for redirecting to the login page
let redirectUrl: string | null = page.url.searchParams.get('redirect');
let email = '';
let password = '';
let firstName = '';
let lastName = '';
let errorMessage = '';
let successMessage = '';
// Form submission handler
async function submitForm() {
// Reset messages
errorMessage = '';
successMessage = '';
const formData = new FormData();
formData.append('email', email);
formData.append('password', password);
formData.append('firstName', firstName);
formData.append('lastName', lastName);
try {
const response = await fetch('/api/register', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
successMessage = data.message;
setTimeout(() => {
goto(`/login?redirect=${redirectUrl}`); // Redirect to login page on success
}, 1000); // Delay redirect to show success message
} else {
errorMessage = data.message;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
errorMessage = 'An error occurred. Please try again.';
}
}
</script>
<Card.Root class="w-[350px]">
<form on:submit|preventDefault={submitForm}>
<Card.Header>
<Card.Title>Register Account</Card.Title>
<Card.Description>Please fill in the details below to create an account.</Card.Description>
</Card.Header>
<Card.Content>
{#if successMessage}
<p class="text-green-500">{successMessage}</p>
{:else if errorMessage}
<p class="text-red-500">{errorMessage}</p>
{/if}
<Label for="email">Email:</Label>
<Input id="email" type="email" bind:value={email} required />
<Label for="password">Password:</Label>
<Input id="password" type="password" bind:value={password} required />
<Label for="firstName">First Name:</Label>
<Input id="firstName" type="text" bind:value={firstName} />
<Label for="lastName">Last Name:</Label>
<Input id="lastName" type="text" bind:value={lastName} />
</Card.Content>
<Card.Footer class="flex justify-between">
<Button variant="outline" on:click={() => goto(`/login?redirect=${redirectUrl}`)}
>Login</Button
>
<Button type="submit">Register</Button>
</Card.Footer>
</form>
</Card.Root>