diff --git a/package.json b/package.json index 9872883..9d8fe1d 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/lib/components/Dropdown.svelte b/src/lib/components/Dropdown.svelte index cb32b6d..676e3df 100644 --- a/src/lib/components/Dropdown.svelte +++ b/src/lib/components/Dropdown.svelte @@ -25,7 +25,7 @@ {#if visible}
diff --git a/src/lib/components/Forms/LoginForm.svelte b/src/lib/components/Forms/LoginForm.svelte index 3c9454e..7c7c268 100644 --- a/src/lib/components/Forms/LoginForm.svelte +++ b/src/lib/components/Forms/LoginForm.svelte @@ -1,4 +1,5 @@ - - + + {#if type === 'username'}
- -
- -
{:else if type === 'email'}
@@ -33,7 +31,7 @@
- -
- -
{:else if type === 'password'}
@@ -54,7 +46,7 @@
- -
- -
{/if} diff --git a/src/lib/components/ListedAPIKey.svelte b/src/lib/components/ListedAPIKey.svelte index 9d2b9fa..1711adc 100644 --- a/src/lib/components/ListedAPIKey.svelte +++ b/src/lib/components/ListedAPIKey.svelte @@ -1,7 +1,15 @@ - @@ -9,7 +17,7 @@ {key.id} - diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 063fa1e..c0abcc3 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -161,3 +161,14 @@ export async function createUserApiKey(userId: number, permissions: number, expi } }); } + +export async function deleteUserApiKey(userId: number, id: string) { + if (!userId || !id) return false; + + return await prisma.aPIKey.delete({ + where: { + userId, + id + } + }); +} diff --git a/src/lib/server/validator.js b/src/lib/server/validator.js deleted file mode 100644 index 62784b6..0000000 --- a/src/lib/server/validator.js +++ /dev/null @@ -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 }); -} diff --git a/src/lib/server/validator.ts b/src/lib/server/validator.ts new file mode 100644 index 0000000..cd0aea7 --- /dev/null +++ b/src/lib/server/validator.ts @@ -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; diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte index 7593b4a..e377417 100644 --- a/src/routes/(app)/settings/+page.svelte +++ b/src/routes/(app)/settings/+page.svelte @@ -1,9 +1,21 @@ -
@@ -21,6 +33,8 @@ name="username" id="username" placeholder="cirro" + maxlength="16" + bind:value={username} />

Your username is used to identify you around the site. You can change it at any time. @@ -36,6 +50,7 @@ name="email" id="email" placeholder="c*******@madhouselabs.net" + bind:value={email} />

Changing your email may require you to verify ownership. @@ -50,7 +65,7 @@

API Keys -

@@ -89,23 +104,25 @@ name="title" id="title" placeholder={`{{ file }}`} + maxlength="256" + bind:value={title} /> -

The title shown on the embed.

+

The title shown on the embed. Max 256 characters.

-

- The description of the embed. Can have up to 2000 characters. -

+

The description of the embed. Max 2000 characters.

diff --git a/src/routes/api/+server.js b/src/routes/api/+server.ts similarity index 52% rename from src/routes/api/+server.js rename to src/routes/api/+server.ts index f750c3b..9633400 100644 --- a/src/routes/api/+server.js +++ b/src/routes/api/+server.ts @@ -1,4 +1,3 @@ -/** @type {import('./$types').RequestHandler} */ export function GET() { return new Response('OK'); } diff --git a/src/routes/api/v1/auth/login/+server.js b/src/routes/api/v1/auth/login/+server.js deleted file mode 100644 index aaf2a15..0000000 --- a/src/routes/api/v1/auth/login/+server.js +++ /dev/null @@ -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 } - ); -} diff --git a/src/routes/api/v1/auth/login/+server.ts b/src/routes/api/v1/auth/login/+server.ts new file mode 100644 index 0000000..1cc1f08 --- /dev/null +++ b/src/routes/api/v1/auth/login/+server.ts @@ -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: '/' }); + + 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 }); + } + } +} diff --git a/src/routes/api/v1/auth/logout/+server.js b/src/routes/api/v1/auth/logout/+server.ts similarity index 75% rename from src/routes/api/v1/auth/logout/+server.js rename to src/routes/api/v1/auth/logout/+server.ts index 01dbad2..f8e53da 100644 --- a/src/routes/api/v1/auth/logout/+server.js +++ b/src/routes/api/v1/auth/logout/+server.ts @@ -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, '/'); diff --git a/src/routes/api/v1/auth/register/+server.js b/src/routes/api/v1/auth/register/+server.js deleted file mode 100644 index d27b763..0000000 --- a/src/routes/api/v1/auth/register/+server.js +++ /dev/null @@ -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 } - ); -} diff --git a/src/routes/api/v1/auth/register/+server.ts b/src/routes/api/v1/auth/register/+server.ts new file mode 100644 index 0000000..be8b459 --- /dev/null +++ b/src/routes/api/v1/auth/register/+server.ts @@ -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: '/' }); + + 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 }); + } + } +} diff --git a/src/routes/api/v1/keys/+server.ts b/src/routes/api/v1/keys/+server.ts index 94437c0..5856dc9 100644 --- a/src/routes/api/v1/keys/+server.ts +++ b/src/routes/api/v1/keys/+server.ts @@ -1,6 +1,6 @@ import { API_KEY_PERMISSIONS } from '$lib/config.js'; -import { createUserApiKey, getUserApiKeys } from '$lib/server/database'; -import { json } from '@sveltejs/kit'; +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)); @@ -15,3 +15,15 @@ export async function POST({ locals }) { ) ); } + +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.' }) + ) + ); +} diff --git a/src/routes/api/v1/statistics/+server.js b/src/routes/api/v1/statistics/+server.ts similarity index 76% rename from src/routes/api/v1/statistics/+server.js rename to src/routes/api/v1/statistics/+server.ts index aa140ba..061d8d0 100644 --- a/src/routes/api/v1/statistics/+server.js +++ b/src/routes/api/v1/statistics/+server.ts @@ -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({}), diff --git a/src/routes/api/v1/user/+server.js b/src/routes/api/v1/user/+server.js deleted file mode 100644 index 7806e5c..0000000 --- a/src/routes/api/v1/user/+server.js +++ /dev/null @@ -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); -} diff --git a/src/routes/api/v1/user/+server.ts b/src/routes/api/v1/user/+server.ts new file mode 100644 index 0000000..111a696 --- /dev/null +++ b/src/routes/api/v1/user/+server.ts @@ -0,0 +1,5 @@ +import { json } from '@sveltejs/kit'; + +export async function GET({ locals }) { + return json(locals.user); +} diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..8b11af6 Binary files /dev/null and b/static/favicon.ico differ diff --git a/yarn.lock b/yarn.lock index 0b30cc3..212d152 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==