diff options
author | Marvin Borner | 2022-04-14 00:58:03 +0200 |
---|---|---|
committer | Marvin Borner | 2022-04-14 00:58:03 +0200 |
commit | 62146a790b7d11949dc987414d12c4eb4919e521 (patch) | |
tree | 931f1f38f07937e7764253304eb64cb519bc2fec /server/index.js |
Diffstat (limited to 'server/index.js')
-rw-r--r-- | server/index.js | 177 |
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}`)); +} |