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 (
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
-}
+};
+
+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 (
+
+
+
+
+
+ );
+};
+
+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