Add: Post creation

This commit is contained in:
Donatas Kirda 2024-05-19 01:04:17 +03:00
parent 96a9f3971d
commit ff34d5fd96
Signed by: bloodwiing
GPG Key ID: 63020D8D3F4A164F
6 changed files with 235 additions and 32 deletions

View File

@ -1,5 +1,5 @@
<script>
import Glowfx from "./fx/glowfx.svelte";
import Glowfx from "./fx/glowfx.svelte";
/**
* @type {boolean | null}

View File

@ -1,6 +1,7 @@
import { sql } from '$lib/db.server';
import { getCategoriesCachedByRef, getCategoryCached } from './category';
import { getUser, getUsersCachedByRef } from './user';
import { isPostgresError } from './root';
import { getUser, getUsersCachedByRef, sqlUserFromToken } from './user';
/**
* @typedef {import('$types/base').Post} Post
@ -174,3 +175,69 @@ export async function getPost(post_id, opts = {}) {
*/
return parsePostFromRow(author, category, post, withMetrics);
}
/**
* @param {string} token
* @param {import('$types/base').Category} category
* @param {string} name
* @param {string} content
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
*/
export async function createPost(token, category, name, content) {
const insert = sql`
INSERT INTO doki8902.message_post (author_id, category_id, name, latest_content)
VALUES (
(${ sqlUserFromToken(token) }),
${ category.id }, ${ name }, ${ content }
)
RETURNING id;`;
let result;
try {
result = await insert;
} catch (e) {
const pgerr = isPostgresError(e);
switch (pgerr?.constraint_name) {
case 'message_require_user':
return {
error: true,
msg: 'User token was invalid',
};
case 'post_category_id_fkey':
return {
error: true,
msg: 'Post category does not exist',
};
case 'content_min_length':
return {
error: true,
msg: 'Post content cannot be empty',
};
case 'name_min_length':
return {
error: true,
msg: 'Post name cannot be empty',
};
default:
console.log(e);
return {
error: true,
msg: 'Unknown error (notify dev)',
};
}
}
const postId = result[0]['id'];
return {
success: true,
result: postId,
};
}

View File

@ -1,3 +1,14 @@
/**
* @param {any} e
* @returns {import('postgres').PostgresError | null}
*/
export function isPostgresError(e) {
if (e && typeof(e) === 'object' && 'name' in e && e.name === 'PostgresError') {
return e;
}
return null;
}
/**
* @template T
* @param {import('node-cache')} cache

View File

@ -1,5 +1,5 @@
import { createCache } from '$lib/cache.server';
import { cacheUpdater, cachedMethod, refExtendCachedMethod } from './root';
import { cacheUpdater, cachedMethod, isPostgresError, refExtendCachedMethod } from './root';
import { sql } from '$lib/db.server';
import { argon2id, hash, verify } from 'argon2';
import { PostgresError } from 'postgres';
@ -33,6 +33,14 @@ function parseUserFromRow(row) {
*/
const updateUserCache = cacheUpdater(cache);
/**
* @param {string} token
* @returns {import('postgres').PendingQuery<import('postgres').Row[]>}
*/
export function sqlUserFromToken(token) {
return sql`SELECT user_id FROM doki8902.user_session WHERE token = ${ token }`;
}
/**
* @param {number[]} user_ids
* @returns {Promise<Result<User>>}
@ -49,9 +57,9 @@ export async function getUsers(user_ids) {
if (user_ids.length == 0) return new Map();
const query = sql`
SELECT id, username, join_time
FROM doki8902.user
WHERE id IN ${ sql(user_ids) };`;
SELECT id, username, join_time
FROM doki8902.user
WHERE id IN ${ sql(user_ids) };`;
let users = await query;
@ -100,33 +108,34 @@ export async function createUser(username, password) {
try {
await insert;
} catch (e) {
if (e && typeof(e) === 'object' && 'name' in e && e.name === 'PostgresError') {
const pgerr = /** @type {PostgresError} */ (e);
const pgerr = isPostgresError(e);
switch (pgerr.constraint_name) {
case 'idx_user_username':
return {
error: true,
msg: "Username taken",
};
switch (pgerr?.constraint_name) {
case 'idx_user_username':
return {
error: true,
msg: "Username taken",
};
case 'username_length_min':
return {
error: true,
msg: "Username has invalid length",
};
case 'username_length_min':
return {
error: true,
msg: "Username has invalid length",
};
case 'username_valid_symbols':
return {
error: true,
msg: "Username contains invalid symbols",
};
case 'username_valid_symbols':
return {
error: true,
msg: "Username contains invalid symbols",
};
default:
break;
}
default:
console.log(e);
return {
error: true,
msg: 'Unknown error (notify dev)',
};
}
}
@ -178,3 +187,47 @@ export async function createUserSession(username, password) {
result: token[0]['token'],
};
}
/**
* @param {string} token
* @returns {Promise<number | import('$types/status').Error>}
*/
export async function getUserIDOfSession(token) {
const query = sql`
SELECT user_id
FROM doki8902.user_session
WHERE token = ${ token }`;
const result = await query;
if (result.length == 0) {
return {
error: true,
msg: "Invalid user session",
};
}
return result[0]['user_id'];
}
/**
* @param {string} token
* @returns {Promise<User | import('$types/status').Error>}
*/
export async function getUserOfSession(token) {
const query = sql`
SELECT user_id
FROM doki8902.user_session
WHERE token = ${ token }`;
const result = await query;
if (result.length == 0) {
return {
error: true,
msg: "Invalid user session",
};
}
return result[0]['user_id'];
}

View File

@ -0,0 +1,61 @@
import { getCategories, getCategoriesCached } from '$lib/server/db/category.js';
import { createPost } from '$lib/server/db/post.js';
import { getUserIDOfSession } from '$lib/server/db/user.js';
import { parseIntNull } from '$lib/util.js';
import { error, redirect } from '@sveltejs/kit';
export async function load({ cookies }) {
if (!cookies.get('token')) {
redirect(302, '/register');
}
const categories = await getCategories();
return {
categories: Array(...categories.values())
};
}
/** @type {import('@sveltejs/kit').Action} */
async function POST({ request, cookies }) {
if (request.method !== 'POST') {
return;
}
const userToken = cookies.get('token');
if (!userToken) {
error(401, 'Need to be logged in!');
}
const data = await request.formData();
const categoryId = parseIntNull(data.get('category')?.toString());
const name = data.get('name')?.toString();
const content = data.get('content')?.toString();
if (!categoryId) {
error(400, `Invalid category ID ${categoryId}`);
}
if (!name || !content) {
error(400, `Not all fields have been filled out`);
}
const category = (await getCategoriesCached([categoryId])).get(categoryId);
if (!category) {
error(400, `Invalid category ID ${categoryId}`);
}
const result = await createPost(userToken, category, name, content);
if ('error' in result) {
} else {
redirect(303, `/posts/${result.result}`);
}
}
export const actions = {
default: POST,
};

View File

@ -1,5 +1,10 @@
<script>
/**
* @type {{
* categories: import("$types/base").Category[],
* }}
*/
export let data;
</script>
<style lang="scss">
@ -12,6 +17,12 @@
<form action="/compose" method="post" class="composeForm">
<h1>Compose</h1>
<select name="category" id="category">
{#each data.categories as category}
<option value={category.id}></option>
{/each}
</select>
<input type="text" name="name" id="name">
<textarea name="content" id="content" rows="4"></textarea>
<button type="submit">Post!</button>
</form>