summaryrefslogtreecommitdiff
path: root/server/index.js
diff options
context:
space:
mode:
authorMarvin Borner2022-04-14 00:58:03 +0200
committerMarvin Borner2022-04-14 00:58:03 +0200
commit62146a790b7d11949dc987414d12c4eb4919e521 (patch)
tree931f1f38f07937e7764253304eb64cb519bc2fec /server/index.js
Basic featuresHEADmain
Diffstat (limited to 'server/index.js')
-rw-r--r--server/index.js177
1 files changed, 177 insertions, 0 deletions
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 0000000..02824c6
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,177 @@
+import process from 'node:process';
+import readline from 'node:readline';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
+import fileUpload from 'express-fileupload';
+import express from 'express';
+import dotenv from 'dotenv';
+import bcrypt from 'bcrypt';
+import {Low, JSONFile} from 'lowdb';
+
+const register = process.argv.includes('register');
+
+const db = new Low(new JSONFile('db.json'));
+await db.read();
+db.data ||= {tokens: [], users: []};
+const tokens = db.data.tokens;
+const users = db.data.users;
+
+const app = express();
+app.use(express.urlencoded({extended: true}));
+app.use(fileUpload({limits: {fileSize: 100 * 1024 * 1024}, useTempFiles: true, tempFileDir: '/tmp/'}));
+dotenv.config();
+
+const PORT = Number(process.env.PORT) || 3000;
+const TOKEN_LENGTH = Number(process.env.TOKEN_LENGTH) || 128;
+const SALT_ROUNDS = Number(process.env.SALT_ROUNDS) || 14;
+const UPLOAD_DIR = process.env.UPLOAD_DIR || `${path.dirname(fileURLToPath(import.meta.url))}/upload/`;
+
+if (register) {
+ const input = readline.createInterface({input: process.stdin, output: process.stdout});
+ input.question('username: ', username => {
+ if (username.length < 4 || users.some(user => user.username === username)) {
+ console.log('> username is invalid');
+ input.close();
+ return;
+ }
+
+ input.question('password: ', plain => {
+ if (plain.length < 9) {
+ console.log('> password is too insecure');
+ input.close();
+ return;
+ }
+
+ console.log('hashing...');
+ bcrypt.hash(plain, SALT_ROUNDS, async (error, password) => {
+ if (error) {
+ console.error(error);
+ input.close();
+ return;
+ }
+
+ users.push({username, password, saved: []});
+ console.log('storing...');
+ await db.write();
+ input.close();
+ console.log('success!');
+ });
+ });
+ });
+}
+
+const auth = (request, response, next) => {
+ if (!request.headers.token || request.headers.token.length !== TOKEN_LENGTH || !tokens.some(tok => tok.data === request.headers.token)) {
+ return response.status(401).json({state: 'unauthorized', error: 'invalid token'});
+ }
+
+ next();
+};
+
+const token = n => {
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ let token = '';
+ for (let i = 0; i < n; i++) {
+ token += chars[Math.floor(Math.random() * chars.length)];
+ }
+
+ return token;
+};
+
+const getUser = request => {
+ const token = tokens.find(tok => tok.data === request.headers.token);
+ if (!token) {
+ return undefined;
+ }
+
+ const user = users.find(user => user.username === token.username);
+ return user;
+};
+
+app.post('/login', (request, response) => {
+ if (!request.body.username || !request.body.password || !users.some(user => user.username === request.body.username)) {
+ return response.status(400).json({state: 'unauthorized', error: 'invalid parameters'});
+ }
+
+ const user = users.find(user => user.username === request.body.username);
+ if (!user || !user.password) {
+ return response.status(500).json({state: 'unauthorized', error: 'invalid password hash'});
+ }
+
+ const hash = user.password;
+ bcrypt.compare(request.body.password, hash).then(async authorized => {
+ if (!authorized) {
+ return response.status(401).json({state: 'unauthorized', error: 'invalid password'});
+ }
+
+ const tok = token(TOKEN_LENGTH);
+ // Indexing a token using a string instead of id is stupid but it's easier
+ tokens.push({username: user.username, data: tok});
+ await db.write();
+ response.json({state: 'authorized', token: tok, error: 'success'});
+ });
+});
+
+app.post('/text', auth, async (request, response) => {
+ const current = getUser(request);
+ if (!current || !request.body.data) {
+ return response.status(400).json({state: 'authorized', error: 'invalid parameters'});
+ }
+
+ current.saved.push({type: 'text', data: request.body.data});
+ await db.write();
+ response.json({state: 'authorized', error: 'success'});
+});
+
+app.post('/file', auth, (request, response) => {
+ const current = getUser(request);
+ if (!current) {
+ return response.status(500).json({state: 'authorized', error: 'invalid user'});
+ }
+
+ if (!request.files || Object.keys(request.files).length !== 1 || !request.files.file) {
+ return response.status(400).send({state: 'authorized', error: 'invalid file'});
+ }
+
+ const tok = token(16);
+ request.files.file.mv(UPLOAD_DIR + tok, async error => {
+ if (error) {
+ return response.status(500).send({state: 'authorized', error});
+ }
+
+ current.saved.push({type: 'file', name: request.files.file.name, mimetype: request.files.file.mimetype, token: tok});
+ await db.write();
+ response.json({state: 'authorized', error: 'success'});
+ });
+});
+
+app.get('/file/:token', auth, (request, response) => {
+ const current = getUser(request);
+ if (!current) {
+ return response.status(500).json({state: 'authorized', error: 'invalid user'});
+ }
+
+ const file = current.saved.find(file => file.type === 'file' && file.token === request.params.token);
+ if (!file) {
+ return response.status(404).json({state: 'authorized', error: 'file not found'});
+ }
+
+ response.download(UPLOAD_DIR + file.token, file.name, {maxAge: '10y', immutable: true});
+});
+
+app.get('/saved', auth, (request, response) => {
+ const current = getUser(request);
+ if (!current) {
+ return response.status(500).json({state: 'authorized', error: 'invalid user'});
+ }
+
+ response.json({state: 'authorized', error: 'success', data: current.saved});
+});
+
+app.get('/', auth, (_, response) => {
+ response.json({state: 'authorized', error: 'success'});
+});
+
+if (!register) {
+ app.listen(PORT, () => console.log(`listening on ${PORT}`));
+}