oops i forgot to commit as I go again
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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' }
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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 {};
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user