Add: Post creation
This commit is contained in:
parent
96a9f3971d
commit
ff34d5fd96
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import Glowfx from "./fx/glowfx.svelte";
|
import Glowfx from "./fx/glowfx.svelte";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {boolean | null}
|
* @type {boolean | null}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { sql } from '$lib/db.server';
|
import { sql } from '$lib/db.server';
|
||||||
import { getCategoriesCachedByRef, getCategoryCached } from './category';
|
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
|
* @typedef {import('$types/base').Post} Post
|
||||||
@ -174,3 +175,69 @@ export async function getPost(post_id, opts = {}) {
|
|||||||
*/
|
*/
|
||||||
return parsePostFromRow(author, category, post, withMetrics);
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
* @template T
|
||||||
* @param {import('node-cache')} cache
|
* @param {import('node-cache')} cache
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createCache } from '$lib/cache.server';
|
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 { sql } from '$lib/db.server';
|
||||||
import { argon2id, hash, verify } from 'argon2';
|
import { argon2id, hash, verify } from 'argon2';
|
||||||
import { PostgresError } from 'postgres';
|
import { PostgresError } from 'postgres';
|
||||||
@ -33,6 +33,14 @@ function parseUserFromRow(row) {
|
|||||||
*/
|
*/
|
||||||
const updateUserCache = cacheUpdater(cache);
|
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
|
* @param {number[]} user_ids
|
||||||
* @returns {Promise<Result<User>>}
|
* @returns {Promise<Result<User>>}
|
||||||
@ -49,9 +57,9 @@ export async function getUsers(user_ids) {
|
|||||||
if (user_ids.length == 0) return new Map();
|
if (user_ids.length == 0) return new Map();
|
||||||
|
|
||||||
const query = sql`
|
const query = sql`
|
||||||
SELECT id, username, join_time
|
SELECT id, username, join_time
|
||||||
FROM doki8902.user
|
FROM doki8902.user
|
||||||
WHERE id IN ${ sql(user_ids) };`;
|
WHERE id IN ${ sql(user_ids) };`;
|
||||||
|
|
||||||
let users = await query;
|
let users = await query;
|
||||||
|
|
||||||
@ -100,33 +108,34 @@ export async function createUser(username, password) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await insert;
|
await insert;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e && typeof(e) === 'object' && 'name' in e && e.name === 'PostgresError') {
|
const pgerr = isPostgresError(e);
|
||||||
const pgerr = /** @type {PostgresError} */ (e);
|
|
||||||
|
|
||||||
switch (pgerr.constraint_name) {
|
switch (pgerr?.constraint_name) {
|
||||||
case 'idx_user_username':
|
case 'idx_user_username':
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
msg: "Username taken",
|
msg: "Username taken",
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'username_length_min':
|
case 'username_length_min':
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
msg: "Username has invalid length",
|
msg: "Username has invalid length",
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'username_valid_symbols':
|
case 'username_valid_symbols':
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
msg: "Username contains invalid symbols",
|
msg: "Username contains invalid symbols",
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
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'],
|
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'];
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
@ -1,5 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
|
/**
|
||||||
|
* @type {{
|
||||||
|
* categories: import("$types/base").Category[],
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export let data;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -12,6 +17,12 @@
|
|||||||
|
|
||||||
<form action="/compose" method="post" class="composeForm">
|
<form action="/compose" method="post" class="composeForm">
|
||||||
<h1>Compose</h1>
|
<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">
|
<input type="text" name="name" id="name">
|
||||||
<textarea name="content" id="content" rows="4"></textarea>
|
<textarea name="content" id="content" rows="4"></textarea>
|
||||||
</form>
|
<button type="submit">Post!</button>
|
||||||
|
</form>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user