From b018c0e3675c34b0ea2d1fd5a381dadaf4815e94 Mon Sep 17 00:00:00 2001 From: Jane Petrovna Date: Fri, 9 Jul 2021 23:02:31 -0400 Subject: [PATCH 1/3] remove js backend --- backend/.prettierrc.yaml | 16 -- backend/README.md | 2 - backend/config.json.example | 16 -- backend/package.json | 36 --- backend/src/config.js | 21 -- backend/src/db_interface.js | 20 -- backend/src/index.js | 70 ----- backend/src/mail.js | 52 ---- backend/src/models.js | 141 ---------- backend/src/todo.js | 146 ---------- backend/src/user.js | 424 ------------------------------ backend/test/db_interface.spec.js | 17 -- backend/test/user.spec.js | 10 - 13 files changed, 971 deletions(-) delete mode 100644 backend/.prettierrc.yaml delete mode 100644 backend/README.md delete mode 100644 backend/config.json.example delete mode 100644 backend/package.json delete mode 100644 backend/src/config.js delete mode 100644 backend/src/db_interface.js delete mode 100644 backend/src/index.js delete mode 100644 backend/src/mail.js delete mode 100644 backend/src/models.js delete mode 100644 backend/src/todo.js delete mode 100644 backend/src/user.js delete mode 100644 backend/test/db_interface.spec.js delete mode 100644 backend/test/user.spec.js diff --git a/backend/.prettierrc.yaml b/backend/.prettierrc.yaml deleted file mode 100644 index ef5eace..0000000 --- a/backend/.prettierrc.yaml +++ /dev/null @@ -1,16 +0,0 @@ -arrowParens: 'always' -bracketSpacing: true -endOfLine: 'lf' -htmlWhitespaceSensitivity: 'css' -insertPragma: false -jsxBracketSameLine: true -jsxSingleQuote: true -printWidth: 120 -proseWrap: 'preserve' -quoteProps: 'consistent' -requirePragma: false -semi: true -singleQuote: true -tabWidth: 2 -trailingComma: 'none' -useTabs: false diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 85dfcb7..0000000 --- a/backend/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# todo - diff --git a/backend/config.json.example b/backend/config.json.example deleted file mode 100644 index 763b2b4..0000000 --- a/backend/config.json.example +++ /dev/null @@ -1,16 +0,0 @@ -{ - "secret": "TEST_SECRET", - "https": true, - "alter_db": true, - "port": 8080, - "frontend_url": "localhost:3000", - "db_url": "postgres://postgres:@127.0.0.1/todo", - "cert": "", - "cert_key": "", - "mail_host": "", - "mail_port": 465, - "mail_username": "", - "mail_password": "", - "discord_id": "", - "discord_secret": "" -} \ No newline at end of file diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 21a2f92..0000000 --- a/backend/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "todo", - "version": "1.0.0", - "description": "todo list app (because it hasnt been done before)", - "main": "src/index.js", - "scripts": { - "who": "pwd", - "start": "node src/index.js", - "test": "mocha" - }, - "repository": { - "type": "git", - "url": "git@ssh.gitdab.com:jane/todo.git" - }, - "author": "jane ", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "cookie-parser": "^1.4.5", - "cors": "^2.8.5", - "express": "^4.17.1", - "express-paginate": "^1.0.2", - "http-proxy": "^1.18.1", - "node-fetch": "^2.6.1", - "nodemailer": "^6.6.2", - "pg": "^8.6.0", - "sequelize": "^6.6.5" - }, - "devDependencies": { - "chai": "^4.3.4", - "mocha": "^9.0.2", - "proxyrequire": "^1.0.21", - "sequelize-cli": "^6.2.0", - "sequelize-test-helpers": "^1.3.3", - "sinon": "^11.1.1" - } -} \ No newline at end of file diff --git a/backend/src/config.js b/backend/src/config.js deleted file mode 100644 index 47634ed..0000000 --- a/backend/src/config.js +++ /dev/null @@ -1,21 +0,0 @@ -const fs = require('fs'); - -if (!global.config) { - global.config = {} - const cfg = JSON.parse(fs.readFileSync('./config.json')); - if (cfg) { - global.config = cfg; - } -} - -class Config { - get config() { - return global.config; - } - - set config(dat) { - global.config = dat; - } -} - -module.exports = new Config(); \ No newline at end of file diff --git a/backend/src/db_interface.js b/backend/src/db_interface.js deleted file mode 100644 index 605c5cb..0000000 --- a/backend/src/db_interface.js +++ /dev/null @@ -1,20 +0,0 @@ -const Sequelize = require('sequelize'); -const Config = require('./config.js'); -const Models = require('./models'); - -if (!Config.config.db_url) { - console.error('No database url found. please set `db_url` in config.json'); - process.exit(); -} - -const db = new Sequelize(Config.config.db_url); - -module.exports = { - db: db, - constructors: { - user: () => { - return User.build(); - } - }, - schemas: Models(db, Sequelize) -}; diff --git a/backend/src/index.js b/backend/src/index.js deleted file mode 100644 index d3b4995..0000000 --- a/backend/src/index.js +++ /dev/null @@ -1,70 +0,0 @@ -const http = require('http'); -const https = require('https'); -const httpProxy = require('http-proxy'); -const cors = require('cors'); -const express = require('express'); -const cookieParser = require('cookie-parser'); -const fs = require('fs'); -const Config = require('./config.js'); - -const UserInterface = require('./user.js'); -const TodoInterface = require('./todo.js'); - -let credentials = {}; - -if (Config.config.https) { - if (fs.existsSync(Config.config.cert) && fs.existsSync(Config.config.cert_key)) { - credentials.key = fs.readFileSync(Config.config.cert_key); - credentials.cert = fs.readFileSync(Config.config.cert); - } - else { - console.error('could not load certs') - process.exit() - } -} - -let app = express(); - -app.use(cors()); -app.use(cookieParser()); - -// force https -app.use((req, res, next) => { - if (Config.config.https) { - if (req.headers['x-forwarded-proto'] !== 'https') { - return res.redirect(`https://${req.headers.host}${req.url}`); - } - } - return next(); -}); - -if (!Config.config.secret) { - console.error('No password secret found. please set `secret` in config.json'); - process.exit(); -} else if (Config.config.https && Config.config.secret == 'TEST_SECRET') { - console.error('please do not use the testing secret in production.'); - process.exit(); -} - -app.use('/api/user', UserInterface.router); -app.use('/api/todo', TodoInterface.router); - -if (Config.config.frontend_url) { - const proxy = httpProxy.createProxyServer({}) - app.use('/', (req, res) => { - return proxy.web(req, res, { - target: Config.config.frontend_url - }) - }); -} - -if (Config.config.https) { - var server = https.createServer(credentials, app); - server.listen(Config.config.port || 8080); -} else { - var server = http.createServer(app); - server.listen(Config.config.port || 8080); -} -console.log( - `listening on port ${Config.config.port || 8080}` + ` with https ${Config.config.https ? 'enabled' : 'disabled'}` -); diff --git a/backend/src/mail.js b/backend/src/mail.js deleted file mode 100644 index 49e57c4..0000000 --- a/backend/src/mail.js +++ /dev/null @@ -1,52 +0,0 @@ -const Config = require('./config.js'); -const nodemailer = require('nodemailer'); - -class Mailer { - sender; - started = false; - mailer; - constructor(host, port, email, password) { - this.mailer = nodemailer.createTransport({ - host: host, - port: port, - secure: true, - auth: { - user: email, - pass: password - } - }); - this.sender = email; - this.started = true; - } - async sendMail(recipients, subject, content, contentStripped) { - console.log(`sending mail to ${recipients}`); - let info = await this.mailer.sendMail({ - from: `"Todo App" <${this.sender}>`, - to: Array.isArray(recipients) ? recipients.join(', ') : recipients, - subject: subject, - text: contentStripped, - html: content - }); - } -} - -if (!global.mailer || !global.mailer.started) { - if ( - !Config.config['mail_host'] || - !Config.config['mail_port'] || - !Config.config['mail_username'] || - !Config.config['mail_password'] - ) { - console.error(`could not create email account as -mail_host, mail_port, mail_username or mail_password is not set.`); - process.exit(); - } - global.mailer = new Mailer( - Config.config['mail_host'], - Config.config['mail_port'], - Config.config['mail_username'], - Config.config['mail_password'] - ); -} - -module.exports = global.mailer; diff --git a/backend/src/models.js b/backend/src/models.js deleted file mode 100644 index 4b219e2..0000000 --- a/backend/src/models.js +++ /dev/null @@ -1,141 +0,0 @@ -const Sequelize = require('sequelize'); -const Config = require('./config.js'); - -const models = (db) => { - const UnverifiedUser = db.define('UnverifiedUser', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - verificationToken: { - type: Sequelize.DataTypes.STRING, - allowNull: false - }, - email: { - type: Sequelize.DataTypes.STRING, - allowNull: false, - unique: true - }, - discord_only_account: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - discord_id: { - type: Sequelize.DataTypes.STRING, - allowNull: true - }, - password_hash: { - type: Sequelize.DataTypes.STRING, - allowNull: true - } - }); - - const User = db.define('User', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - email: { - type: Sequelize.DataTypes.STRING, - allowNull: false, - unique: true - }, - discord_only_account: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - discord_id: { - type: Sequelize.DataTypes.STRING, - allowNull: true - }, - password_hash: { - type: Sequelize.DataTypes.STRING, - allowNull: true - } - }); - - const Todo = db.define('Todo', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - user: { - type: Sequelize.DataTypes.UUID, - allowNull: false - }, - content: { - type: Sequelize.DataTypes.TEXT, - allowNull: false - }, - tags: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - }, - complete: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - deadline: { - type: Sequelize.DataTypes.DATE, - allowNull: true - } - }); - - const Grouping = db.define('Grouping', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - complete: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: true - }, - manually_added: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.UUID), - allowNull: true - }, - required: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - }, - exclusions: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - } - }); - - let options = { - alter: false - }; - if (Config.config.alter_db) { - options.alter = true; - } - UnverifiedUser.sync(options); - User.sync(options); - Todo.sync(options); - Grouping.sync(options); - - return { - user: User, - unverifiedUser: UnverifiedUser, - todo: Todo, - grouping: Grouping - }; -}; - -module.exports = models; diff --git a/backend/src/todo.js b/backend/src/todo.js deleted file mode 100644 index bc31195..0000000 --- a/backend/src/todo.js +++ /dev/null @@ -1,146 +0,0 @@ -const express = require('express'); -const paginate = require('express-paginate'); -const Database = require('./db_interface.js'); -const User = require('./user.js'); -const { Op } = require('sequelize'); - -let router = express.Router(); - -router.use(express.json()); - -function map_todo(result) { - return { - id: result.id, - content: result.content, - tags: result.tags - }; -} - -function parse_tags(tags) { - result = { - complete: undefined, - required: [], - excluded: [] - }; - tags.map((tag) => { - if (tag === 'complete') { - complete = true; - } else if (tag === '~complete') { - complete = false; - } else if (tag.startsWith('~')) { - excluded.push(tag); - } else { - required.push(tag); - } - }); - return result; -} - -const todo_fields = ['currentPage', 'limit']; - -router.use(paginate.middleware(10, 50)); -router.use('/todos', User.enforce_session_login); -router.get('/todos', async (req, res) => { - if (!req.query) { - return res.status(400).json({ - error: `query must include the fields: ${todo_fields.join(', ')}}` - }); - } else { - let error = []; - for (let field of todo_fields) { - if (!req.query[field]) { - error.push(field); - } - } - if (error.length > 0) { - return res.status(400).json({ - error: `query must include the fields: ${error.join(', ')}}` - }); - } - } - let tag_options = {}; - if (req.query.tags) { - let parsed = parse_tags(req.query.tags.split(',')); - tag_options['tags'] = { - [Op.and]: parsed.required, - [Op.not]: parsed.excluded - }; - if (parsed.complete !== undefined) { - tag_options['complete'] = { - [Op.is]: parsed.complete - }; - } - } - console.log(tag_options); - let all_todos = await Database.schemas.todo.findAndCountAll({ - where: { - user: req.get('id'), - ...tag_options - }, - limit: req.query.limit, - offset: req.skip - }); - const item_count = all_todos.count; - const page_count = Math.ceil(item_count / req.query.limit); - res.json({ - result: all_todos.map(map_todo), - currentPage: req.query.currentPage, - pageCount: page_count, - itemCount: item_count, - pages: paginate.getArrayPages(req)(5, page_count, req.query.currentPage) - }); -}); - -router.use('/todo', User.enforce_session_login); -router.get('/todo/:id([a-f0-9-]+)', async (req, res) => { - let userid = req.get('id'); - let id = req.params?.id; - - let match = await Database.schemas.todo.findOne({ - where: { - user: userid, - id: id - } - }); - if (!match) { - return res.sendStatus(404); - } - - return res.json({ - result: map_todo(match), - tags: get_tags(match.id) - }); -}); - -router.use('/todo', User.enforce_session_login); -router.post('/todo/:id([a-f0-9-]+)', async (req, res) => { - let userid = req.get('id'); - let id = req.params?.id; - - let body = req.body; - - if (!body) { - return res.sendStatus(400); - } - - let match = await Database.schemas.todo.findOne({ - where: { - user: userid, - id: id - } - }); - if (!match) { - return res.sendStatus(404); - } - - // - - return res.json({ - result: map_todo(match), - tags: get_tags(match.id) - }); -}); - -module.exports = { - router: router -}; diff --git a/backend/src/user.js b/backend/src/user.js deleted file mode 100644 index 151d1c3..0000000 --- a/backend/src/user.js +++ /dev/null @@ -1,424 +0,0 @@ -const express = require('express'); -const crypto = require('crypto'); -const fetch = require('node-fetch'); -const Config = require('./config.js'); -const Database = require('./db_interface.js'); -const Mail = require('./mail.js'); - -let router = express.Router(); - -router.use(express.json()); - -let session_entropy = {}; - -user_cache = {}; -email_cache = {}; -discord_cache = {}; -discord_user_cache = {}; - -async function fetch_user(where) { - let user = await Database.schemas.user.findOne({ where: where }); - if (user === null) { - return undefined; - } - user_cache[user.id] = { - id: user.id, - email: user.email, - discord_id: user.discord_id, - password_hash: user.password_hash - }; - email_cache[user.email] = user.id; - if (user.discord_id) { - discord_cache[user.discord_id] = user.id - } - return user_cache[user.id] -} - -async function fetch_discord_user(auth) { - const result = await fetch(`https://discord.com/api/v8/users/@me`, { - headers: { - 'Authorization': auth.token_type + ' ' + auth.access_token - } - }); - const json = result.json(); - discord_user_cache[json.id] = { - user: json, - auth: auth, - expires: (new Date().getTime()) + (json.expires_in * 1000) - } - return discord_user_cache[id]; -} - -async function acquire_discord_token(code, redirect) { - let data = { - client_id: Config.config.discord_id, - client_secret: Config.config.discord_secret, - grant_type: 'authorization_code', - code: code, - redirect_uri: redirect - } - const result = await fetch(`https://discord.com/api/oauth2/token`, { - method: 'POST', - body: data, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }).catch(err => console.error(err)); - if (!result.ok) { - return res.status(500).json({error: "could not fetch user details"}) - } - const json = result.json(); - return fetch_discord_user(json); -} - -async function refresh_discord_token(id) { - let data = { - client_id: Config.config.discord_id, - client_secret: Config.config.discord_secret, - grant_type: 'refresh_token', - refresh_token: discord_user_cache[id].auth.refresh_token - } - const result = await fetch(`https://discord.com/api/oauth2/token`, { - method: 'POST', - body: data, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }).catch(err => console.error(err)); - if (!result.ok) { - return false; - } - const json = result.json(); - discord_user_cache[id].auth.access_token = json.access_token; - discord_user_cache[id].expires = (new Date().getTime()) + (json.expires_in * 1000); - return true; -} - -async function get_user_details(id) { - if (!id || id === 'undefined') { - return undefined; - } - console.log(`search for user with id ${id}`); - if (!user_cache[id]) { - return await fetch_user({ id: id }) - } - // console.log(`returning ${JSON.stringify(user_cache[id])}`); - return user_cache[id]; -} - -async function get_user_details_by_email(email) { - if (!email || email === 'undefined') { - return undefined; - } - console.log(`search for user with email ${email}}`); - if (!email_cache[email] || !user_cache[email_cache[email]]) { - return await fetch_user({ email: email }) - } - // console.log(`returning ${JSON.stringify(user_cache[email_cache[email]])}`); - return user_cache[email_cache[email]]; -} - -async function get_user_details_by_discord_id(id) { - if (!id || id === 'undefined') { - return undefined; - } - if (!discord_cache[id] || !user_cache[discord_cache[id]]) { - return await fetch_user({ discord_id: id }) - } - return user_cache[discord_cache[id]]; -} - -function hash(secret, password, base64 = true) { - let pw_hash = crypto.pbkdf2Sync( - password, - secret, - Config.config.key?.iterations || 1000, - Config.config.key?.length || 64, - 'sha512' - ); - - return pw_hash.toString(base64 ? 'base64' : 'hex'); -} - -function verify(secret, password, hash) { - let pw_hash = crypto.pbkdf2Sync( - password, - secret, - Config.config.key?.iterations || 1000, - Config.config.key?.length || 64, - 'sha512' - ); - - return hash === pw_hash.toString('base64'); -} - -function hash_password(password) { - return hash(Config.config.secret, password); -} - -function verify_password(password, hash) { - return verify(Config.config.secret, password, hash); -} - -function get_session_token(id, password_hash, base64 = true) { - session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32); - return hash(session_entropy[id], password_hash, base64); -} - -function verify_session_token(id, hash, token) { - if (session_entropy[id]) { - return verify(session_entropy[id], hash, token); - } else { - return false; - } -} - -async function enforce_session_login(req, res, next) { - let userid = req.get('id'); - let session_token = req.get('authorization'); - console.log('a', userid, session_token); - if (!userid || !session_token) { - return res.sendStatus(401); - } - let user = await get_user_details(userid); - if (!user) { - return res.sendStatus(401); - } - let verified_session = verify_session_token(userid, user.password_hash, session_token); - if (!verified_session) { - return res.sendStatus(401); - } - return next(); -} - -router.post('/signup', async (req, res) => { - if (!req.body?.email || !req.body?.password) { - return res.status(400).json({ - error: 'must have email and password fields' - }); - } - let user = await get_user_details_by_email(req.body?.email); - - if (user !== undefined && user !== {}) { - console.warn(`user already found: ${JSON.stringify(user)}`); - return res.status(403).json({ - error: `email ${req.body.email} is already in use.` - }); - } else { - let match = await Database.schemas.unverifiedUser.findOne({ where: { email: req.body.email } }); - if (!!match) { - await Database.schemas.unverifiedUser.destroy({ where: { email: match.email } }); - } - let randomString = 'Signup'; - for (let i = 0; i < 16; i++) { - randomString += Math.floor(Math.random() * 10); - } - let password_hash = hash_password(req.body.password); - let user = await Database.schemas.unverifiedUser.create({ - email: String(req.body.email), - password_hash: password_hash, - verificationToken: get_session_token(randomString, password_hash, false) - }); - const link = `${Config.config.https ? 'https://' : 'http://'}${req.headers.host}/api/user/verify?verification=${user.verificationToken - }`; - const content = `Click here to verify your sign-up: -${link}`; - const contentHtml = `

Click here to verify your sign-up:

-

${link}

`; - await Mail.sendMail([String(req.body.email)], 'Verify Your Account', contentHtml, content); - return res.sendStatus(204); - } -}); - -router.get('/verify', async (req, res) => { - if (!req.query?.verification) { - return res.status(400).send( - ` - -

No Verification Link

- - ` - ); - } - let verification = req.query?.verification; - let user = await Database.schemas.unverifiedUser.findOne({ where: { verificationToken: verification } }); - - if (user !== undefined && user !== {}) { - if (user.verificationToken != verification) { - return res.status(404).send( - ` - -

Unknown Verification Link

- - ` - ); - } - let newUser = await Database.schemas.user.create({ - email: user.email, - password_hash: user.password_hash - }); - - return res.send(` - -

Sign up complete.

- - `); - } else { - return res.status(404).send(` - -

Unknown Verification Link

- - `); - } -}); - -router.get('/login/discord', async (req, res) => { - if (!Config.config.discord_id || !Config.config.discord_secret) { - return res.status(403).send("discord login is not enabled."); - } - const url = encodeURIComponent(`${req.headers.host}discord`); - return res.send(`https://discord.com/api/oauth2/authorize?client_id=${Config.config.discord_id}&redirect_uri=${url}&response_type=code&scope=identify%20email%20guilds`); -}); - -router.post('/login/discord', async (req, res) => { - if (!Config.config.discord_id || !Config.config.discord_secret) { - return res.status(403).json({ error: "discord login is not enabled." }); - } - if (!req.params.code || !req.headers.host) { - return res.status(400).json({error: "invalid oauth request"}); - } - const result = await acquire_discord_token(req.params.code, req.headers.host); - const matching_account = await get_user_details_by_discord_id(result.user.id); - if (!matching_account) { - let user = await Database.schemas.unverifiedUser.create({ - email: String(result.user.email), - discord_id: user.id, - verificationToken: get_session_token(randomString, result.auth.access_token, false) - }); - return res.json({ - type: 'unverified', - verificationToken: user.verificationToken - }) - } - return res.json({ - type: 'verified', - userid: matching_account.id, - session_token: get_session_token(matching_account.id, result.auth.access_token) - }); -}); - -//TODO -router.post('/discord/create', async (req, res) =>{}); -router.post('/discord/link', async (req, res) =>{}); - -router.post('/login', async (req, res) => { - if (!req.body?.email || !req.body?.password) { - return res.status(400).json({ - error: 'must have email and password fields' - }); - } - let user = await get_user_details_by_email(req.body.email); - if (!user) { - return res.status(401).json({ - error: 'incorrect email or password' - }); - } - let verified = verify_password(req.body.password, user.password_hash); - - if (!verified) { - return res.status(401).json({ - error: 'incorrect email or password' - }); - } - - return res.json({ - userid: user.id, - session_token: get_session_token(user.id, user.password_hash) - }); -}); - - -router.use('/logout', enforce_session_login) -router.post('/logout', async (req, res) => { - let userid = req.get('id'); - let session_token = req.get('authorization'); - - let user = await get_user_details(userid); - if (!user) { - return res.status(401).json({ - error: 'invalid user data' - }); - } - let verified = verify_session_token(user.id, user.password_hash, session_token); - - if (!verified) { - return res.status(401).json({ - error: 'invalid user data' - }); - } - - delete session_entropy[user.id]; - return res.sendStatus(204); -}); - -router.use('/byEmail', enforce_session_login) -router.get('/byEmail/:email', async (req, res) => { - if (!req.params?.email) { - res.status(400).json({ - error: 'email is a required parameter' - }); - } - let user = get_user_details_by_email(req.params.email); - console.log(user); - if (user !== undefined && user !== {}) { - res.json({ - id: user.id, - email: user.email - }); - } else { - res.sendStatus(404); - } -}); - - -router.use('/', enforce_session_login) -router.get('/:id([a-f0-9-]+)', async (req, res) => { - console.log(req.params); - if (!req.params?.id) { - return res.status(400).json({ - error: 'must have id parameter' - }); - } - let id = req.params?.id; - console.log(id); - let user = await get_user_details(id); - console.log(user); - if (user !== undefined && user !== {}) { - return res.json({ - id: user.id, - email: user.email - }); - } else { - return res.sendStatus(404); - } -}); - -router.use('/authorized', enforce_session_login); -router.get('/authorized', async (req, res) => { - let userid = req.get('id'); - let user = await get_user_details(userid); - return res.json({ - authorized: true, - user: { - id: user.id, - email: user.email - } - }); -}); - -module.exports = { - router: router, - enforce_session_login: enforce_session_login, - get_user_details: get_user_details, - get_user_details_by_email: get_user_details_by_email -}; diff --git a/backend/test/db_interface.spec.js b/backend/test/db_interface.spec.js deleted file mode 100644 index cf2fe82..0000000 --- a/backend/test/db_interface.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -const { expect } = require('chai'); -const Models = require('../src/models'); - -const { sequelize, checkModelName, checkUniqueIndex, checkPropertyExists } = require('sequelize-test-helpers'); - -describe('Sequelize model tests', function () { - const models = Models(sequelize); - - checkModelName(models.user)('User'); - checkModelName(models.unverifiedUser)('UnverifiedUser'); - checkModelName(models.grouping)('Grouping'); - checkModelName(models.todo)('Todo'); - - context('user props', function () { - ['id', 'email', 'discord_only_account'].forEach(checkPropertyExists(new models.user())); - }); -}); diff --git a/backend/test/user.spec.js b/backend/test/user.spec.js deleted file mode 100644 index 2861017..0000000 --- a/backend/test/user.spec.js +++ /dev/null @@ -1,10 +0,0 @@ -const { expect } = require('chai'); -const proxyrequire = require('proxyrequire'); -const { match, stub, resetHistory } = require('sinon'); -const { sequelize, Sequelize, makeMockModels } = require('sequelize-test-helpers'); - -describe('User Router Tests', function () { - const Database = proxyrequire('../src/db_interface', { - sequelize: Sequelize - }); -}); From 840959ec25fdba9e63d8d51d4556300e48426c9f Mon Sep 17 00:00:00 2001 From: Jane Petrovna Date: Fri, 9 Jul 2021 23:03:57 -0400 Subject: [PATCH 2/3] crystal init --- .gitignore | 5 +++++ backend/.editorconfig | 9 +++++++++ backend/.gitignore | 5 +++++ backend/.travis.yml | 6 ++++++ backend/LICENSE | 21 +++++++++++++++++++++ backend/README.md | 27 +++++++++++++++++++++++++++ backend/shard.yml | 13 +++++++++++++ backend/spec/backend_spec.cr | 9 +++++++++ backend/spec/spec_helper.cr | 2 ++ backend/src/backend.cr | 6 ++++++ 10 files changed, 103 insertions(+) create mode 100644 backend/.editorconfig create mode 100644 backend/.gitignore create mode 100644 backend/.travis.yml create mode 100644 backend/LICENSE create mode 100644 backend/README.md create mode 100644 backend/shard.yml create mode 100644 backend/spec/backend_spec.cr create mode 100644 backend/spec/spec_helper.cr create mode 100644 backend/src/backend.cr diff --git a/.gitignore b/.gitignore index 50a664d..0397954 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,8 @@ pnpm-lock.yaml backend/config.json *.pem +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..163eb75 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..0bb75ea --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,5 @@ +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf diff --git a/backend/.travis.yml b/backend/.travis.yml new file mode 100644 index 0000000..765f0e9 --- /dev/null +++ b/backend/.travis.yml @@ -0,0 +1,6 @@ +language: crystal + +# Uncomment the following if you'd like Travis to run specs and check code formatting +# script: +# - crystal spec +# - crystal tool format --check diff --git a/backend/LICENSE b/backend/LICENSE new file mode 100644 index 0000000..9689b4f --- /dev/null +++ b/backend/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Jane Petrovna + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..70887de --- /dev/null +++ b/backend/README.md @@ -0,0 +1,27 @@ +# backend + +TODO: Write a description here + +## Installation + +TODO: Write installation instructions here + +## Usage + +TODO: Write usage instructions here + +## Development + +TODO: Write development instructions here + +## Contributing + +1. Fork it () +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request + +## Contributors + +- [Jane Petrovna](https://github.com/your-github-user) - creator and maintainer diff --git a/backend/shard.yml b/backend/shard.yml new file mode 100644 index 0000000..5af70bb --- /dev/null +++ b/backend/shard.yml @@ -0,0 +1,13 @@ +name: backend +version: 0.1.0 + +authors: + - Jane Petrovna + +targets: + backend: + main: src/backend.cr + +crystal: 1.0.0 + +license: MIT diff --git a/backend/spec/backend_spec.cr b/backend/spec/backend_spec.cr new file mode 100644 index 0000000..197b354 --- /dev/null +++ b/backend/spec/backend_spec.cr @@ -0,0 +1,9 @@ +require "./spec_helper" + +describe Backend do + # TODO: Write tests + + it "works" do + false.should eq(true) + end +end diff --git a/backend/spec/spec_helper.cr b/backend/spec/spec_helper.cr new file mode 100644 index 0000000..4e1e289 --- /dev/null +++ b/backend/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/backend" diff --git a/backend/src/backend.cr b/backend/src/backend.cr new file mode 100644 index 0000000..4b62aae --- /dev/null +++ b/backend/src/backend.cr @@ -0,0 +1,6 @@ +# TODO: Write documentation for `Backend` +module Backend + VERSION = "0.1.0" + + # TODO: Put your code here +end From 24acfa5846c44756c5043fd75553de0336e32356 Mon Sep 17 00:00:00 2001 From: Jane Petrovna Date: Sat, 24 Jul 2021 17:17:10 -0400 Subject: [PATCH 3/3] use jennifer for db interface --- .gitignore | 2 + backend/config.json.example | 9 ++ backend/config/config.cr | 2 + backend/config/database.yml | 16 ++ backend/config/initializers/database.cr | 12 ++ backend/config/initializers/zzz_i18n.cr | 3 + backend/config/locales/en.yml | 0 .../20210723170518920_create_users.cr | 19 +++ ...0210723171718613_create_unverifiedusers.cr | 20 +++ .../20210723171731912_create_todos.cr | 20 +++ backend/db/structure.sql | 152 ++++++++++++++++++ backend/sam.cr | 13 ++ backend/shard.lock | 78 +++++++++ backend/shard.yml | 24 +++ backend/spec/backend_spec.cr | 9 -- backend/spec/spec_helper.cr | 6 +- backend/spec/utils/config_spec.cr | 61 +++++++ backend/src/backend.cr | 34 +++- backend/src/endpoints/auth.cr | 0 backend/src/endpoints/register.cr | 0 backend/src/endpoints/todo.cr | 0 backend/src/endpoints/user.cr | 0 backend/src/models/todo.cr | 16 ++ backend/src/models/unverified_user.cr | 16 ++ backend/src/models/user.cr | 15 ++ backend/src/utils/config.cr | 53 ++++++ backend/src/utils/database_caching.cr | 5 + frontend/package.json | 8 +- 28 files changed, 575 insertions(+), 18 deletions(-) create mode 100644 backend/config.json.example create mode 100644 backend/config/config.cr create mode 100644 backend/config/database.yml create mode 100644 backend/config/initializers/database.cr create mode 100644 backend/config/initializers/zzz_i18n.cr create mode 100644 backend/config/locales/en.yml create mode 100644 backend/db/migrations/20210723170518920_create_users.cr create mode 100644 backend/db/migrations/20210723171718613_create_unverifiedusers.cr create mode 100644 backend/db/migrations/20210723171731912_create_todos.cr create mode 100644 backend/db/structure.sql create mode 100644 backend/sam.cr create mode 100644 backend/shard.lock delete mode 100644 backend/spec/backend_spec.cr create mode 100644 backend/spec/utils/config_spec.cr create mode 100644 backend/src/endpoints/auth.cr create mode 100644 backend/src/endpoints/register.cr create mode 100644 backend/src/endpoints/todo.cr create mode 100644 backend/src/endpoints/user.cr create mode 100644 backend/src/models/todo.cr create mode 100644 backend/src/models/unverified_user.cr create mode 100644 backend/src/models/user.cr create mode 100644 backend/src/utils/config.cr create mode 100644 backend/src/utils/database_caching.cr diff --git a/.gitignore b/.gitignore index 0397954..54ed519 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,5 @@ backend/config.json /bin/ /.shards/ *.dwarf +Makefile +.vscode/ diff --git a/backend/config.json.example b/backend/config.json.example new file mode 100644 index 0000000..4999981 --- /dev/null +++ b/backend/config.json.example @@ -0,0 +1,9 @@ +{ + "secret": "TEST_SECRET", + "port": 8080, + "db_url": "postgres://postgres:@127.0.0.1/todo", + "mail_host": "smtp.migadu.com", + "mail_port": 465, + "mail_username": "", + "mail_password": "" +} \ No newline at end of file diff --git a/backend/config/config.cr b/backend/config/config.cr new file mode 100644 index 0000000..c7dcc81 --- /dev/null +++ b/backend/config/config.cr @@ -0,0 +1,2 @@ +require "./initializers/**" +require "../src/models/*" diff --git a/backend/config/database.yml b/backend/config/database.yml new file mode 100644 index 0000000..c81bab7 --- /dev/null +++ b/backend/config/database.yml @@ -0,0 +1,16 @@ +default: &default + host: localhost + user: postgres + adapter: postgres + +development: + <<: *default + db: todo_dev + +test: + <<: *default + db: todo_dev + +production: + <<: *default + db: todo \ No newline at end of file diff --git a/backend/config/initializers/database.cr b/backend/config/initializers/database.cr new file mode 100644 index 0000000..519a926 --- /dev/null +++ b/backend/config/initializers/database.cr @@ -0,0 +1,12 @@ +require "jennifer" +require "jennifer/adapter/postgres" + +APP_ENV = ENV["APP_ENV"]? || "development" + +Jennifer::Config.configure do |conf| + conf.read("config/database.yml", APP_ENV) + conf.from_uri(ENV["DATABASE_URI"]) if ENV.has_key?("DATABASE_URI") + conf.logger.level = APP_ENV == "development" ? Log::Severity::Debug : Log::Severity::Info +end + +Log.setup "db", :debug, Log::IOBackend.new(formatter: Jennifer::Adapter::DBFormatter) diff --git a/backend/config/initializers/zzz_i18n.cr b/backend/config/initializers/zzz_i18n.cr new file mode 100644 index 0000000..26e3268 --- /dev/null +++ b/backend/config/initializers/zzz_i18n.cr @@ -0,0 +1,3 @@ +I18n.load_path += ["./config/locales"] + +I18n.init diff --git a/backend/config/locales/en.yml b/backend/config/locales/en.yml new file mode 100644 index 0000000..e69de29 diff --git a/backend/db/migrations/20210723170518920_create_users.cr b/backend/db/migrations/20210723170518920_create_users.cr new file mode 100644 index 0000000..44238f0 --- /dev/null +++ b/backend/db/migrations/20210723170518920_create_users.cr @@ -0,0 +1,19 @@ +require "jennifer" + +class CreateUsers < Jennifer::Migration::Base + def up + create_table :users do |t| + t.string :id, {:primary => true} + t.string :email + t.bool :discord_only_account + t.string :discord_id, {:null => true} + t.string :password_hash, {:null => true} + + t.timestamps + end + end + + def down + drop_table :users if table_exists? :users + end +end diff --git a/backend/db/migrations/20210723171718613_create_unverifiedusers.cr b/backend/db/migrations/20210723171718613_create_unverifiedusers.cr new file mode 100644 index 0000000..d9628f3 --- /dev/null +++ b/backend/db/migrations/20210723171718613_create_unverifiedusers.cr @@ -0,0 +1,20 @@ +require "jennifer" + +class CreateUnverifiedusers < Jennifer::Migration::Base + def up + create_table :unverifiedusers do |t| + t.string :id, {:primary => true} + t.string :email + t.bool :discord_only_account + t.string :discord_id, {:null => true} + t.string :password_hash, {:null => true} + t.string :verification_token + + t.timestamps + end + end + + def down + drop_table :unverifiedusers if table_exists? :unverifiedusers + end +end diff --git a/backend/db/migrations/20210723171731912_create_todos.cr b/backend/db/migrations/20210723171731912_create_todos.cr new file mode 100644 index 0000000..f020265 --- /dev/null +++ b/backend/db/migrations/20210723171731912_create_todos.cr @@ -0,0 +1,20 @@ +require "jennifer" + +class CreateTodos < Jennifer::Migration::Base + def up + create_table :todos do |t| + t.string :id, {:primary => true} + t.string :userid # remember: trying to insert a column named the same as + # another model will make the database error :) + t.text :content + t.string :tags, {:array => true, :null => true} + t.bool :complete, {:null => true} + t.date_time :deadline, {:null => true} + t.timestamps + end + end + + def down + drop_table :todos if table_exists? :todos + end +end diff --git a/backend/db/structure.sql b/backend/db/structure.sql new file mode 100644 index 0000000..84a3679 --- /dev/null +++ b/backend/db/structure.sql @@ -0,0 +1,152 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 13.3 +-- Dumped by pg_dump version 13.3 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: migration_versions; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.migration_versions ( + id integer NOT NULL, + version character varying(17) NOT NULL +); + + +ALTER TABLE public.migration_versions OWNER TO postgres; + +-- +-- Name: migration_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.migration_versions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.migration_versions_id_seq OWNER TO postgres; + +-- +-- Name: migration_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.migration_versions_id_seq OWNED BY public.migration_versions.id; + + +-- +-- Name: todos; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.todos ( + id character varying(254) NOT NULL, + userid character varying(254), + content text, + tags character varying(254)[], + complete boolean, + deadline timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +ALTER TABLE public.todos OWNER TO postgres; + +-- +-- Name: unverifiedusers; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.unverifiedusers ( + id character varying(254) NOT NULL, + email character varying(254), + discord_only_account boolean, + discord_id character varying(254), + password_hash character varying(254), + verification_token character varying(254), + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +ALTER TABLE public.unverifiedusers OWNER TO postgres; + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.users ( + id character varying(254) NOT NULL, + email character varying(254), + discord_only_account boolean, + discord_id character varying(254), + password_hash character varying(254), + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +ALTER TABLE public.users OWNER TO postgres; + +-- +-- Name: migration_versions id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.migration_versions ALTER COLUMN id SET DEFAULT nextval('public.migration_versions_id_seq'::regclass); + + +-- +-- Name: migration_versions migration_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.migration_versions + ADD CONSTRAINT migration_versions_pkey PRIMARY KEY (id); + + +-- +-- Name: todos todos_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.todos + ADD CONSTRAINT todos_pkey PRIMARY KEY (id); + + +-- +-- Name: unverifiedusers unverifiedusers_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.unverifiedusers + ADD CONSTRAINT unverifiedusers_pkey PRIMARY KEY (id); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/backend/sam.cr b/backend/sam.cr new file mode 100644 index 0000000..92a3974 --- /dev/null +++ b/backend/sam.cr @@ -0,0 +1,13 @@ +require "./config/*" +require "sam" +require "./db/migrations/*" + +load_dependencies "jennifer" + +# Here you can define your tasks +# desc "with description to be used by help command" +# task "test" do +# puts "ping" +# end + +Sam.help diff --git a/backend/shard.lock b/backend/shard.lock new file mode 100644 index 0000000..2413bfa --- /dev/null +++ b/backend/shard.lock @@ -0,0 +1,78 @@ +version: 2.0 +shards: + ameba: + git: https://github.com/crystal-ameba/ameba.git + version: 0.14.3 + + backtracer: + git: https://github.com/sija/backtracer.cr.git + version: 1.2.1 + + base62: + git: https://github.com/janeptrv/base62.cr.git + version: 0.1.3 + + db: + git: https://github.com/crystal-lang/crystal-db.git + version: 0.10.1 + + email: + git: https://github.com/arcage/crystal-email.git + version: 0.6.3 + + exception_page: + git: https://github.com/crystal-loot/exception_page.git + version: 0.2.0 + + i18n: + git: https://github.com/techmagister/i18n.cr.git + version: 0.3.1+git.commit.b323291b772c97bc1661888eb9e82dadb722acaa + + ifrit: + git: https://github.com/imdrasil/ifrit.git + version: 0.1.3 + + inflector: + git: https://github.com/phoffer/inflector.cr.git + version: 1.0.0 + + jennifer: + git: https://github.com/imdrasil/jennifer.cr.git + version: 0.11.0 + + kemal: + git: https://gitdab.com/luna/kemal.git + version: 1.0.0+git.commit.bba5bef50506f7572db9fcdeb107c65709bf1244 + + kilt: + git: https://github.com/jeromegn/kilt.git + version: 0.6.1 + + ksuid: + git: https://github.com/janeptrv/ksuid.cr.git + version: 0.5.2 + + pg: + git: https://github.com/will/crystal-pg.git + version: 0.23.2 + + pool: + git: https://github.com/ysbaddaden/pool.git + version: 0.2.4 + + radix: + git: https://github.com/luislavena/radix.git + version: 0.4.1 + + redis: + git: https://github.com/stefanwille/crystal-redis.git + version: 2.8.0 + + sam: + git: https://github.com/imdrasil/sam.cr.git + version: 0.4.1 + + spec-kemal: + git: https://gitdab.com/luna/spec-kemal.git + version: 1.0.0+git.commit.e4765ff11d66d705438b9c79e77665d85e4ef8f4 + diff --git a/backend/shard.yml b/backend/shard.yml index 5af70bb..0a43d9a 100644 --- a/backend/shard.yml +++ b/backend/shard.yml @@ -11,3 +11,27 @@ targets: crystal: 1.0.0 license: MIT + +dependencies: + spec-kemal: + git: https://gitdab.com/luna/spec-kemal.git + kemal: + git: https://gitdab.com/luna/kemal.git + ksuid: + github: janeptrv/ksuid.cr + email: + github: arcage/crystal-email + pg: + github: will/crystal-pg + redis: + github: stefanwille/crystal-redis + jennifer: + github: imdrasil/jennifer.cr + version: "~> 0.11.0" + sam: + github: imdrasil/sam.cr + +development_dependencies: + ameba: + github: crystal-ameba/ameba + version: ~> 0.14.0 \ No newline at end of file diff --git a/backend/spec/backend_spec.cr b/backend/spec/backend_spec.cr deleted file mode 100644 index 197b354..0000000 --- a/backend/spec/backend_spec.cr +++ /dev/null @@ -1,9 +0,0 @@ -require "./spec_helper" - -describe Backend do - # TODO: Write tests - - it "works" do - false.should eq(true) - end -end diff --git a/backend/spec/spec_helper.cr b/backend/spec/spec_helper.cr index 4e1e289..e8b9e93 100644 --- a/backend/spec/spec_helper.cr +++ b/backend/spec/spec_helper.cr @@ -1,2 +1,6 @@ require "spec" -require "../src/backend" + +# note: we cannot spec backend.cr +# because kemal will start automatically. +# that's fine, because there's nothing of note there +# anyways. diff --git a/backend/spec/utils/config_spec.cr b/backend/spec/utils/config_spec.cr new file mode 100644 index 0000000..7b2cd0c --- /dev/null +++ b/backend/spec/utils/config_spec.cr @@ -0,0 +1,61 @@ +require "../spec_helper" +require "../../src/utils/config" + +describe Config do + before_each { + Config.load_config + } + it "secret is present" do + secret = Config.get_config_value("secret") + secret.is_a?(JSON::Any).should be_true + if secret.is_a?(JSON::Any) + secret.as_s.empty?.should be_false + end + end + + it "port is present" do + port = Config.get_config_value("port") + port.is_a?(JSON::Any).should be_true + if port.is_a?(JSON::Any) + port.as_i.is_a?(Int).should be_true + port.as_i.should_not eq(0) + end + end + + it "database url is present" do + url = Config.get_config_value("db_url") + url.is_a?(JSON::Any).should be_true + if url.is_a?(JSON::Any) + url.as_s.empty?.should be_false + end + end + + it "mail host and port are present" do + host = Config.get_config_value("mail_host") + host.is_a?(JSON::Any).should be_true + if host.is_a?(JSON::Any) + host.as_s.empty?.should be_false + end + + port = Config.get_config_value("mail_port") + port.is_a?(JSON::Any).should be_true + if port.is_a?(JSON::Any) + port.as_i.is_a?(Int).should be_true + port.as_i.should_not eq(0) + end + end + + it "mail username and password are present" do + uname = Config.get_config_value("mail_username") + uname.is_a?(JSON::Any).should be_true + if uname.is_a?(JSON::Any) + uname.as_s.empty?.should be_false + end + + pword = Config.get_config_value("mail_password") + pword.is_a?(JSON::Any).should be_true + if pword.is_a?(JSON::Any) + pword.as_s.empty?.should be_false + end + end +end diff --git a/backend/src/backend.cr b/backend/src/backend.cr index 4b62aae..5ad8087 100644 --- a/backend/src/backend.cr +++ b/backend/src/backend.cr @@ -1,6 +1,32 @@ -# TODO: Write documentation for `Backend` -module Backend - VERSION = "0.1.0" +require "kemal" +require "log" +require "./utils/config" +require "./endpoints/user" - # TODO: Put your code here +if !Config.is_loaded + puts "loading config" + Config.load_config +end + +serve_static false + +# replacement for the expressjs/sequelize backend of todo. +# because javascript sucks. +module Backend + VERSION = "0.0.1" +end + +get "/api/hello" do + "Hello" +end + +# this is a slightly less hilarious way to get the integer value of something +# because now i am using JSON::Any. but i am going to keep this comment +# because i want to. +port = Config.get_config_value("port") +port_to_use = port.is_a?(JSON::Any) ? port.as_i : 8000 + +Kemal.run do |config| + server = config.server.not_nil! + server.bind_tcp "0.0.0.0", port_to_use end diff --git a/backend/src/endpoints/auth.cr b/backend/src/endpoints/auth.cr new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/endpoints/register.cr b/backend/src/endpoints/register.cr new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/endpoints/todo.cr b/backend/src/endpoints/todo.cr new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/endpoints/user.cr b/backend/src/endpoints/user.cr new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/models/todo.cr b/backend/src/models/todo.cr new file mode 100644 index 0000000..81275a7 --- /dev/null +++ b/backend/src/models/todo.cr @@ -0,0 +1,16 @@ +require "jennifer" + +class Todo < Jennifer::Model::Base + with_timestamps + + mapping( + id: {type: String, primary: true}, + userid: String, + content: String, + tags: Array(String)?, + complete: Bool?, + deadline: Time?, + created_at: Time?, + updated_at: Time?, + ) +end diff --git a/backend/src/models/unverified_user.cr b/backend/src/models/unverified_user.cr new file mode 100644 index 0000000..c4d2f76 --- /dev/null +++ b/backend/src/models/unverified_user.cr @@ -0,0 +1,16 @@ +require "jennifer" + +class UnverifiedUser < Jennifer::Model::Base + with_timestamps + + mapping( + id: {type: String, primary: true}, + email: String, + discord_only_account: {type: Bool, default: false}, + discord_id: String?, + password_hash: String?, + verification_token: String, + created_at: Time?, + updated_at: Time?, + ) +end diff --git a/backend/src/models/user.cr b/backend/src/models/user.cr new file mode 100644 index 0000000..7f9f07b --- /dev/null +++ b/backend/src/models/user.cr @@ -0,0 +1,15 @@ +require "jennifer" + +class User < Jennifer::Model::Base + with_timestamps + + mapping( + id: {type: String, primary: true}, + email: String, + discord_only_account: {type: Bool, default: false}, + discord_id: String?, + password_hash: String?, + created_at: Time?, + updated_at: Time?, + ) +end diff --git a/backend/src/utils/config.cr b/backend/src/utils/config.cr new file mode 100644 index 0000000..8371b19 --- /dev/null +++ b/backend/src/utils/config.cr @@ -0,0 +1,53 @@ +require "file" +require "json" +require "log" + +class ConfigInstance + @@config = {} of String => JSON::Any + @@loaded = false + + def config + @@config + end + + def config=(new_value) + @@config = new_value + end + + def loaded + @@loaded + end + + def loaded=(new_value) + @@loaded = new_value + end +end + +Instance = ConfigInstance.new + +module Config + extend self + + def get_config_value(key) : JSON::Any | Nil + Log.debug { "looking for #{key}" } + if Instance.config.has_key? key + Instance.config[key] + else + nil + end + end + + def is_loaded : Bool + Instance.loaded + end + + def load_config : Nil + loaded_config = File.open("config.json") do |file| + Hash(String, JSON::Any).from_json(file) + end + Log.debug { "loaded config is #{loaded_config}" } + Instance.config = loaded_config + Instance.loaded = true + Log.debug { "instance config is #{Instance.config}" } + end +end diff --git a/backend/src/utils/database_caching.cr b/backend/src/utils/database_caching.cr new file mode 100644 index 0000000..60a61ee --- /dev/null +++ b/backend/src/utils/database_caching.cr @@ -0,0 +1,5 @@ +require "jennifer" +require "redis" + +module DatabaseCaching +end diff --git a/frontend/package.json b/frontend/package.json index e06b6a8..4fdffca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,10 +24,10 @@ "web-vitals": "^1.1.2" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "start": "BROWSER=none react-scripts start", + "build": "BROWSER=none react-scripts build", + "test": "BROWSER=none react-scripts test", + "eject": "BROWSER=none react-scripts eject" }, "eslintConfig": { "extends": [