login flow
This commit is contained in:
parent
5e30f331d6
commit
093a679b6d
32 changed files with 1214 additions and 156 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -122,4 +122,5 @@ static/
|
|||
|
||||
elm-stuff/
|
||||
|
||||
yarn.lock
|
||||
yarn.lock
|
||||
backend/config.json
|
|
@ -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": ""
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
user: User,
|
||||
},
|
||||
};
|
||||
|
|
3
backend/src/mail.js
Normal file
3
backend/src/mail.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
const nodemailer = require('nodemailer');
|
||||
|
||||
module.exports =
|
|
@ -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(
|
||||
`<html>
|
||||
<body>
|
||||
<h1>Unknown Verification Link</h1>
|
||||
</body>
|
||||
</html>`
|
||||
));
|
||||
}
|
||||
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,
|
||||
|
|
16
frontend/.prettierrc.yaml
Normal file
16
frontend/.prettierrc.yaml
Normal file
|
@ -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
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
11
frontend/public/config.json
Normal file
11
frontend/public/config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"hosts": {
|
||||
"localhost:3000": "LOCAL",
|
||||
"todo.j4.pm": "prod"
|
||||
},
|
||||
"defaultConfig": {},
|
||||
"configs": {
|
||||
"LOCAL": {},
|
||||
"prod": {}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
<ThemeProvider>
|
||||
<Navbar />
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path='/about'>
|
||||
<AboutPage />
|
||||
</Route>
|
||||
<Route path='/login'>
|
||||
<LoginPage />
|
||||
</Route>
|
||||
<Route path='/signup'>
|
||||
<SignupPage />
|
||||
</Route>
|
||||
<PRoute path='/todos'>
|
||||
<TodoPage />
|
||||
</PRoute>
|
||||
<PRoute path='/account'>
|
||||
<AccountPage />
|
||||
</PRoute>
|
||||
<PRoute path='/'>
|
||||
<RootPage />
|
||||
</PRoute>
|
||||
</Switch>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const PRoute = connect(
|
||||
(state) => {
|
||||
return {
|
||||
token: state.login.token
|
||||
};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {};
|
||||
}
|
||||
)(({ children, ...props }) => {
|
||||
return (
|
||||
<Route
|
||||
{...props}
|
||||
render={({ location }) => {
|
||||
return props.token !== undefined ? (
|
||||
children
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/login',
|
||||
state: { from: location }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
110
frontend/src/Navbar.js
Normal file
110
frontend/src/Navbar.js
Normal file
|
@ -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 (
|
||||
<div>
|
||||
<Grid container direction='row' alignItems='flex-end' justify='flex-end' className={classes.container}>
|
||||
<Grid item className={classes.flexbox}>
|
||||
<Box className={classes.flexbox} />
|
||||
</Grid>
|
||||
<Grid item alignItems='flex-end' justify='flex-end'>
|
||||
<LoginLogoutButton className={classes.buttonWrapper} {...props} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoginLogoutButton = connect(
|
||||
(state) => {
|
||||
return {
|
||||
token: state.login.token
|
||||
};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {};
|
||||
}
|
||||
)(({ children, ...props }) => {
|
||||
const { classes } = props;
|
||||
return props.token !== undefined ? (
|
||||
<>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
window.location.pathname = '/account';
|
||||
}}
|
||||
className={classes.button}>
|
||||
<GroupIcon />
|
||||
<Typography>Settings</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
props.logout();
|
||||
}}
|
||||
className={classes.button}>
|
||||
<ExitToAppIcon />
|
||||
<Typography className={classes.typography}>Log Out</Typography>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
window.location.pathname = '/login';
|
||||
}}
|
||||
className={classes.button}>
|
||||
<ExitToAppIcon />
|
||||
<Typography className={classes.typography}>Log In</Typography>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
export default connect(
|
||||
(state, props) => {
|
||||
return {};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {
|
||||
logout: () => {
|
||||
dispatch(logout());
|
||||
}
|
||||
};
|
||||
}
|
||||
)(withStyles(styles)(Navbar));
|
37
frontend/src/ThemeProvider.js
Normal file
37
frontend/src/ThemeProvider.js
Normal file
|
@ -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 (
|
||||
<ThemeProvider theme={createTheme(theme)}>
|
||||
<CssBaseline />
|
||||
<div id='main' className={classes.content}>
|
||||
{children}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(withStyles(styles)(ThemeWrapper));
|
|
@ -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;
|
||||
}
|
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
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(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
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 });
|
||||
});
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
Before Width: | Height: | Size: 2.6 KiB |
5
frontend/src/modules/About/index.js
Normal file
5
frontend/src/modules/About/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const AboutPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default AboutPage;
|
5
frontend/src/modules/Account/index.js
Normal file
5
frontend/src/modules/Account/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const AccountPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default AccountPage;
|
163
frontend/src/modules/Login/index.js
Normal file
163
frontend/src/modules/Login/index.js
Normal file
|
@ -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 (
|
||||
<Grid container alignItems='center' justify='center'>
|
||||
<Grid item>
|
||||
<form>
|
||||
<div>
|
||||
<Typography align='center' variant='h6' gutterBottom>
|
||||
{forgotPassword ? 'Reset Password' : 'Sign in'}
|
||||
</Typography>
|
||||
<TextField
|
||||
autoComplete='current-email'
|
||||
error={error}
|
||||
label='Email'
|
||||
name='email'
|
||||
type='email'
|
||||
value={email}
|
||||
variant='outlined'
|
||||
onChange={(event) => {
|
||||
return setEmail(event.target.value ?? '');
|
||||
}}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
forgotPassword ? props.forgotPassword(email) : props.login(email, password);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{forgotPassword === false ? (
|
||||
<>
|
||||
<div>
|
||||
<TextField
|
||||
autoComplete='current-password'
|
||||
label='Password'
|
||||
name='password'
|
||||
type={visible ? 'text' : 'password'}
|
||||
value={password}
|
||||
variant='outlined'
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value ?? '');
|
||||
}}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
props.login(email, password);
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton
|
||||
aria-label='toggle password visibility'
|
||||
onClick={() => {
|
||||
return setVisible(!visible);
|
||||
}}
|
||||
edge='end'>
|
||||
{visible ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
id='forgot-password-toggle-button'
|
||||
color='secondary'
|
||||
onClick={() => {
|
||||
return setForgotPassword(true);
|
||||
}}
|
||||
size='small'>
|
||||
Forgot Password?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
id='login-button'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
props.login(email, password);
|
||||
}}>
|
||||
Login
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<Button
|
||||
id='forgot-password-cancel-button'
|
||||
variant='contained'
|
||||
fullWidth
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
setForgotPassword(false);
|
||||
setError(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
id='forgot-password-submit-button'
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
handleForgotPassword();
|
||||
}}>
|
||||
Continue
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {
|
||||
login: (email, password) => {
|
||||
dispatch(login(email, password));
|
||||
},
|
||||
forgotPassword: (email) => {
|
||||
dispatch(forgotPassword(email));
|
||||
}
|
||||
};
|
||||
}
|
||||
)(withStyles(styles)(LoginPage));
|
5
frontend/src/modules/Root/index.js
Normal file
5
frontend/src/modules/Root/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const RootPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default RootPage;
|
5
frontend/src/modules/Signup/index.js
Normal file
5
frontend/src/modules/Signup/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const SignupPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default SignupPage;
|
5
frontend/src/modules/TodoList/index.js
Normal file
5
frontend/src/modules/TodoList/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const TodoPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default TodoPage;
|
28
frontend/src/reducers/config.js
Normal file
28
frontend/src/reducers/config.js
Normal file
|
@ -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;
|
||||
});
|
||||
});
|
10
frontend/src/reducers/index.js
Normal file
10
frontend/src/reducers/index.js
Normal file
|
@ -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
|
||||
});
|
28
frontend/src/reducers/localStorage.js
Normal file
28
frontend/src/reducers/localStorage.js
Normal file
|
@ -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;
|
||||
});
|
||||
});
|
103
frontend/src/reducers/login.js
Normal file
103
frontend/src/reducers/login.js
Normal file
|
@ -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 };
|
||||
});
|
||||
}
|
||||
);
|
16
frontend/src/reducers/utils.js
Normal file
16
frontend/src/reducers/utils.js
Normal file
|
@ -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);
|
||||
};
|
||||
};
|
||||
};
|
13
frontend/src/theme.js
Normal file
13
frontend/src/theme.js
Normal file
|
@ -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;
|
3
readme.md
Normal file
3
readme.md
Normal file
|
@ -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
|
Loading…
Reference in a new issue