api keys & better validator #6

Merged
cirroskais merged 7 commits from development into master 2024-08-02 15:37:11 +00:00
36 changed files with 496 additions and 224 deletions

View file

@ -33,6 +33,6 @@
"mime": "^4.0.4",
"minio": "^7.1.4",
"svelte-sonner": "^0.3.27",
"validator": "^13.12.0"
"zod": "^3.23.8"
}
}

View file

@ -0,0 +1,16 @@
-- AlterTable
ALTER TABLE `User` ADD COLUMN `totp` VARCHAR(191) NULL;
-- CreateTable
CREATE TABLE `APIKey` (
`id` VARCHAR(191) NOT NULL,
`userId` INTEGER NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`expiresAt` DATETIME(3) NOT NULL,
`permissions` INTEGER NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `APIKey` ADD CONSTRAINT `APIKey_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -12,6 +12,7 @@ model User {
username String @unique
email String @unique
password String
totp String?
role Role
createdAt DateTime @default(now())
lastSeen DateTime @default(now())
@ -20,6 +21,7 @@ model User {
maxUploadMB Int @default(100)
uploads Upload[]
sessions Session[]
apiKeys APIKey[]
}
model Session {
@ -58,6 +60,15 @@ model Upload {
uploaded DateTime @default(now())
}
model APIKey {
id String @id
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
expiresAt DateTime
permissions Int
}
enum Role {
ADMINISTRATOR
USER

View file

@ -1,44 +0,0 @@
import { error, redirect } from '@sveltejs/kit';
import { getSession } from '$lib/server/database';
import { COOKIE } from '$lib/config';
const PUBLIC_RESOURCES = [
'/',
'/api',
'/api/auth/register',
'/api/auth/login',
'/terms',
'/privacy'
];
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const { cookies, locals } = event;
const session = await getSession(cookies.get(COOKIE) || '');
if (session && session.user) {
locals.user = {
id: session.user.id,
username: session.user.username,
email: session.user.email,
maxUploadMB: session.user.maxUploadMB,
role: session.user.role
};
} else {
if (event.route.id) {
if (event.route.id.includes('(app)')) return redirect(303, '/');
}
}
return await resolve(event);
}
/** @type {import('@sveltejs/kit').HandleServerError} */
export async function handleError({ error, event, status, message }) {
console.log(error);
return {
status,
message
};
}

52
src/hooks.server.ts Normal file
View file

@ -0,0 +1,52 @@
import { redirect } from '@sveltejs/kit';
import { getSession, getUserApiKey } from '$lib/server/database';
import { COOKIE } from '$lib/config';
export async function handle({ event, resolve }) {
const { cookies, locals, request } = event;
let cookie = cookies.get(COOKIE);
let bearer = request.headers.get('Authorization');
if (bearer) bearer = bearer.replace('Bearer ', '');
if (cookie) {
const session = await getSession(cookie);
if (session && session.user) {
locals.user = {
id: session.user.id,
username: session.user.username,
email: session.user.email,
maxUploadMB: session.user.maxUploadMB,
role: session.user.role
};
}
}
if (bearer && !locals.user) {
const apiKey = await getUserApiKey(bearer);
if (apiKey && apiKey.user) {
locals.user = {
id: apiKey.user.id,
username: apiKey.user.username,
email: apiKey.user.email,
maxUploadMB: apiKey.user.maxUploadMB,
role: apiKey.user.role
};
}
}
if (!locals.user && event.route.id) {
if (event.route.id.includes('(app)')) return redirect(303, '/');
}
return await resolve(event);
}
export async function handleError({ error, status, message }) {
console.log(error);
return {
status,
message
};
}

View file

@ -25,7 +25,7 @@
{#if visible}
<div
transition:slide
class="w-[12rem] z-10 h-fit translate-y-12 py-2 bg-crust rounded-lg origin-top right-0 absolute transition-all shadow-md translate-x-3.5"
class="w-[12rem] z-10 h-fit translate-y-11 py-2 bg-crust rounded-lg origin-top right-0 absolute transition-all shadow-md translate-x-4 outline outline-1 outline-surface0"
>
<slot />
</div>

View file

@ -1,4 +1,5 @@
<script>
// @ts-ignore
import { fade } from 'svelte/transition';
import { Mail, SquareAsterisk, LogIn, Undo } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
@ -35,6 +36,7 @@
method: 'POST',
body: JSON.stringify({ email, password })
}).catch((_) => toast.error(_.message));
// @ts-ignore
const body = await response.json().catch((_) => toast.error(_.message));
if (!body?.success) {
@ -58,7 +60,7 @@
type={'email'}
name={'email'}
id={'email'}
placeholder={'user@example.com'}
placeholder={'john@doefamily.com'}
bind:value={email}
required={true}
>

View file

@ -46,6 +46,7 @@
method: 'POST',
body: JSON.stringify({ username, email, password })
}).catch((_) => toast.error(_.message));
// @ts-ignore
const body = await response.json().catch((_) => toast.error(_.message));
if (!body?.success) {
@ -79,7 +80,7 @@
type={'email'}
name={'email'}
id={'email'}
placeholder={'user@example.com'}
placeholder={'jane@doefamily.com'}
bind:value={email}
required={true}
>

View file

@ -20,7 +20,7 @@
<div class="flex my-auto space-x-3">
<HeaderLink href="/dashboard">Dashboard</HeaderLink>
<HeaderLink href="/uploads">Uploads</HeaderLink>
<HeaderLink href="/links">Links</HeaderLink>
<HeaderLink href="/documentation">Documentation</HeaderLink>
{#if $user?.role === 'ADMINISTRATOR'}
<HeaderLink href="/admin">Admin</HeaderLink>
{/if}
@ -47,7 +47,7 @@
<DropdownButton href="/settings">
<p class="my-auto">Settings</p>
</DropdownButton>
<DropdownButton href="/api/auth/logout">
<DropdownButton href="/api/v1/auth/logout">
<p class="my-auto text-red-500">Logout</p>
</DropdownButton>
</Dropdown>

View file

@ -1,18 +1,22 @@
<script>
<script lang="ts">
import { CircleAlert, Check } from 'lucide-svelte';
export let type, name, id, placeholder, value, required;
export let type, name, id, placeholder, value: string, required;
</script>
<!-- insane that i have to do this because -->
<!-- 'type' attribute cannot be dynamic if input uses two-way binding -->
<!--
insane that i have to do this because
'type' attr cannot be dynamic if input
uses two-way binding
-->
{#if type === 'username'}
<div class="flex p-2 space-x-1 rounded-lg shadow-md bg-crust">
<div class="py-0.5 pr-1 border-r-2 border-overlay2">
<div class="flex p-2 space-x-2 rounded-lg shadow-md bg-crust">
<div class="py-0.5 pr-2 border-r-2 border-overlay0">
<slot />
</div>
<input
class="py-0.5 bg-crust peer placeholder:text-overlay1"
class="py-0.5 w-64 bg-crust placeholder:text-overlay1"
type="username"
{name}
{id}
@ -20,20 +24,14 @@
{required}
bind:value
/>
<div class="hidden my-auto peer-invalid:flex">
<CircleAlert />
</div>
<div class="my-auto peer-invalid:hidden">
<Check />
</div>
</div>
{:else if type === 'email'}
<div class="flex p-2 space-x-1 rounded-lg shadow-md bg-crust">
<div class="py-0.5 pr-1 border-r-2 border-overlay2">
<div class="flex p-2 space-x-2 rounded-lg shadow-md bg-crust">
<div class="py-0.5 pr-2 border-r-2 border-overlay0">
<slot />
</div>
<input
class="py-0.5 bg-crust peer placeholder:text-overlay1"
class="py-0.5 w-64 bg-crust placeholder:text-overlay1"
type="email"
{name}
{id}
@ -41,20 +39,14 @@
{required}
bind:value
/>
<div class="hidden my-auto peer-invalid:flex">
<CircleAlert />
</div>
<div class="my-auto peer-invalid:hidden">
<Check />
</div>
</div>
{:else if type === 'password'}
<div class="flex p-2 space-x-1 rounded-lg shadow-md bg-crust">
<div class="py-0.5 pr-1 border-r-2 border-overlay2">
<div class="flex p-2 space-x-2 rounded-lg shadow-md bg-crust">
<div class="py-0.5 pr-2 border-r-2 border-overlay0">
<slot />
</div>
<input
class="py-0.5 bg-crust peer placeholder:text-overlay1"
class="py-0.5 w-64 bg-crust placeholder:text-overlay1"
type="password"
{name}
{id}
@ -62,11 +54,5 @@
{required}
bind:value
/>
<div class="hidden my-auto peer-invalid:flex">
<CircleAlert />
</div>
<div class="my-auto peer-invalid:hidden">
<Check />
</div>
</div>
{/if}

View file

@ -1,15 +1,17 @@
<script>
export let href,
download,
style = 'link';
</script>
{#if style === 'link'}
<a {href} class="text-blue-500">
<a {href} {download} class="text-blue-500">
<slot />
</a>
{:else if style === 'button'}
<a
{href}
{download}
class="flex p-2 space-x-2 rounded-lg border-b-2 hadow-md bg-crust border-overlay2 hover:border-overlay0 group-disabled/button:border-overlay0 group-disabled/button:hover:border-overlay0"
>
<slot />

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { X } from 'lucide-svelte';
export let key: { id: string };
function remove() {
fetch('/api/v1/keys', {
method: 'DELETE',
body: JSON.stringify({ key: key.id })
}).then(() => invalidateAll());
}
</script>
<tr>
<th>
<tt class="text-md">{key.id}</tt>
</th>
<th class="flex">
<button class="m-auto hover:text-red" on:click={remove}>
<X></X>
</button>
</th>
</tr>

View file

@ -12,3 +12,25 @@ export const MAIL_WHITELIST = [
'pm.me',
'proton.me'
];
export const DOMAINS = [
'cdn.cirroskais.xyz',
'cdn.madhouselabs.net',
'snep.lol',
'i.chadthundercock.com',
'doing-ya.mom',
'*.is-gay.zip'
];
export const DEV_DOMAINS = ['cdn.dev.madhouselabs.net'];
export const API_KEY_PERMISSIONS = {
CREATE_UPLOADS: 1 << 0,
READ_UPLOADS: 1 << 1,
UPDATE_UPLOADS: 1 << 2,
DELETE_UPLOADS: 1 << 3,
READ_ACCOUNT: 1 << 4,
UPDATE_ACCOUNT_ACCOUNT_SETTINGS: 1 << 5, // allows for updating username & email. bad idea? probably.
UPDATE_ACCOUNT_EMBED_SETTINGS: 1 << 6
};

View file

@ -1,7 +1,18 @@
import { COOKIE } from '$lib/config';
import type { Cookies } from '@sveltejs/kit';
import { getSession } from './database';
import type { User, UserSettings } from '@prisma/client';
import { getSession, getUserApiKey } from './database';
import type { Role, UserSettings } from '@prisma/client';
interface User {
id: number;
username: string;
email: string;
password: string;
role: Role;
createdAt: Date;
lastSeen: Date;
maxUploadMB: number;
}
interface UserAndMaybeSettings extends User {
settings: UserSettings | null;
@ -14,7 +25,10 @@ export async function authenticate(request: Request, cookies: Cookies) {
let user: UserAndMaybeSettings | false = false;
if (bearer && !cookie) {
return false;
const key = await getUserApiKey(bearer);
if (key) {
user = key.user;
}
}
if (cookie && !bearer) {

View file

@ -138,3 +138,62 @@ export async function getSettings(id: number) {
}
});
}
export async function getUserApiKeys(userId: number) {
if (!userId) return false;
return await prisma.aPIKey.findMany({
where: {
userId: userId
}
});
}
export async function createUserApiKey(userId: number, permissions: number, expiresAt: Date) {
if (!userId) return false;
return await prisma.aPIKey.create({
data: {
id: randomBytes(42).toString('base64url'),
userId,
permissions,
expiresAt
}
});
}
export async function deleteUserApiKey(userId: number, id: string) {
if (!userId || !id) return false;
return await prisma.aPIKey.delete({
where: {
userId,
id
}
});
}
export async function getUserApiKey(id: string) {
if (!id) return false;
return await prisma.aPIKey.findFirst({
where: {
id
},
include: {
user: {
select: {
id: true,
username: true,
email: true,
password: true,
role: true,
createdAt: true,
lastSeen: true,
maxUploadMB: true,
settings: true
}
}
}
});
}

View file

@ -16,7 +16,7 @@ const ApiKeyLimits = {
setInterval(function resetMinute() {}, 1000 * 60);
setInterval(function resetHour() {}, 1000 * 60 * 60);
function hash(input) {
function hash(input: string) {
createHash('sha256').update(input).digest('hex');
}

View file

@ -1,8 +0,0 @@
import validator from 'validator';
import { MAIL_WHITELIST } from '$lib/config';
// https://github.com/validatorjs/validator.js?tab=readme-ov-file#validators
export function email(input) {
return validator.isEmail(input, { host_whitelist: MAIL_WHITELIST });
}

View file

@ -0,0 +1,42 @@
import { z } from 'zod';
import { findUser } from './database';
const INTERNAL_email = z.string().email('Invalid email address.');
export const email = INTERNAL_email.parse;
export const emailAndNotUsed = INTERNAL_email.refine(async (_) => {
return !Boolean(await findUser({ email: _ }));
}, 'Email is already being used.').parseAsync;
const INTERNAL_username = z
.string()
.min(3, 'Username must be at least 3 characters.')
.max(16, 'Usernames must be no more than 16 characters.')
.regex(
new RegExp(/^[A-z0-9\_\-\.]+$/g),
'Usernames must be alphanumeric with dashes, underscores, and periods.'
);
export const username = INTERNAL_username.parse;
export const usernameAndNotUsed = INTERNAL_username.refine(async (_) => {
return !Boolean(await findUser({ username: _ }));
}, 'Username is already being used.').parseAsync;
export const password = z
.string()
.min(6, 'Passwords must be longer than 6 characters.')
.max(128, 'You do not need a password longer than 128 fucking characters.').parse;
export const embedTitle = z
.string()
.max(256, 'Title must not be longer than 256 characters.').parse;
export const embedDescription = z
.string()
.max(2000, 'Description must not be longer than 2000 characters.').parse;
export const embedColor = z
.number()
.max(parseInt('ffffff', 16), 'Color must be less than 16777215.').parse;

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { page } from '$app/stores';
import Button from '$lib/components/Inputs/Button.svelte';
import Link from '$lib/components/Inputs/Link.svelte';
import { API_KEY_PERMISSIONS } from '$lib/config';
import { get } from 'svelte/store';
let awesome = '';
async function click() {
const response = await fetch('/api/v1/keys');
const body = (await response.json()) as { id: string; permissions: number }[];
const key = body.find((key) => key.permissions & API_KEY_PERMISSIONS.CREATE_UPLOADS);
if (!key) return (awesome = 'What the fuck did i tell you');
awesome = `{
"Version": "14.0.0",
"Name": "cirros file uploader",
"DestinationType": "ImageUploader, FileUploader",
"RequestMethod": "POST",
"RequestURL": "${get(page).url.origin}/api/v1/upload",
"Headers": {
"Authorization": "Bearer ${key.id}",
},
"Body": "MultipartFormData",
"FileFormName": "file",
"URL": "${get(page).url.origin}{response}",
}`;
}
</script>
<p>I'll make real documentation later but for now have this ShareX button</p>
<p class="mb-2">
MAKE SURE TO HAVE A VALID API KEY WITH THE CREATE_UPLOADS PERMISSION ( 1 &lt;&lt; 0 )
</p>
<Button {click}>The sharex button in question</Button>
<tt class="block mt-3 whitespace-pre-wrap">{awesome}</tt>
{#if awesome}
<Link href="data:text/plain;base64,{btoa(awesome)}" download={'uploader.sxcu'}>Download</Link>
{/if}

View file

@ -0,0 +1,7 @@
import { getUserApiKeys } from '$lib/server/database.js';
export async function load({ locals }) {
return {
keys: (await getUserApiKeys(locals.user.id)) || []
};
}

View file

@ -1,6 +1,21 @@
<script>
<script lang="ts">
import { Info, X, Plus } from 'lucide-svelte';
import Button from '$lib/components/Inputs/Button.svelte';
import ListedApiKey from '$lib/components/ListedAPIKey.svelte';
import { invalidateAll } from '$app/navigation';
export let data;
let username: string, email: string;
let title: string, description: string, color: string;
let opassword: string, npassword: string, cpassword: string;
let newPostsPublic: boolean, encryptUploads: boolean;
async function createApiKey() {
fetch('/api/v1/keys', {
method: 'POST'
}).then(() => invalidateAll());
}
</script>
<div class="grid grid-cols-1 gap-2 mx-auto xl:grid-cols-3 w-fit">
@ -18,6 +33,8 @@
name="username"
id="username"
placeholder="cirro"
maxlength="16"
bind:value={username}
/>
<p class="text-sm text-surface2">
Your username is used to identify you around the site. You can change it at any time.
@ -26,13 +43,14 @@
</div>
<div class="mb-1">
<div class="flex flex-col gap-1">
<label class="text-lg font-bold" for="username">Email</label>
<label class="text-lg font-bold" for="email">Email</label>
<input
class="px-2 h-8 rounded-lg ring-1 transition-all bg-mantle ring-surface2 focus-visible:outline-none focus-visible:outline-overlay0"
type="email"
name="email"
id="email"
placeholder="c*******@madhouselabs.net"
bind:value={email}
/>
<p class="text-sm text-surface2">
Changing your email may require you to verify ownership.
@ -47,7 +65,7 @@
<div class="flex flex-col gap-2 p-2 max-w-lg rounded-lg bg-crust">
<p class="flex text-xl font-bold">
API Keys
<button class="my-auto ml-auto hover:text-blue">
<button class="my-auto ml-auto hover:text-blue" on:click={createApiKey}>
<Plus></Plus>
</button>
</p>
@ -60,16 +78,9 @@
</tr>
</thead>
<tbody>
<tr>
<th>
<tt class="text-md">d8895a0c-daa5-4b6e-b66f-2494c039fe9e </tt>
</th>
<th class="flex">
<button class="m-auto hover:text-red">
<X></X>
</button>
</th>
</tr>
{#each data.keys as key}
<ListedApiKey {key} />
{/each}
</tbody>
</table>
</div>
@ -93,23 +104,25 @@
name="title"
id="title"
placeholder={`{{ file }}`}
maxlength="256"
bind:value={title}
/>
<p class="text-sm text-surface2">The title shown on the embed.</p>
<p class="text-sm text-surface2">The title shown on the embed. Max 256 characters.</p>
</div>
</div>
<div>
<div class="flex flex-col gap-1">
<label class="text-lg font-bold" for="description">Desciption</label>
<textarea
class="px-2 py-1 h-48 rounded-lg ring-1 transition-all bg-mantle ring-surface2 focus-visible:outline-none focus-visible:outline-overlay0"
class="px-2 py-1 h-48 rounded-lg ring-1 transition-all resize-none bg-mantle ring-surface2 focus-visible:outline-none focus-visible:outline-overlay0"
name="description"
id="description"
placeholder={`Uploaded by {{ username }} at {{ time }}`}
maxlength="2000"
bind:value={description}
></textarea>
<p class="text-sm text-surface2">
The description of the embed. Can have up to 2000 characters.
</p>
<p class="text-sm text-surface2">The description of the embed. Max 2000 characters.</p>
</div>
</div>
@ -165,8 +178,8 @@
</div>
<div>
<div class="flex flex-col gap-1">
<label class="text-lg font-bold" for="description">2FA</label>
<div class="flex flex-col gap-0.5">
<p class="text-lg font-bold">2FA</p>
<p class="text-sm text-surface2">
A One-Time Password will be required each time you login.
</p>
@ -184,7 +197,7 @@
<span class="w-full border-b-2 border-surface0" />
<div class="flex flex-col gap-3">
<div class="">
<div class="flex flex-col gap-1">
<div class="flex flex-col gap-0.5">
<div>
<input type="checkbox" name="newPostsPublic" id="newPostsPublic" />
<label class="" for="newPostsPublic">New Posts Public</label>
@ -195,10 +208,10 @@
</div>
</div>
<div class="">
<div class="flex flex-col gap-1">
<div class="flex flex-col gap-0.5">
<div>
<input type="checkbox" name="newPostsPublic" id="newPostsPublic" />
<label class="" for="newPostsPublic">Encrypt Uploads</label>
<input type="checkbox" name="encryptUploads" id="encryptUploads" />
<label class="" for="encryptUploads">Encrypt Uploads</label>
</div>
<p class="text-sm text-surface2">Enable encryption for new uploads. (Client-side)</p>
</div>

View file

@ -3,13 +3,11 @@ import { error } from '@sveltejs/kit';
export async function load({ locals, url }) {
if (!locals.user) return error(403, { status: 403, message: 'Forbidden' });
if (+(url.searchParams.get('i') || 0) < 0) error(400, { status: 403, message: 'Invalid Index' });
const totalUploads = await prisma.upload.count();
const totalUploads = await prisma.upload.count({ where: { uploaderId: locals.user.id } });
if (!totalUploads) return { uploads: [], totalUploads: 0 };
if (+(url.searchParams.get('i') || 0) >= Math.ceil(totalUploads / 15))
error(400, { status: 403, message: 'Invalid Index' });

View file

@ -1,4 +1,3 @@
/** @type {import('./$types').RequestHandler} */
export function GET() {
return new Response('OK');
}

View file

@ -1,39 +0,0 @@
import { json } from '@sveltejs/kit';
import { COOKIE } from '$lib/config';
import { findUser, createSession } from '$lib/server/database';
import { email } from '$lib/server/validator';
import { verifyHash } from '$lib/server/crypto';
/** @type {import('./$types').RequestHandler} */
export async function POST(event) {
const { request, cookies } = event;
const body = await request.json();
if (!body?.email || !email(body?.email))
return json({ error: 'Invalid email.' }, { status: 400 });
if (!body?.password || body?.password.length > 128 || body?.password.length < 6)
return json({ error: 'Invalid password.' }, { status: 400 });
const user = await findUser({ email: body?.email });
if (!user) return json({ error: 'User record not found.' }, { status: 401 });
if (!(await verifyHash(user.password, body?.password)))
return json({ error: 'User record not found.' }, { status: 401 });
const session = await createSession(user.id);
cookies.set(COOKIE, session.id, { path: '/' });
return json(
{
success: true,
data: {
id: user.id,
username: user.username,
email: user.email
}
},
{ status: 200 }
);
}

View file

@ -0,0 +1,47 @@
import { json } from '@sveltejs/kit';
import { COOKIE } from '$lib/config';
import { findUser, createSession } from '$lib/server/database';
import { email } from '$lib/server/validator';
import { verifyHash } from '$lib/server/crypto';
import * as validator from '$lib/server/validator';
import { ZodError } from 'zod';
export async function POST({ request, cookies }) {
const body = (await request.json()) as {
email?: string;
password?: string;
};
try {
const email = validator.email(body.email);
const password = validator.password(body.password);
const user = await findUser({ email: email });
if (!user) return json({ error: 'User record not found.' }, { status: 401 });
if (!(await verifyHash(user.password, password)))
return json({ error: 'User record not found.' }, { status: 401 });
const session = await createSession(user.id);
cookies.set(COOKIE, session.id, { path: '/', sameSite: 'strict' });
return json(
{
success: true,
data: {
id: user.id,
username: user.username,
email: user.email
}
},
{ status: 200 }
);
} catch (e) {
if (e instanceof ZodError) {
return json({ error: e.errors[0].message }, { status: 400 });
} else {
return json({ error: 'Internal Server Error' }, { status: 500 });
}
}
}

View file

@ -2,9 +2,11 @@ import { redirect } from '@sveltejs/kit';
import { getSession, deleteSession } from '$lib/server/database';
import { COOKIE } from '$lib/config';
/** @type {import('./$types').RequestHandler} */
export async function GET({ cookies }) {
const session = await getSession(cookies.get(COOKIE));
const cookie = cookies.get(COOKIE);
if (!cookie) return redirect(302, '/');
const session = await getSession(cookie);
if (!session) {
cookies.delete(COOKIE, { path: '/' });
return redirect(302, '/');

View file

@ -1,45 +0,0 @@
import { json } from '@sveltejs/kit';
import { COOKIE } from '$lib/config';
import { createUser, createSession, findUser } from '$lib/server/database';
import { email } from '$lib/server/validator';
export async function POST(event) {
const { request, cookies } = event;
const body = await request.json();
if (!body?.username || body?.username.length > 16 || body?.username.length < 3)
return json({ error: 'Invalid username.' }, { status: 400 });
if (!body?.email || !email(body?.email))
return json({ error: 'Invalid email.' }, { status: 400 });
if (!body?.password || body?.password.length > 128 || body?.password.length < 6)
return json({ error: 'Invalid password.' }, { status: 400 });
const usernameTaken = !!(await findUser({ username: body?.username }));
if (usernameTaken) return json({ error: 'That username is taken.' }, { status: 400 });
const emailUsed = !!(await findUser({ email: body?.email }));
if (emailUsed)
return json({ error: 'That email has been used too many times.' }, { status: 400 });
const user = await createUser(body?.username, body?.email, body?.password).catch((e) => {});
if (!user) return json({ error: 'Internal Server Error' }, { status: 500 });
const session = await createSession(user.id);
if (!session) return json({ error: 'Internal Server Error' }, { status: 500 });
cookies.set(COOKIE, session.id, { path: '/' });
return json(
{
success: true,
data: {
id: user.id,
username: user.username,
email: user.email
}
},
{ status: 200 }
);
}

View file

@ -0,0 +1,42 @@
import { json } from '@sveltejs/kit';
import { COOKIE } from '$lib/config';
import { createUser, createSession } from '$lib/server/database';
import * as validator from '$lib/server/validator';
import { ZodError } from 'zod';
export async function POST({ request, cookies }) {
const body = (await request.json()) as {
username?: string;
password?: string;
email?: string;
};
try {
const username = await validator.usernameAndNotUsed(body.username);
const email = await validator.emailAndNotUsed(body.email);
const password = validator.password(body.password);
const user = await createUser(username, email, password);
const session = await createSession(user.id);
cookies.set(COOKIE, session.id, { path: '/', sameSite: 'strict' });
return json(
{
success: true,
data: {
id: user.id,
username: user.username,
email: user.email
}
},
{ status: 200 }
);
} catch (e) {
if (e instanceof ZodError) {
return json({ error: e.errors[0].message }, { status: 400 });
} else {
return json({ error: 'Internal Server Error' }, { status: 500 });
}
}
}

View file

@ -0,0 +1,29 @@
import { API_KEY_PERMISSIONS } from '$lib/config.js';
import { createUserApiKey, deleteUserApiKey, getUserApiKeys } from '$lib/server/database';
import { error, json } from '@sveltejs/kit';
export async function GET({ locals }) {
return json(await getUserApiKeys(locals.user.id));
}
export async function POST({ locals }) {
return json(
await createUserApiKey(
locals.user.id,
API_KEY_PERMISSIONS.CREATE_UPLOADS | API_KEY_PERMISSIONS.READ_ACCOUNT,
new Date('2099')
)
);
}
export async function DELETE({ locals, request }) {
const body = (await request.json().catch(() => {})) as { key?: string };
if (!body?.key) error(400, { status: 400, message: 'Missing "key" value.' });
if (!locals.user) error(401, { status: 401, message: 'Unauthorized' });
return json(
await deleteUserApiKey(locals.user.id, body.key).catch((_) =>
error(400, { status: 400, message: 'API key does not exist.' })
)
);
}

View file

@ -1,10 +1,9 @@
import { bytesToHumanReadable } from '$lib';
import prisma from '$lib/server/database';
import minio, { USAGE } from '$lib/server/minio';
import { USAGE } from '$lib/server/minio';
import { json } from '@sveltejs/kit';
import { get } from 'svelte/store';
/** @type {import("@sveltejs/kit").RequestHandler} */
export async function GET() {
return json({
users: await prisma.user.count({}),

View file

@ -1,8 +0,0 @@
import { json } from '@sveltejs/kit';
/** @type {import('./$types').RequestHandler} */
export async function GET(event) {
const { request, cookies, locals } = event;
return json(locals?.user);
}

View file

@ -0,0 +1,5 @@
import { json } from '@sveltejs/kit';
export async function GET({ locals }) {
return json(locals.user);
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -4,7 +4,8 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter()
adapter: adapter(),
csrf: { checkOrigin: false }
},
preprocess: vitePreprocess()
};

View file

@ -1787,11 +1787,6 @@ util@^0.12.3:
is-typed-array "^1.1.3"
which-typed-array "^1.1.2"
validator@^13.12.0:
version "13.12.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f"
integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==
vite@^5.3.5:
version "5.3.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.5.tgz#b847f846fb2b6cb6f6f4ed50a830186138cb83d8"
@ -1870,3 +1865,8 @@ yaml@^2.3.4:
version "2.5.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d"
integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==