From 093a679b6d46bbb5024d9ebe2285826bfba02d96 Mon Sep 17 00:00:00 2001 From: Jane Petrovna Date: Thu, 17 Jun 2021 17:00:56 -0400 Subject: [PATCH] login flow --- .gitignore | 3 +- backend/{config.json => config.json.example} | 6 +- backend/package.json | 4 +- backend/pnpm-lock.yaml | 8 + backend/src/db_interface.js | 68 ++-- backend/src/mail.js | 3 + backend/src/user.js | 146 +++++--- frontend/.prettierrc.yaml | 16 + frontend/package.json | 8 + frontend/pnpm-lock.yaml | 339 +++++++++++++++++++ frontend/public/config.json | 11 + frontend/src/App.css | 38 --- frontend/src/App.js | 86 +++-- frontend/src/App.test.js | 8 - frontend/src/Navbar.js | 110 ++++++ frontend/src/ThemeProvider.js | 37 ++ frontend/src/index.css | 13 - frontend/src/index.js | 76 ++++- frontend/src/logo.svg | 1 - frontend/src/modules/About/index.js | 5 + frontend/src/modules/Account/index.js | 5 + frontend/src/modules/Login/index.js | 163 +++++++++ frontend/src/modules/Root/index.js | 5 + frontend/src/modules/Signup/index.js | 5 + frontend/src/modules/TodoList/index.js | 5 + frontend/src/reducers/config.js | 28 ++ frontend/src/reducers/index.js | 10 + frontend/src/reducers/localStorage.js | 28 ++ frontend/src/reducers/login.js | 103 ++++++ frontend/src/reducers/utils.js | 16 + frontend/src/theme.js | 13 + readme.md | 3 + 32 files changed, 1214 insertions(+), 156 deletions(-) rename backend/{config.json => config.json.example} (58%) create mode 100644 backend/src/mail.js create mode 100644 frontend/.prettierrc.yaml create mode 100644 frontend/public/config.json delete mode 100644 frontend/src/App.css delete mode 100644 frontend/src/App.test.js create mode 100644 frontend/src/Navbar.js create mode 100644 frontend/src/ThemeProvider.js delete mode 100644 frontend/src/index.css delete mode 100644 frontend/src/logo.svg create mode 100644 frontend/src/modules/About/index.js create mode 100644 frontend/src/modules/Account/index.js create mode 100644 frontend/src/modules/Login/index.js create mode 100644 frontend/src/modules/Root/index.js create mode 100644 frontend/src/modules/Signup/index.js create mode 100644 frontend/src/modules/TodoList/index.js create mode 100644 frontend/src/reducers/config.js create mode 100644 frontend/src/reducers/index.js create mode 100644 frontend/src/reducers/localStorage.js create mode 100644 frontend/src/reducers/login.js create mode 100644 frontend/src/reducers/utils.js create mode 100644 frontend/src/theme.js create mode 100644 readme.md diff --git a/.gitignore b/.gitignore index 3f2288d..fdb31ac 100644 --- a/.gitignore +++ b/.gitignore @@ -122,4 +122,5 @@ static/ elm-stuff/ -yarn.lock \ No newline at end of file +yarn.lock +backend/config.json \ No newline at end of file diff --git a/backend/config.json b/backend/config.json.example similarity index 58% rename from backend/config.json rename to backend/config.json.example index 1623976..5fea072 100644 --- a/backend/config.json +++ b/backend/config.json.example @@ -1,9 +1,11 @@ { "secret": "TEST_SECRET", - "https": false, + "https": true, "alter_db": true, "port": 8080, "db_url": "postgres://postgres:@127.0.0.1/todo", "cert": "", - "cert_key": "" + "cert_key": "", + "mail_username": "", + "mail_password": "" } \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index fd35f15..642f2fd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,9 +5,6 @@ "main": "src/index.js", "scripts": { "who": "pwd", - "build": "cd frontend; elm make src/Main.elm --output=../static/elm.js", - "debug": "cd frontend; elm make src/Main.elm --debug --output=../static/elm.js", - "prod": "cd frontend; elm make src/Main.elm --optimize --output=../static/elm.js", "start": "node src/index.js" }, "repository": { @@ -20,6 +17,7 @@ "cookie-parser": "^1.4.5", "cors": "^2.8.5", "express": "^4.17.1", + "nodemailer": "^6.6.1", "pg": "^8.6.0", "sequelize": "^6.6.2" } diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 1b8148f..82b9747 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -2,6 +2,7 @@ dependencies: cookie-parser: 1.4.5 cors: 2.8.5 express: 4.17.1 + nodemailer: 6.6.1 pg: 8.6.0 sequelize: 6.6.2_pg@8.6.0 lockfileVersion: 5.2 @@ -343,6 +344,12 @@ packages: node: '>= 0.6' resolution: integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + /nodemailer/6.6.1: + dev: false + engines: + node: '>=6.0.0' + resolution: + integrity: sha512-1xzFN3gqv+/qJ6YRyxBxfTYstLNt0FCtZaFRvf4Sg9wxNGWbwFmGXVpfSi6ThGK6aRxAo+KjHtYSW8NvCsNSAg== /object-assign/4.1.1: dev: false engines: @@ -697,5 +704,6 @@ specifiers: cookie-parser: ^1.4.5 cors: ^2.8.5 express: ^4.17.1 + nodemailer: ^6.6.1 pg: ^8.6.0 sequelize: ^6.6.2 diff --git a/backend/src/db_interface.js b/backend/src/db_interface.js index 60ff18f..cfdd80a 100644 --- a/backend/src/db_interface.js +++ b/backend/src/db_interface.js @@ -1,5 +1,5 @@ -const Sequelize = require('sequelize'); -const Config = require('./config.js'); +const Sequelize = require("sequelize"); +const Config = require("./config.js"); if (!Config.config.db_url) { console.error("No database url found. please set `db_url` in config.json"); @@ -8,70 +8,98 @@ if (!Config.config.db_url) { const db = new Sequelize(Config.config.db_url); -const User = db.define('User', { +const UnverifiedUser = db.define("UnverifiedUser", { id: { type: Sequelize.DataTypes.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, primaryKey: true, - unique: true + unique: true, + }, + verificationToken: { + type: Sequelize.DataTypes.STRING, + allowNull: false, }, email: { type: Sequelize.DataTypes.STRING, allowNull: false, - unique: true + unique: true, }, password_hash: { type: Sequelize.DataTypes.STRING, - allowNull: true - } + allowNull: true, + }, }); -const Todo = db.define('Todo', { +const User = db.define("User", { id: { type: Sequelize.DataTypes.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, primaryKey: true, - unique: true + unique: true, + }, + email: { + type: Sequelize.DataTypes.STRING, + allowNull: false, + unique: 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, }, content: { type: Sequelize.DataTypes.TEXT, - allowNull: false - } + allowNull: false, + }, }); -const Tag = db.define('Tag', { +const Tag = db.define("Tag", { id: { type: Sequelize.DataTypes.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, primaryKey: true, - unique: true + unique: true, }, content: { type: Sequelize.DataTypes.STRING, - allowNull: false - } + allowNull: false, + }, }); User.hasMany(Todo); Todo.hasMany(Tag); let options = { - alter: false + alter: false, }; if (Config.config.alter_db) { options.alter = true; } +UnverifiedUser.sync(options); User.sync(options); +Todo.sync(options); +Tag.sync(options); module.exports = { db: db, constructors: { - user: () => { return User.build(); } + user: () => { + return User.build(); + }, }, schemas: { - user: User - } -} \ No newline at end of file + user: User, + }, +}; diff --git a/backend/src/mail.js b/backend/src/mail.js new file mode 100644 index 0000000..b57f777 --- /dev/null +++ b/backend/src/mail.js @@ -0,0 +1,3 @@ +const nodemailer = require('nodemailer'); + +module.exports = diff --git a/backend/src/user.js b/backend/src/user.js index f0cfff1..48d0da8 100644 --- a/backend/src/user.js +++ b/backend/src/user.js @@ -1,7 +1,8 @@ -const express = require('express'); -const crypto = require('crypto'); -const Config = require('./config.js'); -const Database = require('./db_interface.js'); +const express = require("express"); +const crypto = require("crypto"); +const Config = require("./config.js"); +const Database = require("./db_interface.js"); +const Mail = require('./mail.js'); let router = express.Router(); @@ -13,12 +14,12 @@ user_cache = {}; email_cache = {}; async function get_user_details(id) { - if (!id) { + if (!id || id === "undefined") { return undefined; } console.log(`search for user with id ${id}`); if (!user_cache[id]) { - let user = await Database.schemas.user.findOne({where: {id: id}}); + let user = await Database.schemas.user.findOne({ where: { id: id } }); if (!user) { return undefined; } @@ -34,12 +35,12 @@ async function get_user_details(id) { } async function get_user_details_by_email(email) { - if (!email) { + if (!email || email === "undefined") { return undefined; } console.log(`search for user with email ${email}}`); if (!email_cache[email] || !user_cache[email_cache[email]]) { - let user = await Database.schemas.user.findOne({where: {email: email}}); + let user = await Database.schemas.user.findOne({ where: { email: email } }); if (!user) { return undefined; } @@ -54,10 +55,10 @@ async function get_user_details_by_email(email) { return user_cache[email_cache[email]]; } -router.get('/byEmail/:email', async (req, res) => { +router.get("/byEmail/:email", async (req, res) => { if (!req.params?.email) { res.status(400).json({ - error: 'email is a required parameter', + error: "email is a required parameter", }); } let user = get_user_details_by_email(req.params.email); @@ -78,10 +79,10 @@ function hash(secret, password) { secret, Config.config.key?.iterations || 1000, Config.config.key?.length || 64, - 'sha512' + "sha512" ); - return pw_hash.toString('base64'); + return pw_hash.toString("base64"); } function verify(secret, password, hash) { @@ -90,10 +91,10 @@ function verify(secret, password, hash) { secret, Config.config.key?.iterations || 1000, Config.config.key?.length || 64, - 'sha512' + "sha512" ); - return hash === pw_hash.toString('base64'); + return hash === pw_hash.toString("base64"); } function hash_password(password) { @@ -104,9 +105,9 @@ function verify_password(password, hash) { return verify(Config.config.secret, password, hash); } -function get_session_token(id, token) { +function get_session_token(id, password_hash) { session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32); - return hash(session_entropy[id], token); + return hash(session_entropy[id], password_hash); } function verify_session_token(id, hash, token) { @@ -118,9 +119,9 @@ function verify_session_token(id, hash, token) { } async function enforce_session_login(req, res, next) { - let userid = req.cookies?.userid; - let session_token = req.cookies?._session; - console.log('a', userid, session_token); + 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); } @@ -139,67 +140,126 @@ async function enforce_session_login(req, res, next) { return next(); } -router.post('/new', async (req, res) => { +router.post("/signup", async (res, req) => { if (!req.body?.email || !req.body?.password) { return res.status(400).json({ - error: 'must have email and password fields', + error: "must have email and password fields", }); } - let user = await get_user_details_by_email(req.body.email); - console.log(user); - if (user != null) { + let user = find_user_by_email(req.body?.email); + + if (user != null) { return res.status(403).json({ error: `email ${req.body.email} is already in use.`, }); - } else { - let user = await Database.schemas.user.create({ + } else { + let randomString = "Signup" + for (let i = 0; i < 16, i++) { + randomString += Math.floor(Math.random() * 10) + } + let user = await Database.schemas.unverifiedUser.create({ email: String(req.body.email), - password_hash: hash_password(req.body.password), + password_hash: hash_password(req.body.password), + verificationToken: get_session_token(randomString, ) + }); + + return res.sendStatus(204); + } +}) + +router.post("/verify", async (req, res) => { + if (!req.params?.verification) { + return res.status(400).send( + ` + +

Unknown Verification Link

+ + ` + )); + } +let verification = +let user = await Database.schemas.unverifiedUser.findOne({ where: { id: id } }); + +if (user != null) { + let newUser = await Database.schemas.user.create({ + email: user.email, + password_hash: user.password_hash, }); return res.json({ id: user.id, email: user.email, }); + } else { + return res.status(403).json({ + error: `email ${req.body.email} is already in use.`, + }); } }); -router.post('/login', 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', + 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', + 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', + error: "incorrect email or password", }); } - res.cookie('userid', user.id, { - httpOnly: true, - secure: true, - }); - res.cookie('_session', get_session_token(user.id, user.password_hash), { - httpOnly: true, - secure: true, + return res.json({ + userid: user.id, + session_token: get_session_token(user.id, user.password_hash), }); +}); + +router.post("/logout", async (req, res) => { + if (!req.body?.session_token || !req.body?.userid) { + return res.status(400).json({ + error: "must include user id and session token to log out", + }); + } + + let userid = req.body?.userid; + let session_token = req.body?.session_token; + + 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.get('/:id([a-f0-9-]+)', async (req, res) => { +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', + error: "must have id parameter", }); } let id = req.params?.id; @@ -216,9 +276,9 @@ router.get('/:id([a-f0-9-]+)', async (req, res) => { } }); -router.use('/authorized', enforce_session_login); -router.get('/authorized', async (req, res) => { - let userid = req.cookies?.userid; +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, diff --git a/frontend/.prettierrc.yaml b/frontend/.prettierrc.yaml new file mode 100644 index 0000000..3a9fff3 --- /dev/null +++ b/frontend/.prettierrc.yaml @@ -0,0 +1,16 @@ +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/frontend/package.json b/frontend/package.json index fa63682..2ff27eb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,12 +7,20 @@ "@emotion/styled": "^11.3.0", "@material-ui/core": "^5.0.0-alpha.36", "@material-ui/icons": "^4.11.2", + "@material-ui/styles": "^4.11.4", + "@reduxjs/toolkit": "^1.6.0", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "axios": "^0.21.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-redux": "^7.2.4", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0", "web-vitals": "^1.0.1" }, "scripts": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1a3edd4..e7b0856 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -3,12 +3,20 @@ dependencies: '@emotion/styled': 11.3.0_c94ce1a602d602ceca4c9cb32dbce62d '@material-ui/core': 5.0.0-alpha.36_0cc86b8010d608d52ef25836204eb4f5 '@material-ui/icons': 4.11.2_90b48a22f51b8440edf141ccfea311d4 + '@material-ui/styles': 4.11.4_react-dom@17.0.2+react@17.0.2 + '@reduxjs/toolkit': 1.6.0_react-redux@7.2.4+react@17.0.2 '@testing-library/jest-dom': 5.14.1 '@testing-library/react': 11.2.7_react-dom@17.0.2+react@17.0.2 '@testing-library/user-event': 12.8.3 + axios: 0.21.1 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 + react-redux: 7.2.4_react-dom@17.0.2+react@17.0.2 + react-router: 5.2.0_react@17.0.2 + react-router-dom: 5.2.0_react@17.0.2 react-scripts: 4.0.3_react@17.0.2 + redux-logger: 3.0.6 + redux-thunk: 2.3.0 web-vitals: 1.1.2 lockfileVersion: 5.2 packages: @@ -2030,6 +2038,38 @@ packages: optional: true resolution: integrity: sha512-1j+4tIxS6x3McJ+3O9mxwzjkci/uu09nnON7ZDgqX9O3f15D8CP8cmAy0PDm47M4utMwIqj+EaS4Y6d2PZWF5Q== + /@material-ui/styles/4.11.4_react-dom@17.0.2+react@17.0.2: + dependencies: + '@babel/runtime': 7.14.6 + '@emotion/hash': 0.8.0 + '@material-ui/types': 5.1.0 + '@material-ui/utils': 4.11.2_react-dom@17.0.2+react@17.0.2 + clsx: 1.1.1 + csstype: 2.6.17 + hoist-non-react-statics: 3.3.2 + jss: 10.6.0 + jss-plugin-camel-case: 10.6.0 + jss-plugin-default-unit: 10.6.0 + jss-plugin-global: 10.6.0 + jss-plugin-nested: 10.6.0 + jss-plugin-props-sort: 10.6.0 + jss-plugin-rule-value-function: 10.6.0 + jss-plugin-vendor-prefixer: 10.6.0 + prop-types: 15.7.2 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + dev: false + engines: + node: '>=8.0.0' + peerDependencies: + '@types/react': ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + resolution: + integrity: sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew== /@material-ui/system/5.0.0-alpha.36_7874f5fd290ce587db5f5f81e8a37ea9: dependencies: '@babel/runtime': 7.14.6 @@ -2059,6 +2099,15 @@ packages: optional: true resolution: integrity: sha512-1QqINDfNv5y+5TY/p66qkWAZsQGO+HDPDFpTELXtSC9F7zzuosPdJGMM8YFD7vecY4KdDWhCvA4f2G/vI6x8nw== + /@material-ui/types/5.1.0: + dev: false + peerDependencies: + '@types/react': '*' + peerDependenciesMeta: + '@types/react': + optional: true + resolution: + integrity: sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== /@material-ui/types/6.0.1: dev: false peerDependencies: @@ -2089,6 +2138,21 @@ packages: optional: true resolution: integrity: sha512-iTlwlftnH/3w4hU/xaJePMDVsL/JtYJhruJxZ2Tt/2eVzGTvYq88PLQ5+o2VBRROnxQGKRjpNuX3nss+/RudPg== + /@material-ui/utils/4.11.2_react-dom@17.0.2+react@17.0.2: + dependencies: + '@babel/runtime': 7.14.6 + prop-types: 15.7.2 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + react-is: 17.0.2 + dev: false + engines: + node: '>=8.0.0' + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + resolution: + integrity: sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA== /@material-ui/utils/5.0.0-alpha.35_react@17.0.2: dependencies: '@babel/runtime': 7.14.6 @@ -2179,6 +2243,25 @@ packages: dev: false resolution: integrity: sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== + /@reduxjs/toolkit/1.6.0_react-redux@7.2.4+react@17.0.2: + dependencies: + immer: 9.0.3 + react: 17.0.2 + react-redux: 7.2.4_react-dom@17.0.2+react@17.0.2 + redux: 4.1.0 + redux-thunk: 2.3.0 + reselect: 4.0.0 + dev: false + peerDependencies: + react: ^16.14.0 || ^17.0.0 + react-redux: ^7.2.1 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + resolution: + integrity: sha512-eGL50G+Vj5AG5uD0lineb6rRtbs96M8+hxbcwkHpZ8LQcmt0Bm33WyBSnj5AweLkjQ7ZP+KFRDHiLMznljRQ3A== /@rollup/plugin-node-resolve/7.1.3_rollup@1.32.1: dependencies: '@rollup/pluginutils': 3.1.0_rollup@1.32.1 @@ -2478,6 +2561,13 @@ packages: dev: false resolution: integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + /@types/hoist-non-react-statics/3.3.1: + dependencies: + '@types/react': 17.0.11 + hoist-non-react-statics: 3.3.2 + dev: false + resolution: + integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== /@types/html-minifier-terser/5.1.1: dev: false resolution: @@ -2547,6 +2637,15 @@ packages: dev: false resolution: integrity: sha512-X6jVqDIibL2sY0Qtth5EzNeUgPyoCWeBZdmE5xKr7hI4zaQDwN0VaQd7pJnlOB0mDGnOVH0cZZVXg9cnWhztQg== + /@types/react-redux/7.1.16: + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 17.0.11 + hoist-non-react-statics: 3.3.2 + redux: 4.1.0 + dev: false + resolution: + integrity: sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== /@types/react-transition-group/4.4.1: dependencies: '@types/react': 17.0.11 @@ -3311,6 +3410,12 @@ packages: node: '>=4' resolution: integrity: sha512-OKRkKM4ojMEZRJ5UNJHmq9tht7cEnRnqKG6KyB/trYws00Xtkv12mHtlJ0SK7cmuNbrU8dPUova3ELTuilfBbw== + /axios/0.21.1: + dependencies: + follow-redirects: 1.14.1_debug@4.3.1 + dev: false + resolution: + integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== /axobject-query/2.2.0: dev: false resolution: @@ -4547,6 +4652,13 @@ packages: node: '>=8.0.0' resolution: integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + /css-vendor/2.0.8: + dependencies: + '@babel/runtime': 7.14.6 + is-in-browser: 1.1.3 + dev: false + resolution: + integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== /css-what/3.4.2: dev: false engines: @@ -4696,6 +4808,10 @@ packages: node: '>=8' resolution: integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + /csstype/2.6.17: + dev: false + resolution: + integrity: sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A== /csstype/3.0.8: dev: false resolution: @@ -4784,6 +4900,10 @@ packages: dev: false resolution: integrity: sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + /deep-diff/0.3.8: + dev: false + resolution: + integrity: sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ= /deep-equal/1.1.1: dependencies: is-arguments: 1.1.0 @@ -6404,6 +6524,17 @@ packages: dev: false resolution: integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + /history/4.10.1: + dependencies: + '@babel/runtime': 7.14.6 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.1.0 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + dev: false + resolution: + integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== /hmac-drbg/1.0.1: dependencies: hash.js: 1.1.7 @@ -6601,6 +6732,10 @@ packages: node: '>=8.12.0' resolution: integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + /hyphenate-style-name/1.0.4: + dev: false + resolution: + integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== /iconv-lite/0.4.24: dependencies: safer-buffer: 2.1.2 @@ -6649,6 +6784,10 @@ packages: dev: false resolution: integrity: sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== + /immer/9.0.3: + dev: false + resolution: + integrity: sha512-mONgeNSMuyjIe0lkQPa9YhdmTv8P19IeHV0biYhcXhbd5dhdB9HSK93zBpyKjp6wersSUgT5QyU0skmejUVP2A== /import-cwd/2.1.0: dependencies: import-from: 2.1.0 @@ -6709,6 +6848,12 @@ packages: node: '>=0.8.19' resolution: integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o= + /indefinite-observable/2.0.1: + dependencies: + symbol-observable: 1.2.0 + dev: false + resolution: + integrity: sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ== /indent-string/4.0.0: dev: false engines: @@ -6997,6 +7142,10 @@ packages: node: '>=0.10.0' resolution: integrity: sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + /is-in-browser/1.1.3: + dev: false + resolution: + integrity: sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= /is-module/1.0.0: dev: false resolution: @@ -7154,6 +7303,10 @@ packages: node: '>=8' resolution: integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + /isarray/0.0.1: + dev: false + resolution: + integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= /isarray/1.0.0: dev: false resolution: @@ -7837,6 +7990,69 @@ packages: graceful-fs: 4.2.6 resolution: integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + /jss-plugin-camel-case/10.6.0: + dependencies: + '@babel/runtime': 7.14.6 + hyphenate-style-name: 1.0.4 + jss: 10.6.0 + dev: false + resolution: + integrity: sha512-JdLpA3aI/npwj3nDMKk308pvnhoSzkW3PXlbgHAzfx0yHWnPPVUjPhXFtLJzgKZge8lsfkUxvYSQ3X2OYIFU6A== + /jss-plugin-default-unit/10.6.0: + dependencies: + '@babel/runtime': 7.14.6 + jss: 10.6.0 + dev: false + resolution: + integrity: sha512-7y4cAScMHAxvslBK2JRK37ES9UT0YfTIXWgzUWD5euvR+JR3q+o8sQKzBw7GmkQRfZijrRJKNTiSt1PBsLI9/w== + /jss-plugin-global/10.6.0: + dependencies: + '@babel/runtime': 7.14.6 + jss: 10.6.0 + dev: false + resolution: + integrity: sha512-I3w7ji/UXPi3VuWrTCbHG9rVCgB4yoBQLehGDTmsnDfXQb3r1l3WIdcO8JFp9m0YMmyy2CU7UOV6oPI7/Tmu+w== + /jss-plugin-nested/10.6.0: + dependencies: + '@babel/runtime': 7.14.6 + jss: 10.6.0 + tiny-warning: 1.0.3 + dev: false + resolution: + integrity: sha512-fOFQWgd98H89E6aJSNkEh2fAXquC9aZcAVjSw4q4RoQ9gU++emg18encR4AT4OOIFl4lQwt5nEyBBRn9V1Rk8g== + /jss-plugin-props-sort/10.6.0: + dependencies: + '@babel/runtime': 7.14.6 + jss: 10.6.0 + dev: false + resolution: + integrity: sha512-oMCe7hgho2FllNc60d9VAfdtMrZPo9n1Iu6RNa+3p9n0Bkvnv/XX5San8fTPujrTBScPqv9mOE0nWVvIaohNuw== + /jss-plugin-rule-value-function/10.6.0: + dependencies: + '@babel/runtime': 7.14.6 + jss: 10.6.0 + tiny-warning: 1.0.3 + dev: false + resolution: + integrity: sha512-TKFqhRTDHN1QrPTMYRlIQUOC2FFQb271+AbnetURKlGvRl/eWLswcgHQajwuxI464uZk91sPiTtdGi7r7XaWfA== + /jss-plugin-vendor-prefixer/10.6.0: + dependencies: + '@babel/runtime': 7.14.6 + css-vendor: 2.0.8 + jss: 10.6.0 + dev: false + resolution: + integrity: sha512-doJ7MouBXT1lypLLctCwb4nJ6lDYqrTfVS3LtXgox42Xz0gXusXIIDboeh6UwnSmox90QpVnub7au8ybrb0krQ== + /jss/10.6.0: + dependencies: + '@babel/runtime': 7.14.6 + csstype: 3.0.8 + indefinite-observable: 2.0.1 + is-in-browser: 1.1.3 + tiny-warning: 1.0.3 + dev: false + resolution: + integrity: sha512-n7SHdCozmxnzYGXBHe0NsO0eUf9TvsHVq2MXvi4JmTn3x5raynodDVE/9VQmBdWFyyj9HpHZ2B4xNZ7MMy7lkw== /jsx-ast-utils/3.2.0: dependencies: array-includes: 3.1.3 @@ -8273,6 +8489,18 @@ packages: node: '>=4' resolution: integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + /mini-create-react-context/0.4.1_prop-types@15.7.2+react@17.0.2: + dependencies: + '@babel/runtime': 7.14.6 + prop-types: 15.7.2 + react: 17.0.2 + tiny-warning: 1.0.3 + dev: false + peerDependencies: + prop-types: ^15.0.0 + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + resolution: + integrity: sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== /mini-css-extract-plugin/0.11.3_webpack@4.44.2: dependencies: loader-utils: 1.4.0 @@ -9054,6 +9282,12 @@ packages: dev: false resolution: integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + /path-to-regexp/1.8.0: + dependencies: + isarray: 0.0.1 + dev: false + resolution: + integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== /path-type/3.0.0: dependencies: pify: 3.0.0 @@ -10216,12 +10450,67 @@ packages: dev: false resolution: integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + /react-redux/7.2.4_react-dom@17.0.2+react@17.0.2: + dependencies: + '@babel/runtime': 7.14.6 + '@types/react-redux': 7.1.16 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.7.2 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + react-is: 16.13.1 + dev: false + peerDependencies: + react: ^16.8.3 || ^17 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + resolution: + integrity: sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== /react-refresh/0.8.3: dev: false engines: node: '>=0.10.0' resolution: integrity: sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== + /react-router-dom/5.2.0_react@17.0.2: + dependencies: + '@babel/runtime': 7.14.6 + history: 4.10.1 + loose-envify: 1.4.0 + prop-types: 15.7.2 + react: 17.0.2 + react-router: 5.2.0_react@17.0.2 + tiny-invariant: 1.1.0 + tiny-warning: 1.0.3 + dev: false + peerDependencies: + react: '>=15' + resolution: + integrity: sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== + /react-router/5.2.0_react@17.0.2: + dependencies: + '@babel/runtime': 7.14.6 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + mini-create-react-context: 0.4.1_prop-types@15.7.2+react@17.0.2 + path-to-regexp: 1.8.0 + prop-types: 15.7.2 + react: 17.0.2 + react-is: 16.13.1 + tiny-invariant: 1.1.0 + tiny-warning: 1.0.3 + dev: false + peerDependencies: + react: '>=15' + resolution: + integrity: sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== /react-scripts/4.0.3_react@17.0.2: dependencies: '@babel/core': 7.12.3 @@ -10418,6 +10707,22 @@ packages: node: '>=8' resolution: integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + /redux-logger/3.0.6: + dependencies: + deep-diff: 0.3.8 + dev: false + resolution: + integrity: sha1-91VZZvMJjzyIYExEnPC69XeCdL8= + /redux-thunk/2.3.0: + dev: false + resolution: + integrity: sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + /redux/4.1.0: + dependencies: + '@babel/runtime': 7.14.6 + dev: false + resolution: + integrity: sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== /regenerate-unicode-properties/8.2.0: dependencies: regenerate: 1.4.2 @@ -10548,6 +10853,10 @@ packages: dev: false resolution: integrity: sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + /reselect/4.0.0: + dev: false + resolution: + integrity: sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== /resolve-cwd/2.0.0: dependencies: resolve-from: 3.0.0 @@ -10582,6 +10891,10 @@ packages: node: '>=8' resolution: integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + /resolve-pathname/3.0.0: + dev: false + resolution: + integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== /resolve-url-loader/3.1.3: dependencies: adjust-sourcemap-loader: 3.0.0 @@ -11561,6 +11874,12 @@ packages: hasBin: true resolution: integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + /symbol-observable/1.2.0: + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== /symbol-tree/3.2.4: dev: false resolution: @@ -11723,6 +12042,14 @@ packages: dev: false resolution: integrity: sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + /tiny-invariant/1.1.0: + dev: false + resolution: + integrity: sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + /tiny-warning/1.0.3: + dev: false + resolution: + integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== /tmpl/1.0.4: dev: false resolution: @@ -12158,6 +12485,10 @@ packages: dev: false resolution: integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + /value-equal/1.0.1: + dev: false + resolution: + integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== /vary/1.1.2: dev: false engines: @@ -12740,10 +13071,18 @@ specifiers: '@emotion/styled': ^11.3.0 '@material-ui/core': ^5.0.0-alpha.36 '@material-ui/icons': ^4.11.2 + '@material-ui/styles': ^4.11.4 + '@reduxjs/toolkit': ^1.6.0 '@testing-library/jest-dom': ^5.11.4 '@testing-library/react': ^11.1.0 '@testing-library/user-event': ^12.1.10 + axios: ^0.21.1 react: ^17.0.2 react-dom: ^17.0.2 + react-redux: ^7.2.4 + react-router: ^5.2.0 + react-router-dom: ^5.2.0 react-scripts: 4.0.3 + redux-logger: ^3.0.6 + redux-thunk: ^2.3.0 web-vitals: ^1.0.1 diff --git a/frontend/public/config.json b/frontend/public/config.json new file mode 100644 index 0000000..c30666f --- /dev/null +++ b/frontend/public/config.json @@ -0,0 +1,11 @@ +{ + "hosts": { + "localhost:3000": "LOCAL", + "todo.j4.pm": "prod" + }, + "defaultConfig": {}, + "configs": { + "LOCAL": {}, + "prod": {} + } +} \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/frontend/src/App.js b/frontend/src/App.js index 3784575..fd79436 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,25 +1,71 @@ -import logo from './logo.svg'; -import './App.css'; +import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; +import RootPage from './modules/Root'; +import AboutPage from './modules/About'; +import AccountPage from './modules/Account'; +import LoginPage from './modules/Login'; +import SignupPage from './modules/Signup'; +import TodoPage from './modules/TodoList'; +import { connect } from 'react-redux'; +import ThemeProvider from './ThemeProvider'; +import Navbar from './Navbar'; -function App() { +const App = (props) => { return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
+ + + + + + + + + + + + + + + + + + + + + + + + + ); -} +}; + +const PRoute = connect( + (state) => { + return { + token: state.login.token + }; + }, + (dispatch, props) => { + return {}; + } +)(({ children, ...props }) => { + return ( + { + return props.token !== undefined ? ( + children + ) : ( + + ); + }} + /> + ); +}); export default App; diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/frontend/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/frontend/src/Navbar.js b/frontend/src/Navbar.js new file mode 100644 index 0000000..be54659 --- /dev/null +++ b/frontend/src/Navbar.js @@ -0,0 +1,110 @@ +import Link from '@material-ui/core/Link'; +import Grid from '@material-ui/core/Grid'; +import GroupIcon from '@material-ui/icons/Group'; +import Box from '@material-ui/core/Box'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import { connect } from 'react-redux'; +import { withStyles } from '@material-ui/styles'; +import { logout } from './reducers/login'; + +const styles = (theme) => { + return { + container: { + width: '100%' + }, + flexbox: { + display: 'flex', + flexGrow: 1, + width: 'auto' + }, + buttonWrapper: { + alignItems: 'flex-end' + }, + button: { + alignItems: 'flex-end' + }, + typography: { + margin: '5px 0px' + } + }; +}; + +const Navbar = (props) => { + const { classes } = props; + return ( +
+ + + + + + + + +
+ ); +}; + +const LoginLogoutButton = connect( + (state) => { + return { + token: state.login.token + }; + }, + (dispatch, props) => { + return {}; + } +)(({ children, ...props }) => { + const { classes } = props; + return props.token !== undefined ? ( + <> + + + + ) : ( + + ); +}); + +export default connect( + (state, props) => { + return {}; + }, + (dispatch, props) => { + return { + logout: () => { + dispatch(logout()); + } + }; + } +)(withStyles(styles)(Navbar)); diff --git a/frontend/src/ThemeProvider.js b/frontend/src/ThemeProvider.js new file mode 100644 index 0000000..75a0fa7 --- /dev/null +++ b/frontend/src/ThemeProvider.js @@ -0,0 +1,37 @@ +import CssBaseline from '@material-ui/core/CssBaseline'; +import { withStyles, withTheme } from '@material-ui/styles'; +import { createTheme, ThemeProvider } from '@material-ui/core/styles'; +import theme from './theme'; + +const styles = () => { + return { + root: { + height: '100vh', + zIndex: 1, + overflow: 'hidden', + position: 'relative', + display: 'flex' + }, + content: { + zIndex: 3, + flexGrow: 1 + }, + spacing: (n) => { + return `${n * 2}px`; + } + }; +}; + +const ThemeWrapper = (props) => { + const { children, classes } = props; + return ( + + +
+ {children} +
+
+ ); +}; + +export default withTheme(withStyles(styles)(ThemeWrapper)); diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/frontend/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/frontend/src/index.js b/frontend/src/index.js index ef2edf8..7362884 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,17 +1,79 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './index.css'; +import { Provider } from 'react-redux'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { configureStore } from '@reduxjs/toolkit'; +import RootReducer from './reducers'; +import axios from 'axios'; +import logger from 'redux-logger'; -ReactDOM.render( - - - , - document.getElementById('root') -); +const defaultConfig = { + apiUrl: 'http://localhost:8080/api' +}; + +const renderApp = ({ config, user }) => { + const isDev = process.env.NODE_ENV !== 'production'; + const store = configureStore({ + devTools: isDev, + preloadedState: { + config: config, + login: user + }, + reducer: RootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger) + }); + ReactDOM.render( + + + , + document.getElementById('root') + ); +}; // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); + +const findConfig = (fullConfig) => { + return Object.assign(fullConfig.defaultConfig, fullConfig.configs[fullConfig.hosts[window.location.host]]); +}; + +axios + .get('/config.json') + .then( + (success) => { + return Object.assign(defaultConfig, findConfig(success.data)); + }, + () => { + return defaultConfig; + } + ) + .then((config) => { + const details = JSON.parse(localStorage.getItem('userDetails') || '{}'); + return axios + .get(`${config.apiUrl}/user/authorized`, { + headers: { + id: details.id, + Authorization: details.token + } + }) + .then( + (success) => { + return { + config, + user: details || {} + }; + }, + () => { + return { + config, + user: {} + }; + } + ); + }) + .then(({ config, user }) => { + renderApp({ config, user }); + }); diff --git a/frontend/src/logo.svg b/frontend/src/logo.svg deleted file mode 100644 index 9dfc1c0..0000000 --- a/frontend/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/modules/About/index.js b/frontend/src/modules/About/index.js new file mode 100644 index 0000000..5a88a04 --- /dev/null +++ b/frontend/src/modules/About/index.js @@ -0,0 +1,5 @@ +const AboutPage = (props) => { + return
; +}; + +export default AboutPage; diff --git a/frontend/src/modules/Account/index.js b/frontend/src/modules/Account/index.js new file mode 100644 index 0000000..dfbbd52 --- /dev/null +++ b/frontend/src/modules/Account/index.js @@ -0,0 +1,5 @@ +const AccountPage = (props) => { + return
; +}; + +export default AccountPage; diff --git a/frontend/src/modules/Login/index.js b/frontend/src/modules/Login/index.js new file mode 100644 index 0000000..dc4c78e --- /dev/null +++ b/frontend/src/modules/Login/index.js @@ -0,0 +1,163 @@ +import { login, forgotPassword } from '../../reducers/login'; +import { useState } from 'react'; +import Button from '@material-ui/core/Button'; +import Grid from '@material-ui/core/Grid'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import IconButton from '@material-ui/core/IconButton'; +import TextField from '@material-ui/core/TextField'; +import Typography from '@material-ui/core/Typography'; +import Visibility from '@material-ui/icons/Visibility'; +import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import { connect } from 'react-redux'; +import { withStyles } from '@material-ui/styles'; + +const styles = (theme) => {}; + +const LoginPage = (props) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [visible, setVisible] = useState(false); + const [error, setError] = useState(false); + const [forgotPassword, setForgotPassword] = useState(false); + + const handleForgotPassword = () => { + if (!!email) { + props.forgotPassword(email); + setForgotPassword(false); + setError(false); + setEmail(''); + } else { + setError(true); + } + }; + return ( + + +
+
+ + {forgotPassword ? 'Reset Password' : 'Sign in'} + + { + return setEmail(event.target.value ?? ''); + }} + onKeyPress={(event) => { + if (event.key === 'Enter') { + forgotPassword ? props.forgotPassword(email) : props.login(email, password); + } + }} + /> + {forgotPassword === false ? ( + <> +
+ { + setPassword(event.target.value ?? ''); + }} + onKeyPress={(event) => { + if (event.key === 'Enter') { + props.login(email, password); + } + }} + InputProps={{ + endAdornment: ( + + { + return setVisible(!visible); + }} + edge='end'> + {visible ? : } + + + ) + }} + /> +
+ +
+
+ + + ) : ( + + + + + + + + + )} +
+
+
+
+ ); +}; + +export default connect( + (state) => { + return {}; + }, + (dispatch, props) => { + return { + login: (email, password) => { + dispatch(login(email, password)); + }, + forgotPassword: (email) => { + dispatch(forgotPassword(email)); + } + }; + } +)(withStyles(styles)(LoginPage)); diff --git a/frontend/src/modules/Root/index.js b/frontend/src/modules/Root/index.js new file mode 100644 index 0000000..d3df59d --- /dev/null +++ b/frontend/src/modules/Root/index.js @@ -0,0 +1,5 @@ +const RootPage = (props) => { + return
; +}; + +export default RootPage; diff --git a/frontend/src/modules/Signup/index.js b/frontend/src/modules/Signup/index.js new file mode 100644 index 0000000..d890acc --- /dev/null +++ b/frontend/src/modules/Signup/index.js @@ -0,0 +1,5 @@ +const SignupPage = (props) => { + return
; +}; + +export default SignupPage; diff --git a/frontend/src/modules/TodoList/index.js b/frontend/src/modules/TodoList/index.js new file mode 100644 index 0000000..1a7a5ea --- /dev/null +++ b/frontend/src/modules/TodoList/index.js @@ -0,0 +1,5 @@ +const TodoPage = (props) => { + return
; +}; + +export default TodoPage; diff --git a/frontend/src/reducers/config.js b/frontend/src/reducers/config.js new file mode 100644 index 0000000..87cfb4a --- /dev/null +++ b/frontend/src/reducers/config.js @@ -0,0 +1,28 @@ +import { createAction, createAsyncAction } from './utils'; +import { createReducer } from '@reduxjs/toolkit'; + +const actions = { + refresh: 'LOCAL_STORAGE_REFRESH' +}; + +export const getConfigValue = createAsyncAction((dispatch, getState, config, key) => { + const payload = { + key: key, + value: JSON.parse(localStorage.getItem(key)) || undefined + }; + return dispatch(refreshConfigValue(payload)); +}); +export const setConfigValue = createAsyncAction((dispatch, getState, config, key, value) => { + localStorage.setItem(key, JSON.stringify(value)); + return dispatch(refreshConfigValue({ key: key, value: value })); +}); + +export const refreshConfigValue = createAction(actions.refresh, (payload) => { + return payload; +}); + +export default createReducer({}, (builder) => { + builder.addDefaultCase((state, action) => { + state[action.payload?.key] = action.payload?.value; + }); +}); diff --git a/frontend/src/reducers/index.js b/frontend/src/reducers/index.js new file mode 100644 index 0000000..e8c5219 --- /dev/null +++ b/frontend/src/reducers/index.js @@ -0,0 +1,10 @@ +import { combineReducers } from '@reduxjs/toolkit'; +import LocalStorageReducer from './localStorage'; +import LoginReducer from './login'; +import ConfigReducer from './config'; + +export default combineReducers({ + localStorage: LocalStorageReducer, + login: LoginReducer, + config: ConfigReducer +}); diff --git a/frontend/src/reducers/localStorage.js b/frontend/src/reducers/localStorage.js new file mode 100644 index 0000000..9eff4bf --- /dev/null +++ b/frontend/src/reducers/localStorage.js @@ -0,0 +1,28 @@ +import { createAction, createAsyncAction } from './utils'; +import { createReducer } from '@reduxjs/toolkit'; + +const actions = { + refresh: 'LOCAL_STORAGE_REFRESH' +}; + +export const readLocalStorage = createAsyncAction((dispatch, getState, config, key) => { + const payload = { + key: key, + value: JSON.parse(localStorage.getItem(key)) || undefined + }; + return dispatch(refreshLocalStorage(payload)); +}); +export const updateLocalStorage = createAsyncAction((dispatch, getState, config, key, value) => { + localStorage.setItem(key, JSON.stringify(value)); + return dispatch(refreshLocalStorage({ key: key, value: value })); +}); + +export const refreshLocalStorage = createAction(actions.refresh, (payload) => { + return payload; +}); + +export default createReducer({}, (builder) => { + builder.addDefaultCase((state, action) => { + state[action.payload?.key] = action.payload?.value; + }); +}); diff --git a/frontend/src/reducers/login.js b/frontend/src/reducers/login.js new file mode 100644 index 0000000..0eaf01e --- /dev/null +++ b/frontend/src/reducers/login.js @@ -0,0 +1,103 @@ +import axios from 'axios'; +import { createAction, createAsyncAction } from './utils'; +import { createReducer } from '@reduxjs/toolkit'; +import { updateLocalStorage } from './localStorage'; + +const actions = { + update: 'UPDATE_LOGIN_DETAILS' +}; + +const updateLoginDetails = createAction(actions.update, (payload) => { + return payload; +}); + +export const login = createAsyncAction((dispatch, getState, config, email, password) => { + axios + .post(`${config.apiUrl}/user/login`, { + email: email, + password: password + }) + .then( + (success) => { + console.error('success', success); + dispatch( + updateLoginDetails({ + id: success.data['userid'], + token: success.data['session_token'], + error: false + }) + ); + dispatch( + updateLocalStorage('userDetails', { + id: success.data['userid'], + token: success.data['session_token'] + }) + ); + window.location.pathname = '/'; + }, + (reject) => { + console.error(reject); + dispatch( + updateLoginDetails({ + id: undefined, + token: undefined, + error: true + }) + ); + dispatch( + updateLocalStorage('userDetails', { + id: undefined, + token: undefined + }) + ); + } + ); +}); + +export const forgotPassword = createAsyncAction((dispatch, getState, config, email) => {}); + +export const logout = createAsyncAction((dispatch, getState, config) => { + const details = getState().login; + axios + .post(`${config.apiUrl}/user/logout`, { + userid: details.id, + session_token: details.token + }) + .then( + (success) => { + dispatch( + updateLoginDetails({ + id: undefined, + token: undefined, + error: false + }) + ); + dispatch( + updateLocalStorage('userDetails', { + id: undefined, + token: undefined + }) + ); + + window.location.pathname = '/login'; + }, + (reject) => { + console.warn(reject); + console.warn('could not log out.'); + } + ); +}); + +export default createReducer( + { + id: undefined, + token: undefined, + error: false + }, + (builder) => { + builder.addCase(actions.update, (state, action) => { + console.error(state, action); + state = { ...state, ...action?.payload }; + }); + } +); diff --git a/frontend/src/reducers/utils.js b/frontend/src/reducers/utils.js new file mode 100644 index 0000000..29de8b4 --- /dev/null +++ b/frontend/src/reducers/utils.js @@ -0,0 +1,16 @@ +export const createAction = (type, payload) => { + return (...args) => { + return { + type: type, + payload: payload(...args) + }; + }; +}; + +export const createAsyncAction = (payload) => { + return (...args) => { + return function (dispatch, getState, config) { + payload(dispatch, getState, getState()?.config, ...args); + }; + }; +}; diff --git a/frontend/src/theme.js b/frontend/src/theme.js new file mode 100644 index 0000000..3eb926f --- /dev/null +++ b/frontend/src/theme.js @@ -0,0 +1,13 @@ +import grey from '@material-ui/core/colors/grey'; + +const theme = { + palette: { + primary: { + main: grey[200] + }, + secondary: { + main: grey[200] + } + } +}; +export default theme; diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..fe72d3f --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# Todo app + +literally i have adhd so fucking bad this is my attempt to make something that i can actually use to keep track of my shit