From 7745b0741845378e550cb034743e1a121fbe6b6a Mon Sep 17 00:00:00 2001 From: cirroskais Date: Fri, 2 Aug 2024 09:33:34 -0400 Subject: [PATCH] better validator --- package.json | 2 +- src/lib/components/Dropdown.svelte | 2 +- src/lib/components/Forms/LoginForm.svelte | 2 + src/lib/components/Forms/RegisterForm.svelte | 1 + src/lib/components/Header.svelte | 2 +- src/lib/components/Inputs/FormInput.svelte | 36 ++++---------- src/lib/components/ListedAPIKey.svelte | 14 ++++-- src/lib/server/database.ts | 11 ++++ src/lib/server/validator.js | 8 --- src/lib/server/validator.ts | 42 ++++++++++++++++ src/routes/(app)/settings/+page.svelte | 31 +++++++++--- src/routes/api/{+server.js => +server.ts} | 1 - src/routes/api/v1/auth/login/+server.js | 39 --------------- src/routes/api/v1/auth/login/+server.ts | 47 ++++++++++++++++++ .../v1/auth/logout/{+server.js => +server.ts} | 6 ++- src/routes/api/v1/auth/register/+server.js | 45 ----------------- src/routes/api/v1/auth/register/+server.ts | 42 ++++++++++++++++ src/routes/api/v1/keys/+server.ts | 16 +++++- .../v1/statistics/{+server.js => +server.ts} | 3 +- src/routes/api/v1/user/+server.js | 8 --- src/routes/api/v1/user/+server.ts | 5 ++ static/favicon.ico | Bin 0 -> 16958 bytes yarn.lock | 10 ++-- 23 files changed, 223 insertions(+), 150 deletions(-) delete mode 100644 src/lib/server/validator.js create mode 100644 src/lib/server/validator.ts rename src/routes/api/{+server.js => +server.ts} (52%) delete mode 100644 src/routes/api/v1/auth/login/+server.js create mode 100644 src/routes/api/v1/auth/login/+server.ts rename src/routes/api/v1/auth/logout/{+server.js => +server.ts} (75%) delete mode 100644 src/routes/api/v1/auth/register/+server.js create mode 100644 src/routes/api/v1/auth/register/+server.ts rename src/routes/api/v1/statistics/{+server.js => +server.ts} (76%) delete mode 100644 src/routes/api/v1/user/+server.js create mode 100644 src/routes/api/v1/user/+server.ts create mode 100644 static/favicon.ico 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 0000000000000000000000000000000000000000..8b11af6b4b32f9a56d1f94aa2ce00bd92e84342b GIT binary patch literal 16958 zcmeI4cZ?KO9LMKCz=9m2vBMpTNU&kAcy}TYOAIz*qGE|9ilW9wROAo@H7ZyLs6?<~ zj2L4>{X+wyV(%KG0(XK9+oDldU3UF^XXcTYad-A@xg8_-BcFZm{a%^h_sz_kHxt0~ z>iE;#?D5;^4LZ#8w)8x25a9A!L4Mz;i&`$1^9m7d`+qDK(k}n{V4eCqum3{VK`liv;xRy8kSUQ|tPeAd;&5YCSm>bl*5x0{Ru7IrqB%>zo4Zphh*; zFRcqf`n9$(}1J?8TlZ zUmp>u^Q~sCXW4K zd(eF^{aP3Q6IP-AA&B@1r0M)Tf-b`MB@70AmN#vmBmbYU3iS6x!0RCGv+{_Uitkg{ zGpyT`bOorr*1_(OxK*Hk3c*XuTcEYynD+R3mwyKujFVLUMW8;q4(vLL$yk~C4eYWfJ8w^y|CRD#e`+@hjQ)9YByDBr zzkryf;Unm6#jkHTZLo*si)OVaS_$gE8n%N(twj9|1p6)Md3UrG_)g^8;TX$TC~MyX zlL|GemA^#&bBIglZa!!|lj+;z5^&YB3vIP_m}_7M@`+gq`VS^ZeQ(#=J-`ZhI(dE5 z(KlV&22vOYTiebsKbutg#iNkeUf5B8cVb)wigyAS(;BY*{$`;=_rZDaENC5k2(<5s zT3;My9nhGUMjO&4oG91jiyrTh{jFBsZeus z(64WFZ-U|+V+A;kyuM*Bg_&?7#I5JL4x;zSjrcUK_#O28tm(brp#Ow0zV}JB)*0KI z%s#MzaagJL#)DuOoCWv8cW@=x){VkkFms^vao7=RVny^z+e@Ihq@$4OyN|SvhXwFC z`~|iyUE>;~C4|H`?WgVpyX{KKdY{?^I@F5jAIZV9@DXS(WIAVB=X71%44R)G!H!>f zX?O+ZgXTs1qK)Be(B7vH*mg37RbY?vE@iE!`S#MdE4L#07jgu}qdkk(tGhul{RVcN zzfyh+?t(L6FVH$^_6THs(;TXu`tIj@f}bv7egLViAA4-&b?-a_*TLCv3N%8+Rz$zi zsddhdN%6b@Q$c&aMljb0*;vyYX)HY>jEyC8VOY?d9S6F0>|;jruTvkLUtFbG5&dfz zO~=)qSI_jTpb55wa_tN{YRq}o05i#7Yn7Gl21mdHpnEB*e+9nbpnIxpE24j220j!j z(Ho6zHypa2w5I6UDw_@y{9OK@&2b+Ly02j_6*PzvNJjPIpU-!9eG(7F+f*uY;n3=>naYyZ2+Bbt@oFM{Ty)XU7(e##?E$y|NUp@+;q;(dTd*mjooNmgkRGON&E>$tt=KtohjX|2}AEfCpO`Q>x+uDP4 z@!}wz9H!Hj1m$}FV7Y9+Ak7@o;`=j0f_CXk+Wm5BWtc7vj@v#x<@?(khznb?{rD!O zQyYVHN?oQw<(~EL^zhU1y>x(VLu*(42MO8UUfrT5Uv&GtI;9P3W>Assnp-o+o0!Ws z`xGv3%Xz=}+!<&NitS}uvF(?o5 clU$}TO#25ZA^4W{<0hmq%>}~;m4$x)0p`F`l>h($ literal 0 HcmV?d00001 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==