begin impl of todo endpoints

This commit is contained in:
jane 2021-06-27 14:44:09 -04:00
parent 093a679b6d
commit 4c58da37f4
12 changed files with 1438 additions and 492 deletions

16
backend/.prettierrc.yaml Normal file
View 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

View File

@ -6,6 +6,8 @@
"db_url": "postgres://postgres:@127.0.0.1/todo", "db_url": "postgres://postgres:@127.0.0.1/todo",
"cert": "", "cert": "",
"cert_key": "", "cert_key": "",
"mail_host": "",
"mail_port": 465,
"mail_username": "", "mail_username": "",
"mail_password": "" "mail_password": ""
} }

View File

@ -17,8 +17,12 @@
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"express-paginate": "^1.0.2",
"nodemailer": "^6.6.1", "nodemailer": "^6.6.1",
"pg": "^8.6.0", "pg": "^8.6.0",
"sequelize": "^6.6.2" "sequelize": "^6.6.2"
},
"devDependencies": {
"sequelize-cli": "^6.2.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,105 +1,136 @@
const Sequelize = require("sequelize"); const Sequelize = require('sequelize');
const Config = require("./config.js"); const Config = require('./config.js');
if (!Config.config.db_url) { if (!Config.config.db_url) {
console.error("No database url found. please set `db_url` in config.json"); console.error('No database url found. please set `db_url` in config.json');
process.exit(); process.exit();
} }
const db = new Sequelize(Config.config.db_url); const db = new Sequelize(Config.config.db_url);
const UnverifiedUser = db.define("UnverifiedUser", { const UnverifiedUser = db.define('UnverifiedUser', {
id: { id: {
type: Sequelize.DataTypes.UUID, type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.UUIDV4, defaultValue: Sequelize.UUIDV4,
allowNull: false, allowNull: false,
primaryKey: true, primaryKey: true,
unique: true, unique: true
}, },
verificationToken: { verificationToken: {
type: Sequelize.DataTypes.STRING, type: Sequelize.DataTypes.STRING,
allowNull: false, allowNull: false
}, },
email: { email: {
type: Sequelize.DataTypes.STRING, type: Sequelize.DataTypes.STRING,
allowNull: false, allowNull: false,
unique: true, unique: true
}, },
password_hash: { password_hash: {
type: Sequelize.DataTypes.STRING, type: Sequelize.DataTypes.STRING,
allowNull: true, allowNull: true
}, }
}); });
const User = db.define("User", { const User = db.define('User', {
id: { id: {
type: Sequelize.DataTypes.UUID, type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.UUIDV4, defaultValue: Sequelize.UUIDV4,
allowNull: false, allowNull: false,
primaryKey: true, primaryKey: true,
unique: true, unique: true
}, },
email: { email: {
type: Sequelize.DataTypes.STRING, type: Sequelize.DataTypes.STRING,
allowNull: false, allowNull: false,
unique: true, unique: true
}, },
password_hash: { password_hash: {
type: Sequelize.DataTypes.STRING, type: Sequelize.DataTypes.STRING,
allowNull: true, allowNull: true
}, }
}); });
const Todo = db.define("Todo", { const Todo = db.define('Todo', {
id: { id: {
type: Sequelize.DataTypes.UUID, type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.UUIDV4, defaultValue: Sequelize.UUIDV4,
allowNull: false, allowNull: false,
primaryKey: true, primaryKey: true,
unique: true, unique: true
},
user: {
type: Sequelize.DataTypes.UUID,
allowNull: false
}, },
content: { content: {
type: Sequelize.DataTypes.TEXT, type: Sequelize.DataTypes.TEXT,
allowNull: false, allowNull: false
}, },
tags: {
type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING),
allowNull: true
},
complete: {
type: Sequelize.DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
deadline: {
type: Sequelize.DataTypes.DATE,
allowNull: true
}
}); });
const Tag = db.define("Tag", { const Grouping = db.define('Grouping', {
id: { id: {
type: Sequelize.DataTypes.UUID, type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.UUIDV4, defaultValue: Sequelize.UUIDV4,
allowNull: false, allowNull: false,
primaryKey: true, primaryKey: true,
unique: true, unique: true
}, },
content: { complete: {
type: Sequelize.DataTypes.STRING, type: Sequelize.DataTypes.BOOLEAN,
allowNull: false, allowNull: true
}, },
manually_added: {
type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.UUID),
allowNull: true
},
required: {
type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING),
allowNull: true
},
exclusions: {
type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING),
allowNull: true
}
}); });
User.hasMany(Todo);
Todo.hasMany(Tag);
let options = { let options = {
alter: false, alter: false
}; };
if (Config.config.alter_db) { if (Config.config.alter_db) {
options.alter = true; options.alter = true;
} }
let start = async () => {
UnverifiedUser.sync(options); await UnverifiedUser.sync(options);
User.sync(options); await User.sync(options);
Todo.sync(options); await Todo.sync(options);
Tag.sync(options); await Grouping.sync(options);
};
start();
module.exports = { module.exports = {
db: db, db: db,
constructors: { constructors: {
user: () => { user: () => {
return User.build(); return User.build();
}, }
}, },
schemas: { schemas: {
user: User, user: User,
}, unverifiedUser: UnverifiedUser,
todo: Todo,
grouping: Grouping
}
}; };

View File

@ -1,19 +1,17 @@
const http = require("http"); const http = require('http');
const https = require("https"); const https = require('https');
const cors = require("cors"); const cors = require('cors');
const express = require("express"); const express = require('express');
const cookieParser = require("cookie-parser"); const cookieParser = require('cookie-parser');
const Config = require("./config.js"); const Config = require('./config.js');
const UserInterface = require("./user.js"); const UserInterface = require('./user.js');
const TodoInterface = require('./todo.js');
let credentials = {}; let credentials = {};
if (Config.config.https) { if (Config.config.https) {
if ( if (fs.existsSync(Config.config.cert) && fs.existsSync(Config.config.cert_key)) {
fs.existsSync(Config.config.cert) &&
fs.existsSync(Config.config.cert_key)
) {
credentials.key = fs.readFileSync(Config.config.cert_key); credentials.key = fs.readFileSync(Config.config.cert_key);
credentials.cert = fs.readFileSync(Config.config.cert); credentials.cert = fs.readFileSync(Config.config.cert);
} }
@ -27,7 +25,7 @@ app.use(cookieParser());
// force https // force https
app.use((req, res, next) => { app.use((req, res, next) => {
if (Config.config.https) { if (Config.config.https) {
if (req.headers["x-forwarded-proto"] !== "https") { if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(`https://${req.headers.host}${req.url}`); return res.redirect(`https://${req.headers.host}${req.url}`);
} }
} }
@ -35,14 +33,15 @@ app.use((req, res, next) => {
}); });
if (!Config.config.secret) { if (!Config.config.secret) {
console.error("No password secret found. please set `secret` in config.json"); console.error('No password secret found. please set `secret` in config.json');
process.exit(); process.exit();
} else if (Config.config.https && Config.config.secret == "TEST_SECRET") { } else if (Config.config.https && Config.config.secret == 'TEST_SECRET') {
console.error("please do not use the testing secret in production."); console.error('please do not use the testing secret in production.');
process.exit(); process.exit();
} }
app.use("/api/user", UserInterface.router); app.use('/api/user', UserInterface.router);
app.use('/api/todo', TodoInterface.router);
// serve static files last // serve static files last
// app.use(express.static('./static')); // app.use(express.static('./static'));
@ -57,6 +56,5 @@ if (Config.config.https) {
server.listen(Config.config.port || 8080); server.listen(Config.config.port || 8080);
} }
console.log( console.log(
`listening on port ${Config.config.port || 8080}` + `listening on port ${Config.config.port || 8080}` + ` with https ${Config.config.https ? 'enabled' : 'disabled'}`
` with https ${Config.config.https ? "enabled" : "disabled"}`
); );

View File

@ -1,3 +1,52 @@
const Config = require('./config.js');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
module.exports = class Mailer {
sender;
started = false;
mailer;
constructor(host, port, email, password) {
this.mailer = nodemailer.createTransport({
host: host,
port: port,
secure: true,
auth: {
user: email,
pass: password
}
});
this.sender = email;
this.started = true;
}
async sendMail(recipients, subject, content, contentStripped) {
console.log(`sending mail to ${recipients}`);
let info = await this.mailer.sendMail({
from: `"Todo App" <${this.sender}>`,
to: Array.isArray(recipients) ? recipients.join(', ') : recipients,
subject: subject,
text: contentStripped,
html: content
});
}
}
if (!global.mailer || !global.mailer.started) {
if (
!Config.config['mail_host'] ||
!Config.config['mail_port'] ||
!Config.config['mail_username'] ||
!Config.config['mail_password']
) {
console.error(`could not create email account as
mail_host, mail_port, mail_username or mail_password is not set.`);
process.exit();
}
global.mailer = new Mailer(
Config.config['mail_host'],
Config.config['mail_port'],
Config.config['mail_username'],
Config.config['mail_password']
);
}
module.exports = global.mailer;

146
backend/src/todo.js Normal file
View File

@ -0,0 +1,146 @@
const express = require('express');
const paginate = require('express-paginate');
const Database = require('./db_interface.js');
const User = require('./user.js');
const { Op } = require('sequelize');
let router = express.Router();
router.use(express.json());
function map_todo(result) {
return {
id: result.id,
content: result.content,
tags: result.tags
};
}
function parse_tags(tags) {
result = {
complete: undefined,
required: [],
excluded: []
};
tags.map((tag) => {
if (tag === 'complete') {
complete = true;
} else if (tag === '~complete') {
complete = false;
} else if (tag.startsWith('~')) {
excluded.push(tag);
} else {
required.push(tag);
}
});
return result;
}
const todo_fields = ['currentPage', 'limit'];
router.use(paginate.middleware(10, 50));
router.use('/todos', User.enforce_session_login);
router.get('/todos', async (req, res) => {
if (!req.query) {
return res.status(400).json({
error: `query must include the fields: ${todo_fields.join(', ')}}`
});
} else {
let error = [];
for (let field of todo_fields) {
if (!req.query[field]) {
error.push(field);
}
}
if (error.length > 0) {
return res.status(400).json({
error: `query must include the fields: ${error.join(', ')}}`
});
}
}
let tag_options = {};
if (req.query.tags) {
let parsed = parse_tags(req.query.tags.split(','));
tag_options['tags'] = {
[Op.and]: parsed.required,
[Op.not]: parsed.excluded
};
if (parsed.complete !== undefined) {
tag_options['complete'] = {
[Op.is]: parsed.complete
};
}
}
console.log(tag_options);
let all_todos = await Database.schemas.todo.findAndCountAll({
where: {
user: req.get('id'),
...tag_options
},
limit: req.query.limit,
offset: req.skip
});
const item_count = all_todos.count;
const page_count = Math.ceil(item_count / req.query.limit);
res.json({
result: all_todos.map(map_todo),
currentPage: req.query.currentPage,
pageCount: page_count,
itemCount: item_count,
pages: paginate.getArrayPages(req)(5, page_count, req.query.currentPage)
});
});
router.use('/todo', User.enforce_session_login);
router.get('/todo/:id([a-f0-9-]+)', async (req, res) => {
let userid = req.get('id');
let id = req.params?.id;
let match = await Database.schemas.todo.findOne({
where: {
user: userid,
id: id
}
});
if (!match) {
return res.sendStatus(404);
}
return res.json({
result: map_todo(match),
tags: get_tags(match.id)
});
});
router.use('/todo', User.enforce_session_login);
router.post('/todo/:id([a-f0-9-]+)', async (req, res) => {
let userid = req.get('id');
let id = req.params?.id;
let body = req.body;
if (!body) {
return res.sendStatus(400);
}
let match = await Database.schemas.todo.findOne({
where: {
user: userid,
id: id
}
});
if (!match) {
return res.sendStatus(404);
}
//
return res.json({
result: map_todo(match),
tags: get_tags(match.id)
});
});
module.exports = {
router: router
};

View File

@ -1,7 +1,7 @@
const express = require("express"); const express = require('express');
const crypto = require("crypto"); const crypto = require('crypto');
const Config = require("./config.js"); const Config = require('./config.js');
const Database = require("./db_interface.js"); const Database = require('./db_interface.js');
const Mail = require('./mail.js'); const Mail = require('./mail.js');
let router = express.Router(); let router = express.Router();
@ -14,19 +14,19 @@ user_cache = {};
email_cache = {}; email_cache = {};
async function get_user_details(id) { async function get_user_details(id) {
if (!id || id === "undefined") { if (!id || id === 'undefined') {
return undefined; return undefined;
} }
console.log(`search for user with id ${id}`); console.log(`search for user with id ${id}`);
if (!user_cache[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) { if (user === null) {
return undefined; return undefined;
} }
user_cache[user.id] = { user_cache[user.id] = {
id: user.id, id: user.id,
email: user.email, email: user.email,
password_hash: user.password_hash, password_hash: user.password_hash
}; };
email_cache[user.email] = user.id; email_cache[user.email] = user.id;
} }
@ -35,19 +35,19 @@ async function get_user_details(id) {
} }
async function get_user_details_by_email(email) { async function get_user_details_by_email(email) {
if (!email || email === "undefined") { if (!email || email === 'undefined') {
return undefined; return undefined;
} }
console.log(`search for user with email ${email}}`); console.log(`search for user with email ${email}}`);
if (!email_cache[email] || !user_cache[email_cache[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) { if (user === null) {
return undefined; return undefined;
} }
user_cache[user.id] = { user_cache[user.id] = {
id: user.id, id: user.id,
email: user.email, email: user.email,
password_hash: user.password_hash, password_hash: user.password_hash
}; };
email_cache[user.email] = user.id; email_cache[user.email] = user.id;
} }
@ -55,34 +55,34 @@ async function get_user_details_by_email(email) {
return user_cache[email_cache[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) { if (!req.params?.email) {
res.status(400).json({ 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); let user = get_user_details_by_email(req.params.email);
console.log(user); console.log(user);
if (user != null) { if (user !== undefined && user !== {}) {
res.json({ res.json({
id: user.id, id: user.id,
email: user.email, email: user.email
}); });
} else { } else {
res.sendStatus(404); res.sendStatus(404);
} }
}); });
function hash(secret, password) { function hash(secret, password, base64 = true) {
let pw_hash = crypto.pbkdf2Sync( let pw_hash = crypto.pbkdf2Sync(
password, password,
secret, secret,
Config.config.key?.iterations || 1000, Config.config.key?.iterations || 1000,
Config.config.key?.length || 64, Config.config.key?.length || 64,
"sha512" 'sha512'
); );
return pw_hash.toString("base64"); return pw_hash.toString(base64 ? 'base64' : 'hex');
} }
function verify(secret, password, hash) { function verify(secret, password, hash) {
@ -91,10 +91,10 @@ function verify(secret, password, hash) {
secret, secret,
Config.config.key?.iterations || 1000, Config.config.key?.iterations || 1000,
Config.config.key?.length || 64, Config.config.key?.length || 64,
"sha512" 'sha512'
); );
return hash === pw_hash.toString("base64"); return hash === pw_hash.toString('base64');
} }
function hash_password(password) { function hash_password(password) {
@ -105,9 +105,9 @@ function verify_password(password, hash) {
return verify(Config.config.secret, password, hash); return verify(Config.config.secret, password, hash);
} }
function get_session_token(id, password_hash) { function get_session_token(id, password_hash, base64 = true) {
session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32); session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32);
return hash(session_entropy[id], password_hash); return hash(session_entropy[id], password_hash, base64);
} }
function verify_session_token(id, hash, token) { function verify_session_token(id, hash, token) {
@ -119,9 +119,9 @@ function verify_session_token(id, hash, token) {
} }
async function enforce_session_login(req, res, next) { async function enforce_session_login(req, res, next) {
let userid = req.get("id"); let userid = req.get('id');
let session_token = req.get("authorization"); let session_token = req.get('authorization');
console.log("a", userid, session_token); console.log('a', userid, session_token);
if (!userid || !session_token) { if (!userid || !session_token) {
return res.sendStatus(401); return res.sendStatus(401);
} }
@ -129,104 +129,125 @@ async function enforce_session_login(req, res, next) {
if (!user) { if (!user) {
return res.sendStatus(401); return res.sendStatus(401);
} }
let verified_session = verify_session_token( let verified_session = verify_session_token(userid, user.password_hash, session_token);
userid,
user.password_hash,
session_token
);
if (!verified_session) { if (!verified_session) {
return res.sendStatus(401); return res.sendStatus(401);
} }
return next(); return next();
} }
router.post("/signup", async (res, req) => { router.post('/signup', async (req, res) => {
if (!req.body?.email || !req.body?.password) { if (!req.body?.email || !req.body?.password) {
return res.status(400).json({ return res.status(400).json({
error: "must have email and password fields", error: 'must have email and password fields'
}); });
} }
let user = find_user_by_email(req.body?.email); let user = await get_user_details_by_email(req.body?.email);
if (user != null) { if (user !== undefined && user !== {}) {
console.warn(`user already found: ${JSON.stringify(user)}`);
return res.status(403).json({ return res.status(403).json({
error: `email ${req.body.email} is already in use.`, error: `email ${req.body.email} is already in use.`
}); });
} else { } else {
let randomString = "Signup" let match = await Database.schemas.unverifiedUser.findOne({ where: { email: req.body.email } });
for (let i = 0; i < 16, i++) { if (!!match) {
randomString += Math.floor(Math.random() * 10) await Database.schemas.unverifiedUser.destroy({ where: { email: match.email } });
} }
let randomString = 'Signup';
for (let i = 0; i < 16; i++) {
randomString += Math.floor(Math.random() * 10);
}
let password_hash = hash_password(req.body.password);
let user = await Database.schemas.unverifiedUser.create({ let user = await Database.schemas.unverifiedUser.create({
email: String(req.body.email), email: String(req.body.email),
password_hash: hash_password(req.body.password), password_hash: password_hash,
verificationToken: get_session_token(randomString, ) verificationToken: get_session_token(randomString, password_hash, false)
}); });
const link = `${Config.config.https ? 'https://' : 'http://'}${req.headers.host}/api/user/verify?verification=${
user.verificationToken
}`;
const content = `Click here to verify your sign-up:
${link}`;
const contentHtml = `<h1>Click here to verify your sign-up:</h1>
<p><a href=${link}>${link}</a></p>`;
await Mail.sendMail([String(req.body.email)], 'Verify Your Account', contentHtml, content);
return res.sendStatus(204); return res.sendStatus(204);
} }
}) });
router.post("/verify", async (req, res) => { router.get('/verify', async (req, res) => {
if (!req.params?.verification) { if (!req.query?.verification) {
return res.status(400).send( return res.status(400).send(
`<html> `<html>
<body>
<h1>No Verification Link</h1>
</body>
</html>`
);
}
let verification = req.query?.verification;
let user = await Database.schemas.unverifiedUser.findOne({ where: { verificationToken: verification } });
if (user !== undefined && user !== {}) {
if (user.verificationToken != verification) {
return res.status(404).send(
`<html>
<body> <body>
<h1>Unknown Verification Link</h1> <h1>Unknown Verification Link</h1>
</body> </body>
</html>` </html>`
)); );
} }
let verification =
let user = await Database.schemas.unverifiedUser.findOne({ where: { id: id } });
if (user != null) {
let newUser = await Database.schemas.user.create({ let newUser = await Database.schemas.user.create({
email: user.email, email: user.email,
password_hash: user.password_hash, password_hash: user.password_hash
}); });
return res.json({ return res.send(`<html>
id: user.id, <body>
email: user.email, <h1>Sign up complete.</h1>
}); </body>
</html>`);
} else { } else {
return res.status(403).json({ return res.status(404).send(`<html>
error: `email ${req.body.email} is already in use.`, <body>
}); <h1>Unknown Verification Link</h1>
</body>
</html>`);
} }
}); });
router.post("/login", async (req, res) => { router.post('/login', async (req, res) => {
if (!req.body?.email || !req.body?.password) { if (!req.body?.email || !req.body?.password) {
return res.status(400).json({ 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); let user = await get_user_details_by_email(req.body.email);
if (!user) { if (!user) {
return res.status(401).json({ 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); let verified = verify_password(req.body.password, user.password_hash);
if (!verified) { if (!verified) {
return res.status(401).json({ return res.status(401).json({
error: "incorrect email or password", error: 'incorrect email or password'
}); });
} }
return res.json({ return res.json({
userid: user.id, userid: user.id,
session_token: get_session_token(user.id, user.password_hash), session_token: get_session_token(user.id, user.password_hash)
}); });
}); });
router.post("/logout", async (req, res) => { router.post('/logout', async (req, res) => {
if (!req.body?.session_token || !req.body?.userid) { if (!req.body?.session_token || !req.body?.userid) {
return res.status(400).json({ return res.status(400).json({
error: "must include user id and session token to log out", error: 'must include user id and session token to log out'
}); });
} }
@ -236,18 +257,14 @@ router.post("/logout", async (req, res) => {
let user = await get_user_details(userid); let user = await get_user_details(userid);
if (!user) { if (!user) {
return res.status(401).json({ return res.status(401).json({
error: "invalid user data", error: 'invalid user data'
}); });
} }
let verified = verify_session_token( let verified = verify_session_token(user.id, user.password_hash, session_token);
user.id,
user.password_hash,
session_token
);
if (!verified) { if (!verified) {
return res.status(401).json({ return res.status(401).json({
error: "invalid user data", error: 'invalid user data'
}); });
} }
@ -255,41 +272,43 @@ router.post("/logout", async (req, res) => {
return res.sendStatus(204); 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); console.log(req.params);
if (!req.params?.id) { if (!req.params?.id) {
return res.status(400).json({ return res.status(400).json({
error: "must have id parameter", error: 'must have id parameter'
}); });
} }
let id = req.params?.id; let id = req.params?.id;
console.log(id); console.log(id);
let user = await get_user_details(id); let user = await get_user_details(id);
console.log(user); console.log(user);
if (user != null) { if (user !== undefined && user !== {}) {
return res.json({ return res.json({
id: user.id, id: user.id,
email: user.email, email: user.email
}); });
} else { } else {
return res.sendStatus(404); return res.sendStatus(404);
} }
}); });
router.use("/authorized", enforce_session_login); router.use('/authorized', enforce_session_login);
router.get("/authorized", async (req, res) => { router.get('/authorized', async (req, res) => {
let userid = req.get("id"); let userid = req.get('id');
let user = await get_user_details(userid); let user = await get_user_details(userid);
return res.json({ return res.json({
authorized: true, authorized: true,
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email
}, }
}); });
}); });
module.exports = { module.exports = {
router: router, router: router,
enforce_session_login: enforce_session_login, enforce_session_login: enforce_session_login,
get_user_details: get_user_details,
get_user_details_by_email: get_user_details_by_email
}; };

View File

@ -1,6 +1,7 @@
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid'; import Grid from '@material-ui/core/Grid';
import GroupIcon from '@material-ui/icons/Group'; import GroupIcon from '@material-ui/icons/Group';
import SettingsIcon from '@material-ui/icons/Settings';
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
import Box from '@material-ui/core/Box'; import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
@ -67,7 +68,7 @@ const LoginLogoutButton = connect(
window.location.pathname = '/account'; window.location.pathname = '/account';
}} }}
className={classes.button}> className={classes.button}>
<GroupIcon /> <SettingsIcon />
<Typography>Settings</Typography> <Typography>Settings</Typography>
</Button> </Button>
<Button <Button
@ -82,17 +83,28 @@ const LoginLogoutButton = connect(
</Button> </Button>
</> </>
) : ( ) : (
<Button <>
variant='contained' <Button
color='primary' variant='contained'
fullWidth color='primary'
onClick={() => { onClick={() => {
window.location.pathname = '/login'; window.location.pathname = '/login';
}} }}
className={classes.button}> className={classes.button}>
<ExitToAppIcon /> <GroupIcon />
<Typography className={classes.typography}>Log In</Typography> <Typography className={classes.typography}>Log In</Typography>
</Button> </Button>
<Button
variant='contained'
color='primary'
onClick={() => {
window.location.pathname = '/signup';
}}
className={classes.button}>
<AssignmentIndIcon />
<Typography className={classes.typography}>Sign Up</Typography>
</Button>
</>
); );
}); });

View File

@ -1,5 +1,162 @@
import { signup } 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 SignupPage = (props) => { const SignupPage = (props) => {
return <div></div>; const { classes } = props;
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [visible, setVisible] = useState(false);
const [error, setError] = useState(false);
const checkSignup = () => {
if (password !== confirmPassword) {
setError(true);
} else {
setError(false);
}
};
return (
<Grid container alignItems='center' justify='center'>
<Grid item alignItems='center' justify='center'>
<form>
<div>
<Typography align='center' variant='h6' gutterBottom>
Sign Up
</Typography>
<TextField
autoComplete='new-email'
label='Email'
name='email'
type='email'
value={email}
variant='outlined'
fullWidth
onChange={(event) => {
return setEmail(event.target.value ?? '');
}}
onKeyPress={(event) => {
if (event.key === 'Enter') {
checkSignup();
if (!error) {
props.signup(email, password);
}
}
}}
/>
<div>
<TextField
autoComplete='new-password'
label='Password'
name='password'
type={visible ? 'text' : 'password'}
value={password}
variant='outlined'
fullWidth
onChange={(event) => {
setPassword(event.target.value ?? '');
}}
onKeyPress={(event) => {
if (event.key === 'Enter') {
checkSignup();
if (!error) {
props.signup(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>
<div>
<TextField
autoComplete='confirm-password'
label='Confirm Password'
name='confirm-password'
type={visible ? 'text' : 'password'}
error={error}
value={confirmPassword}
variant='outlined'
fullWidth
onChange={(event) => {
setConfirmPassword(event.target.value ?? '');
}}
onKeyPress={(event) => {
checkSignup();
if (event.key === 'Enter') {
if (!error) {
props.signup(email, password);
}
}
}}
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<IconButton
aria-label='toggle confirm password visibility'
onClick={() => {
return setVisible(!visible);
}}
edge='end'>
{visible ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
</div>
<Button
id='login-button'
variant='contained'
color='primary'
onClick={() => {
checkSignup();
if (!error) {
props.signup(email, password);
}
}}>
Sign Up
</Button>
</div>
</form>
</Grid>
</Grid>
);
}; };
export default SignupPage; export default connect(
(state) => {
return {};
},
(dispatch, props) => {
return {
signup: (email, password) => {
dispatch(signup(email, password));
}
};
}
)(withStyles(styles)(SignupPage));

View File

@ -54,6 +54,23 @@ export const login = createAsyncAction((dispatch, getState, config, email, passw
); );
}); });
export const signup = createAsyncAction((dispatch, getState, config, email, password) => {
axios
.post(`${config.apiUrl}/user/signup`, {
email: email,
password: password
})
.then(
(success) => {
console.error('success', success);
window.location.pathname = '/login';
},
(reject) => {
console.error(reject);
}
);
});
export const forgotPassword = createAsyncAction((dispatch, getState, config, email) => {}); export const forgotPassword = createAsyncAction((dispatch, getState, config, email) => {});
export const logout = createAsyncAction((dispatch, getState, config) => { export const logout = createAsyncAction((dispatch, getState, config) => {