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}`)); }