better validator

This commit is contained in:
cirroskais 2024-08-02 09:33:34 -04:00
parent 303f1a232f
commit 7745b07418
No known key found for this signature in database
GPG key ID: 5FC73EBF2678E33D
23 changed files with 223 additions and 150 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

@ -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) {

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) {

View file

@ -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">
<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,12 +24,6 @@
{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">
@ -33,7 +31,7 @@
<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,12 +39,6 @@
{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">
@ -54,7 +46,7 @@
<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,7 +1,15 @@
<script>
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { X } from 'lucide-svelte';
export let key;
export let key: { id: string };
function remove() {
fetch('/api/v1/keys', {
method: 'DELETE',
body: JSON.stringify({ key: key.id })
}).then(() => invalidateAll());
}
</script>
<tr>
@ -9,7 +17,7 @@
<tt class="text-md">{key.id}</tt>
</th>
<th class="flex">
<button class="m-auto hover:text-red">
<button class="m-auto hover:text-red" on:click={remove}>
<X></X>
</button>
</th>

View file

@ -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
}
});
}

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

@ -1,9 +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">
@ -21,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.
@ -36,6 +50,7 @@
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.
@ -50,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>
@ -89,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>

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: '/' });
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: '/' });
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

@ -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.' })
)
);
}

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

@ -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==