big commit
This commit is contained in:
parent
e96f85efdc
commit
dcae12453c
26 changed files with 3183 additions and 8 deletions
11
src/app.html
11
src/app.html
|
@ -2,11 +2,14 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" href="%sveltekit.assets%/icon_sanifae_black.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
<body data-sveltekit-preload-data="hover" style="margin: 0; padding: 0; overflow: hidden;">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
47
src/lib/Area.svelte
Normal file
47
src/lib/Area.svelte
Normal file
|
@ -0,0 +1,47 @@
|
|||
<style>
|
||||
#content {
|
||||
background: var(--dark-1);
|
||||
|
||||
width: min(700px, 90vw);
|
||||
padding: 20px;
|
||||
padding-top: 8px;
|
||||
margin: 25px;
|
||||
|
||||
border-radius: 25px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#header {
|
||||
font-weight: bold;
|
||||
|
||||
font-size: 2rem;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#main {
|
||||
min-height: 250px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='content'>
|
||||
<div id='header'>
|
||||
<slot name="header">
|
||||
Header
|
||||
</slot>
|
||||
</div>
|
||||
<div id='main'>
|
||||
<slot name="main">
|
||||
<p>Content</p>
|
||||
</slot>
|
||||
</div>
|
||||
<div id='footer'>
|
||||
<slot name="footer">
|
||||
<p>Footer</p>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
15
src/lib/Form.svelte
Normal file
15
src/lib/Form.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import Area from '$lib/Area.svelte';
|
||||
</script>
|
||||
|
||||
<Area>
|
||||
<p slot="header">
|
||||
Welcome to SvelteKit
|
||||
</p>
|
||||
<p slot="main">
|
||||
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
||||
</p>
|
||||
<p slot="footer">
|
||||
|
||||
</p>
|
||||
</Area>
|
15
src/lib/Post.svelte
Normal file
15
src/lib/Post.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import Area from '$lib/Area.svelte';
|
||||
</script>
|
||||
|
||||
<Area>
|
||||
<p slot="header">
|
||||
Welcome to SvelteKit
|
||||
</p>
|
||||
<p slot="main">
|
||||
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
||||
</p>
|
||||
<p slot="footer">
|
||||
|
||||
</p>
|
||||
</Area>
|
108
src/lib/db.js
Normal file
108
src/lib/db.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
import sqlite3 from 'sqlite3'
|
||||
import { open } from 'sqlite'
|
||||
import { hash, compare } from 'bcrypt'
|
||||
|
||||
const {
|
||||
randomBytes
|
||||
} = await import('node:crypto');
|
||||
|
||||
var db;
|
||||
async function initDb() {
|
||||
db = await open({
|
||||
filename: `${process.cwd()}/db/main.db`,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
|
||||
await db.run('CREATE TABLE IF NOT EXISTS auth (username CHAR(64), password CHAR(1024))');
|
||||
await db.run('CREATE TABLE IF NOT EXISTS token (username CHAR(64), token CHAR(1024))');
|
||||
await db.run('CREATE TABLE IF NOT EXISTS post (username CHAR(64), id CHAR(64), content CHAR(10240), upvotes INTEGER, downvotes INTEGER)');
|
||||
await db.run('CREATE TABLE IF NOT EXISTS vote (id CHAR(64), username CHAR(64), type INTEGER)');
|
||||
}
|
||||
|
||||
async function registerBackend({user, pass}) {
|
||||
if (!db) await initDb();
|
||||
|
||||
var existingAccounts = await db.all('SELECT username FROM auth WHERE username = ?',[
|
||||
user
|
||||
]);
|
||||
|
||||
if (existingAccounts && existingAccounts.length > 0)
|
||||
return { success: 'Account already exists.' };
|
||||
|
||||
var passHash = await hash(pass,10);
|
||||
|
||||
await db.run('INSERT INTO auth (username, password) VALUES (?, ?)', [
|
||||
user,
|
||||
passHash
|
||||
])
|
||||
|
||||
return { success: 'Successfully created account.' };
|
||||
}
|
||||
|
||||
async function loginBackend({user, pass}) {
|
||||
if (!db) await initDb();
|
||||
|
||||
var existingAccounts = await db.all('SELECT username, password FROM auth WHERE username = ?',[
|
||||
user
|
||||
]);
|
||||
|
||||
if (!existingAccounts || existingAccounts.length < 1)
|
||||
return { success: 'Account does not exist.' };
|
||||
|
||||
var passHash = await compare(pass,existingAccounts[0].password);
|
||||
|
||||
if (!passHash)
|
||||
return { success: 'Incorrect password.' };
|
||||
|
||||
var token = randomBytes(256).toString('hex');
|
||||
|
||||
await db.run('INSERT INTO token (username, token) VALUES (?, ?)', [
|
||||
user,
|
||||
token
|
||||
])
|
||||
|
||||
return { success: 'Successfully logged into account.', token };
|
||||
}
|
||||
|
||||
async function postCreateBackend({user, content}) {
|
||||
var id = randomBytes(10).toString('hex');
|
||||
|
||||
await db.run('INSERT INTO post (username, id, content) VALUES (?, ?, ?)', [
|
||||
user,
|
||||
id,
|
||||
content
|
||||
])
|
||||
}
|
||||
|
||||
async function postGetBackend({id}) {
|
||||
var posts = await db.all('SELECT * from post WHERE id = ?', [
|
||||
id
|
||||
])
|
||||
|
||||
if (!posts || posts.length < 1) {
|
||||
return {'success': 'Post does not exist.'}
|
||||
}
|
||||
|
||||
return posts[0];
|
||||
}
|
||||
|
||||
async function tokenBackend({token}) {
|
||||
if (!db) await initDb();
|
||||
|
||||
var existingAccounts = await db.all('SELECT username from token WHERE token = ?',[
|
||||
token
|
||||
]);
|
||||
|
||||
if (!existingAccounts || existingAccounts.length < 1)
|
||||
return false;
|
||||
|
||||
return existingAccounts[0].username;
|
||||
}
|
||||
|
||||
export {
|
||||
registerBackend,
|
||||
loginBackend,
|
||||
tokenBackend,
|
||||
postCreateBackend,
|
||||
postGetBackend
|
||||
}
|
26
src/lib/util.js
Normal file
26
src/lib/util.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
let checkLength = function(string, field, lowerBound, upperBound) {
|
||||
if (string.length < lowerBound) {
|
||||
if (string.length == 0) {
|
||||
return { success: `${field} cannot be blank.` }
|
||||
}
|
||||
return { success: `${field} is too short (minimum length: ${lowerBound} characters).` }
|
||||
}
|
||||
|
||||
if (string.length > upperBound) {
|
||||
return { success: `${field} is too long (maximum length: ${upperBound} characters).` }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
let checkRegex = function(string, field, regex) {
|
||||
if (string.search(regex) != -1) {
|
||||
return { success: `${field} contains illegal characters.` }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export {
|
||||
checkLength,
|
||||
checkRegex
|
||||
};
|
7
src/routes/+layout.js
Normal file
7
src/routes/+layout.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ fetch, params }) {
|
||||
const res = await fetch(`/api/session`);
|
||||
const username = await res.text();
|
||||
|
||||
return { username };
|
||||
}
|
91
src/routes/+layout.svelte
Normal file
91
src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,91 @@
|
|||
<style>
|
||||
:global(:root) {
|
||||
--dark-1: #1a1b1d;
|
||||
--dark-2: #22242b;
|
||||
|
||||
--light-1: #ffffff;
|
||||
|
||||
--hyperlink: #97b9eb;
|
||||
|
||||
color: var(--light-1);
|
||||
font-family: 'Open Sans';
|
||||
}
|
||||
|
||||
:global(a) {
|
||||
color: var(--hyperlink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:global(input, textarea) {
|
||||
border: 0;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
#content {
|
||||
background: var(--dark-2);
|
||||
|
||||
height: 100vh;
|
||||
width: calc(100vw - 50px);
|
||||
padding: 25px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#logo {
|
||||
background: var(--dark-1);
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border-radius: 50px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#logo a {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#logo img {
|
||||
height: auto;
|
||||
width: 125px;
|
||||
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<div id='content'>
|
||||
<div id='logo'>
|
||||
<a href='/'>
|
||||
<img src='/logo_sanifae.svg' alt='Sanifae Logo'>
|
||||
</a>
|
||||
{#if data.username}
|
||||
<a href='/users/{data.username}'>
|
||||
{data.username}
|
||||
</a>
|
||||
<a href='/logout'>
|
||||
Log out
|
||||
</a>
|
||||
<a href='/new_post'>
|
||||
Create
|
||||
</a>
|
||||
{:else}
|
||||
<a href='/account'>
|
||||
Log in / Register
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
|
@ -1,2 +1,15 @@
|
|||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||
<script>
|
||||
import Area from '$lib/Area.svelte';
|
||||
</script>
|
||||
|
||||
<Area>
|
||||
<p slot="header">
|
||||
Welcome to SvelteKit
|
||||
</p>
|
||||
<p slot="main">
|
||||
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
||||
</p>
|
||||
<p slot="footer">
|
||||
|
||||
</p>
|
||||
</Area>
|
59
src/routes/account/+page.server.js
Normal file
59
src/routes/account/+page.server.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { checkLength, checkRegex } from '../../lib/util.js';
|
||||
import { registerBackend, loginBackend } from '../../lib/db.js';
|
||||
|
||||
async function basicChecks(data) {
|
||||
const pass = data.get('pass') + '';
|
||||
const user = data.get('user') + '';
|
||||
|
||||
var lengthCheck = false;
|
||||
|
||||
lengthCheck =
|
||||
checkLength(pass,'Password',4,1024) ||
|
||||
checkLength(user,'Username',1,64) ||
|
||||
checkRegex(user,'Username',/[^A-Za-z0-9\-\_]/g);
|
||||
|
||||
return {
|
||||
user,
|
||||
pass,
|
||||
lengthCheck
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
register: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
|
||||
const pass2 = data.get('pass2') + '';
|
||||
|
||||
var {lengthCheck, user, pass} = await basicChecks(data);
|
||||
|
||||
if (lengthCheck)
|
||||
return lengthCheck;
|
||||
|
||||
if (pass != pass2)
|
||||
return { success: 'Failed to confirm password.' };
|
||||
|
||||
return await registerBackend({ user, pass });
|
||||
},
|
||||
|
||||
login: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
|
||||
var {lengthCheck, user, pass} = await basicChecks(data);
|
||||
|
||||
if (lengthCheck)
|
||||
return lengthCheck;
|
||||
|
||||
var result = await loginBackend({ user, pass });
|
||||
|
||||
if (result.token) {
|
||||
cookies.set('token',result.token, {
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
37
src/routes/account/+page.svelte
Normal file
37
src/routes/account/+page.svelte
Normal file
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import Area from '$lib/Area.svelte';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
</script>
|
||||
|
||||
<Area>
|
||||
<p slot="header">
|
||||
Log in / Register
|
||||
</p>
|
||||
<form slot="main" method='POST'>
|
||||
<p>
|
||||
Username: <input name='user'>
|
||||
</p>
|
||||
<p>
|
||||
Password: <input type='password' name='pass'>
|
||||
</p>
|
||||
<p>
|
||||
Confirm Password: <input type='password' name='pass2'>
|
||||
</p>
|
||||
<p>
|
||||
<input formaction="?/login" type='submit' value='Log in'>
|
||||
</p>
|
||||
<p>
|
||||
<input formaction="?/register" type='submit' value='Register'>
|
||||
</p>
|
||||
</form>
|
||||
<p slot="footer">
|
||||
{#if form?.success}
|
||||
<p>{form?.success}</p>
|
||||
{/if}
|
||||
By using the Sanifae service, you agree to the <a href='https://insfa.net/rules'>Terms of Service</a>.
|
||||
</p>
|
||||
</Area>
|
10
src/routes/api/post/+server.js
Normal file
10
src/routes/api/post/+server.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { postGetBackend } from '../../../lib/db.js';
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET({ url, cookies }) {
|
||||
var id = url.searchParams.get('post');
|
||||
|
||||
var postData = await postGetBackend({ id });
|
||||
|
||||
return new Response(JSON.stringify(postData));
|
||||
};
|
11
src/routes/api/session/+server.js
Normal file
11
src/routes/api/session/+server.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { tokenBackend } from '../../../lib/db.js';
|
||||
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET({ url, cookies }) {
|
||||
var token = cookies.get('token') || '';
|
||||
|
||||
var username = await tokenBackend({token});
|
||||
|
||||
return new Response(username + '');
|
||||
};
|
27
src/routes/new_post/+page.server.js
Normal file
27
src/routes/new_post/+page.server.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { tokenBackend, postCreateBackend } from '../../lib/db.js';
|
||||
import { checkLength, checkRegex } from '../../lib/util.js';
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
create: async ({ request, cookies }) => {
|
||||
|
||||
var user = await tokenBackend({
|
||||
token: cookies.get('token')
|
||||
});
|
||||
|
||||
if (!user)
|
||||
return {'success': 'Not logged in.'}
|
||||
|
||||
const data = await request.formData();
|
||||
const content = data.get('content') + '';
|
||||
|
||||
var lengthCheck = checkLength(content,'Post content',1,10240);
|
||||
|
||||
if (lengthCheck)
|
||||
return lengthCheck;
|
||||
|
||||
await postCreateBackend({user, content});
|
||||
|
||||
return {'success': 'Successfully posted.'};
|
||||
}
|
||||
};
|
35
src/routes/new_post/+page.svelte
Normal file
35
src/routes/new_post/+page.svelte
Normal file
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import Area from '$lib/Area.svelte';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Area>
|
||||
<p slot="header">
|
||||
Create Post
|
||||
</p>
|
||||
<form slot="main" method='POST'>
|
||||
<p>
|
||||
<textarea name='content'></textarea>
|
||||
</p>
|
||||
<p>
|
||||
<input formaction="?/create" type='submit' value='Post'>
|
||||
</p>
|
||||
</form>
|
||||
<p slot="footer">
|
||||
{#if form?.success}
|
||||
<p>{form?.success}</p>
|
||||
{/if}
|
||||
Create a post for the world to see.
|
||||
</p>
|
||||
</Area>
|
12
src/routes/post/[post]/+page.js
Normal file
12
src/routes/post/[post]/+page.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ fetch, params, url }) {
|
||||
var id = params.post;
|
||||
const res = await fetch(`/api/post?post=${id}`);
|
||||
const postJson = await res.json();
|
||||
|
||||
var search = url.searchParams;
|
||||
|
||||
var voteType = (search.get('upvote')) ? 1 : ((search.get('downvote')) ? 2 : 0);
|
||||
|
||||
return { postJson };
|
||||
}
|
65
src/routes/post/[post]/+page.svelte
Normal file
65
src/routes/post/[post]/+page.svelte
Normal file
|
@ -0,0 +1,65 @@
|
|||
<script>
|
||||
import Area from '$lib/Area.svelte';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.button {
|
||||
width: auto;
|
||||
height: 75px;
|
||||
}
|
||||
.votes {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.vote-area {
|
||||
margin-right: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if data.postJson.success}
|
||||
<Area>
|
||||
<p slot="header">
|
||||
Error
|
||||
</p>
|
||||
<p slot="main">
|
||||
{data.postJson.success}
|
||||
</p>
|
||||
<p slot="footer">
|
||||
Failed to get post.
|
||||
</p>
|
||||
</Area>
|
||||
{:else}
|
||||
<Area>
|
||||
<p slot="header">
|
||||
<a href='/users/{data.postJson.username}'>
|
||||
{data.postJson.username}
|
||||
</a>
|
||||
</p>
|
||||
<p slot="main">
|
||||
{data.postJson.content}
|
||||
</p>
|
||||
<p slot="footer">
|
||||
<span class='vote-area'>
|
||||
<a href='?upvote'>
|
||||
<img src='/upvote.svg' class='button' alt='Upvote'>
|
||||
</a>
|
||||
<span class='votes'>
|
||||
{data.postJson.upvotes + 0}
|
||||
</span>
|
||||
</span>
|
||||
<span class='vote-area'>
|
||||
<a href='?downvote'>
|
||||
<img src='/downvote.svg' class='button' alt='Downvote'>
|
||||
</a>
|
||||
<span class='votes'>
|
||||
{data.postJson.downvotes + 0}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</Area>
|
||||
{/if}
|
Loading…
Add table
Add a link
Reference in a new issue