oops i forgot to commit as I go again
This commit is contained in:
+78
-3
@@ -1,3 +1,78 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { Button as ButtonPrimitive } from "bits-ui";
|
||||
import { type Events, type Props, buttonVariants } from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = Props;
|
||||
type $$Events = Events;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let variant: $$Props["variant"] = "default";
|
||||
export let size: $$Props["size"] = "default";
|
||||
export let builders: $$Props["builders"] = [];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<ButtonPrimitive.Root
|
||||
{builders}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
type="button"
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</ButtonPrimitive.Root>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
import type { Button as ButtonPrimitive } from "bits-ui";
|
||||
import Root from "./button.svelte";
|
||||
|
||||
const buttonVariants = tv({
|
||||
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
type Variant = VariantProps<typeof buttonVariants>["variant"];
|
||||
type Size = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
type Props = ButtonPrimitive.Props & {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
};
|
||||
|
||||
type Events = ButtonPrimitive.Events;
|
||||
|
||||
export {
|
||||
Root,
|
||||
type Props,
|
||||
type Events,
|
||||
//
|
||||
Root as Button,
|
||||
type Props as ButtonProps,
|
||||
type Events as ButtonEvents,
|
||||
buttonVariants,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn("p-6", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLParagraphElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<p class={cn("text-muted-foreground text-sm", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn("flex items-center p-6 pt-0", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { HeadingLevel } from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||
tag?: HeadingLevel;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let tag: $$Props["tag"] = "h3";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={tag}
|
||||
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
};
|
||||
|
||||
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
@@ -0,0 +1,29 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export type FormInputEvent<T extends Event = Event> = T & {
|
||||
currentTarget: EventTarget & HTMLInputElement;
|
||||
};
|
||||
export type InputEvents = {
|
||||
blur: FormInputEvent<FocusEvent>;
|
||||
change: FormInputEvent<Event>;
|
||||
click: FormInputEvent<MouseEvent>;
|
||||
focus: FormInputEvent<FocusEvent>;
|
||||
focusin: FormInputEvent<FocusEvent>;
|
||||
focusout: FormInputEvent<FocusEvent>;
|
||||
keydown: FormInputEvent<KeyboardEvent>;
|
||||
keypress: FormInputEvent<KeyboardEvent>;
|
||||
keyup: FormInputEvent<KeyboardEvent>;
|
||||
mouseover: FormInputEvent<MouseEvent>;
|
||||
mouseenter: FormInputEvent<MouseEvent>;
|
||||
mouseleave: FormInputEvent<MouseEvent>;
|
||||
mousemove: FormInputEvent<MouseEvent>;
|
||||
paste: FormInputEvent<ClipboardEvent>;
|
||||
input: FormInputEvent<InputEvent>;
|
||||
wheel: FormInputEvent<WheelEvent>;
|
||||
};
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import type { InputEvents } from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLInputAttributes;
|
||||
type $$Events = InputEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"] = undefined;
|
||||
export { className as class };
|
||||
|
||||
// Workaround for https://github.com/sveltejs/svelte/issues/9305
|
||||
// Fixed in Svelte 5, but not backported to 4.x.
|
||||
export let readonly: $$Props["readonly"] = undefined;
|
||||
</script>
|
||||
|
||||
<input
|
||||
class={cn(
|
||||
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{readonly}
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
on:focus
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousemove
|
||||
on:paste
|
||||
on:input
|
||||
on:wheel|passive
|
||||
{...$$restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = LabelPrimitive.Props;
|
||||
type $$Events = LabelPrimitive.Events;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
class={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:mousedown
|
||||
>
|
||||
<slot />
|
||||
</LabelPrimitive.Root>
|
||||
@@ -0,0 +1,3 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
@@ -0,0 +1,62 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import type { TransitionConfig } from "svelte/transition";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
type FlyAndScaleParams = {
|
||||
y?: number;
|
||||
x?: number;
|
||||
start?: number;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
export const flyAndScale = (
|
||||
node: Element,
|
||||
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
|
||||
): TransitionConfig => {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === "none" ? "" : style.transform;
|
||||
|
||||
const scaleConversion = (
|
||||
valueA: number,
|
||||
scaleA: [number, number],
|
||||
scaleB: [number, number]
|
||||
) => {
|
||||
const [minA, maxA] = scaleA;
|
||||
const [minB, maxB] = scaleB;
|
||||
|
||||
const percentage = (valueA - minA) / (maxA - minA);
|
||||
const valueB = percentage * (maxB - minB) + minB;
|
||||
|
||||
return valueB;
|
||||
};
|
||||
|
||||
const styleToString = (
|
||||
style: Record<string, number | string | undefined>
|
||||
): string => {
|
||||
return Object.keys(style).reduce((str, key) => {
|
||||
if (style[key] === undefined) return str;
|
||||
return str + `${key}:${style[key]};`;
|
||||
}, "");
|
||||
};
|
||||
|
||||
return {
|
||||
duration: params.duration ?? 200,
|
||||
delay: 0,
|
||||
css: (t) => {
|
||||
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||
|
||||
return styleToString({
|
||||
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||
opacity: t
|
||||
});
|
||||
},
|
||||
easing: cubicOut
|
||||
};
|
||||
};
|
||||
@@ -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