Added user model; added pages to register a new account, login and logout; added middleware to process whether a user is logged in or not
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Gregory Ballantine 2024-05-26 17:00:24 -04:00
parent 8c1c43e4df
commit 9a13319948
10 changed files with 292 additions and 0 deletions

View File

@ -19,6 +19,12 @@ a{
}
}
header{
h1{
text-align: center;
}
}
.button.button-primary,
button.button-primary,
input[type="button"].button-primary,
@ -56,6 +62,7 @@ input[type="submit"].button-primary{
}
.nav-bar-right{
float: right;
margin-right: 35px;
}
ul{

View File

@ -42,6 +42,10 @@ app.use(express.urlencoded({
extended: true,
}));
// load middleware
const userSessionMiddleware = require('./src/middleware/user-session');
app.use(userSessionMiddleware.userSession);
// load the template engine
app.set('view engine', 'twig');
@ -50,12 +54,18 @@ app.use(express.static('public'));
// load route handlers
const homeRoutes = require('./src/routes/home');
const authRoutes = require('./src/routes/auth');
const itemRoutes = require('./src/routes/item');
const licenseRoutes = require('./src/routes/license');
const searchRoutes = require('./src/routes/search');
// register route handlers
app.get('/', homeRoutes.getIndex);
app.get('/auth/register', authRoutes.getRegister);
app.post('/auth/register', authRoutes.postRegister);
app.get('/auth/login', authRoutes.getLogin);
app.post('/auth/login', authRoutes.postLogin);
app.get('/auth/logout', authRoutes.getLogout);
app.get('/item/add', itemRoutes.getAdd);
app.post('/item/add', itemRoutes.postAdd);
app.get('/item/:id', itemRoutes.getItem);

View File

@ -0,0 +1,44 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
username: {
type: Sequelize.DataTypes.STRING,
allowNull: false,
unique: true,
},
email: {
type: Sequelize.DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: Sequelize.DataTypes.STRING,
allowNull: false,
},
salt: {
type: Sequelize.DataTypes.STRING,
allowNull: false,
},
firstName: {
type: Sequelize.DataTypes.STRING,
allowNull: true,
},
lastName: {
type: Sequelize.DataTypes.STRING,
allowNull: true,
},
createdAt: Sequelize.DataTypes.DATE,
updatedAt: Sequelize.DataTypes.DATE,
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('users');
}
};

View File

@ -0,0 +1,18 @@
const db = require('../models');
const User = db.users;
// checks if a session user ID is set, and if so grab the user's info
exports.userSession = async function(req, res, next) {
if ('user' in req.session) {
const user = await User.findAll({
where: {
id: req.session.user,
},
});
// pass user info to views
res.locals.user = user[0];
}
next();
};

View File

@ -10,6 +10,7 @@ db.sequelize = sequelize;
db.items = require('./item.js')(sequelize, Sequelize);
db.licenses = require('./license.js')(sequelize, Sequelize);
db.users = require('./user.js')(sequelize, Sequelize);
module.exports = db;

39
src/models/user.js Normal file
View File

@ -0,0 +1,39 @@
module.exports = (sequelize, Sequelize) => {
const User = sequelize.define('user', {
username: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
password: {
type: Sequelize.STRING,
allowNull: false,
},
salt: {
type: Sequelize.STRING,
allowNull: false,
},
firstName: {
type: Sequelize.STRING,
allowNull: true,
},
lastName: {
type: Sequelize.STRING,
allowNull: true,
},
});
return User;
};

58
src/routes/auth.js Normal file
View File

@ -0,0 +1,58 @@
const db = require('../models');
const User = db.users;
const crypto = require('crypto');
// GET - /auth/login
exports.getLogin = async function(req, res) {
res.render('auth/login.twig');
};
// POST - /auth/login
exports.postLogin = async function(req, res) {
const user = await User.findAll({
where: {
username: req.body.login_username,
},
});
const attemptedKey = crypto.pbkdf2Sync(req.body.login_password, user[0].salt, 10000, 64, 'sha512');
const attemptedHash = attemptedKey.toString('hex');
if (attemptedHash == user[0].password) {
req.session.user = user[0].id;
res.redirect('/');
} else {
res.redirect('/auth/login');
}
}
// GET - /auth/register
exports.getRegister = async function(req, res) {
res.render('auth/register.twig');
};
// POST - /auth/register
exports.postRegister = async function(req, res) {
const passwordSalt = crypto.randomBytes(32).toString('base64');
const passwordKey = crypto.pbkdf2Sync(req.body.register_password, passwordSalt, 10000, 64, 'sha512');
const passwordHash = passwordKey.toString('hex');
const user = await User.create({
username: req.body.register_username,
password: passwordHash,
salt: passwordSalt,
email: req.body.register_email,
firstName: req.body.register_first_name,
lastName: req.body.register_last_name,
});
res.redirect('/');
};
// GET - /auth/logout
exports.getLogout = async function(req, res) {
// destroy the user's session
req.session.destroy();
res.redirect('/');
}

40
views/auth/login.twig Normal file
View File

@ -0,0 +1,40 @@
{% extends 'layout.twig' %}
{% block title %}Home{% endblock %}
{% block content %}
<!-- page header -->
<header class="row">
<div class="columns twelve">
<h1>Login to your account.</h1>
</div>
</header>
<section id="record-actions" class="row">
<div class="three columns">
<p>.</p>
</div>
<div class="six columns">
<form class="u-full-width" action="/auth/login" method="POST">
<div class="row">
<label for="login_username">
Username:
<input type="text" id="login_username" class="u-full-width" name="login_username" placeholder="myuser1">
</label>
</div>
<div class="twelve columns">
<label for="login_password">
Password:
<input type="password" id="login_password" class="u-full-width" name="login_password" placeholder="Enter your password...">
</label>
</div>
<input type="submit" class="button-primary u-full-width" value="Login">
</form>
</div>
</section>
{% endblock %}

67
views/auth/register.twig Normal file
View File

@ -0,0 +1,67 @@
{% extends 'layout.twig' %}
{% block title %}Home{% endblock %}
{% block content %}
<!-- page header -->
<header class="row">
<div class="columns twelve">
<h1>Register for a new account.</h1>
</div>
</header>
<section id="record-actions" class="row">
<div class="three columns">
<p>.</p>
</div>
<div class="six columns">
<form class="u-full-width" action="/auth/register" method="POST">
<div class="row">
<div class="six columns">
<label for="register_username">
Username:
<input type="text" id="register_username" class="u-full-width" name="register_username" placeholder="myuser1">
</label>
</div>
<div class="six columns">
<label for="register_password">
Password:
<input type="password" id="register_password" class="u-full-width" name="register_password" placeholder="Enter your password...">
</label>
</div>
</div>
<div class="row">
<div class="twelve columns">
<label for="register_email">
Email address:
<input type="email" id="register_email" class="u-full-width" name="register_email" placeholder="myemail@example.com">
</label>
</div>
</div>
<div class="row">
<div class="six columns">
<label for="register_first_name">
First name:
<input type="text" id="register_first_name" class="u-full-width" name="register_first_name" placeholder="Firstname">
</label>
</div>
<div class="six columns">
<label for="register_last_name">
Last name:
<input type="password" id="register_last_name" class="u-full-width" name="register_last_name" placeholder="Lastname">
</label>
</div>
</div>
<input type="submit" class="button-primary u-full-width" value="Register account">
</form>
</div>
</section>
{% endblock %}

View File

@ -33,6 +33,14 @@
<button id="search-button" type="submit" for="search-form"><i class="fa-solid fa-magnifying-glass"></i></button>
</li>
{% if user %}
<li class="nav-link"><a href="/account">{{ user.username }}</a></li>
<li class="nav-link"><a href="/auth/logout">Logout</a></li>
{% else %}
<li class="nav-link"><a href="/auth/register">Register</a></li>
<li class="nav-link"><a href="/auth/login">Login</a></li>
{% endif %}
</ul>
</div>
</nav>