mirror of
https://gitlab.com/Alamantus/Readlebee.git
synced 2025-09-03 01:34:26 +02:00
Compare commits
6 commits
3634774ff9
...
fd84706104
Author | SHA1 | Date | |
---|---|---|---|
fd84706104 | |||
7ed4148b37 | |||
9b95bd9fb3 | |||
0cad0adfdb | |||
d213ce6f75 | |||
52f603ccc4 |
9 changed files with 256 additions and 36 deletions
|
@ -61,5 +61,14 @@
|
|||
"add": "Add to Shelf",
|
||||
"average_rating": "Average Rating",
|
||||
"reviews_written": "Total Reviews Written"
|
||||
},
|
||||
"api": {
|
||||
"account_already_logged_in": "You are already logged in! You cannot create an account or log in again.",
|
||||
"account_create_required_data_missing": "Could not create account because required data is missing.",
|
||||
"account_create_invalid_email": "The email address entered is not valid.",
|
||||
"account_create_invalid_username": "The username entered is not valid. Usernames must be at least 2 characters long and can only contain letters a–z, numbers 0–9, and underscores",
|
||||
"account_email_exists": "The email address entered is already in use.",
|
||||
"account_username_exists": "The username entered is already in use.",
|
||||
"account_create_success": "Account created successfully! You are now logged in."
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@
|
|||
"fastify-cookie": "^3.1.0",
|
||||
"fastify-helmet": "^3.0.1",
|
||||
"fastify-jwt": "^1.0.0",
|
||||
"fastify-nodemailer": "^4.1.1",
|
||||
"fastify-sequelize": "^1.0.4",
|
||||
"fastify-static": "^2.5.0",
|
||||
"make-promises-safe": "^5.0.0",
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
"db_database": "readlebee",
|
||||
"db_username": "postgres",
|
||||
"db_password": "password",
|
||||
"email_host": null,
|
||||
"email_port": 465,
|
||||
"email_username": null,
|
||||
"email_password": "password",
|
||||
"jwtSecretKey": "SomethingAtLeast32CharactersLong!",
|
||||
"tokenExpireDays": 7,
|
||||
"inventaireDomain": "https://inventaire.io"
|
||||
|
|
115
server/controllers/account.js
Normal file
115
server/controllers/account.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
const crypto = require('crypto');
|
||||
|
||||
class Account {
|
||||
constructor (userModel) {
|
||||
this.model = userModel;
|
||||
}
|
||||
|
||||
// hashPassword and validatePassword modified from https://stackoverflow.com/a/17201493
|
||||
static hashPassword (password) {
|
||||
var salt = crypto.randomBytes(128).toString('base64');
|
||||
var iterations = 10000;
|
||||
var hash = crypto.pbkdf2Sync(password, salt, iterations, 128, 'sha512').toString('base64');
|
||||
|
||||
return {
|
||||
salt,
|
||||
hash,
|
||||
iterations,
|
||||
};
|
||||
}
|
||||
|
||||
static verifyPassword (savedHash, savedSalt, passwordAttempt) {
|
||||
const attemptedHash = crypto.pbkdf2Sync(passwordAttempt, savedSalt, 10000, 128, 'sha512').toString('base64');
|
||||
return savedHash == attemptedHash;
|
||||
}
|
||||
|
||||
static createAccountDataIsValid (createAccountData) {
|
||||
if (typeof createAccountData.email === 'undefined'
|
||||
|| typeof createAccountData.username === 'undefined'
|
||||
|| typeof createAccountData.password === 'undefined'
|
||||
|| createAccountData.password === '') {
|
||||
return {
|
||||
error: true,
|
||||
message: 'api.account_create_required_data_missing',
|
||||
};
|
||||
}
|
||||
if (createAccountData.email.length < 5 || !/^.+@.+\..+$/.test(createAccountData.email)) {
|
||||
return {
|
||||
error: true,
|
||||
message: 'api.account_create_invalid_email',
|
||||
};
|
||||
}
|
||||
if (createAccountData.username.length < 2 || !/^[a-z0-9_]+$/i.test(createAccountData.username)) {
|
||||
return {
|
||||
error: true,
|
||||
message: 'api.account_create_invalid_username',
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static cleanCreateAccountFormData (formData) {
|
||||
return {
|
||||
email: formData.email.trim(),
|
||||
username: formData.username.toString().trim(),
|
||||
displayName: typeof formData.displayName !== 'undefined' ? formData.displayName.toString().trim() : 'A Bee',
|
||||
password: formData.password,
|
||||
}
|
||||
}
|
||||
|
||||
async emailExists (email) {
|
||||
const existingUser = await this.model.find({
|
||||
attributes: ['id'],
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
return existingUser != null;
|
||||
}
|
||||
|
||||
async usernameExists (username) {
|
||||
const existingUser = await this.model.find({
|
||||
attributes: ['id'],
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
return existingUser != null;
|
||||
}
|
||||
|
||||
async canCreateUser (email, username) {
|
||||
const emailExists = await this.emailExists(email);
|
||||
const usernameExists = await this.usernameExists(username);
|
||||
if (emailExists) {
|
||||
return {
|
||||
error: true,
|
||||
message: 'api.account_email_exists',
|
||||
};
|
||||
}
|
||||
if (usernameExists) {
|
||||
return {
|
||||
error: true,
|
||||
message: 'api.account_username_exists',
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async createUser (email, username, displayName, password, needsConfirmation) {
|
||||
const hashData = Account.hashPassword(password);
|
||||
// The data should already have gone through Account.cleanCreateAccountFormData()
|
||||
return await this.model.create({
|
||||
email,
|
||||
username,
|
||||
displayName,
|
||||
passwordHash: hashData.hash,
|
||||
passwordSalt: hashData.salt,
|
||||
accountConfirm: needsConfirmation ? crypto.randomBytes(32).toString('hex') : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = Account;
|
|
@ -11,16 +11,27 @@ function getSequelizeModels (sequelize) {
|
|||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
isEmail: true,
|
||||
len: [5, 150],
|
||||
},
|
||||
},
|
||||
username: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
is: /^[a-z0-9_]+$/i, // Is a set of characters a-z, 0-9, or _, case insensitive
|
||||
len: [2, 32],
|
||||
},
|
||||
},
|
||||
displayName: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
len: [2, 32],
|
||||
},
|
||||
},
|
||||
passwordHash: {
|
||||
type: Sequelize.STRING,
|
||||
|
@ -30,6 +41,10 @@ function getSequelizeModels (sequelize) {
|
|||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
accountConfirm: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
createdAt: {
|
||||
|
|
|
@ -47,10 +47,41 @@ switch (fastify.siteConfig.db_engine) {
|
|||
}
|
||||
fastify.register(require('fastify-sequelize'), sequelizeConfig);
|
||||
|
||||
if (!fastify.siteConfig.email_host || !fastify.siteConfig.email_username) {
|
||||
console.warn('###\nNo email server set up. You will not be able to send emails without entering your email configuration.\n###');
|
||||
} else {
|
||||
fastify.register(require('fastify-nodemailer'), {
|
||||
pool: true,
|
||||
host: fastify.siteConfig.email_host,
|
||||
port: fastify.siteConfig.email_port,
|
||||
secure: true, // use TLS
|
||||
auth: {
|
||||
user: fastify.siteConfig.email_username,
|
||||
pass: fastify.siteConfig.email_password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Every request, check to see if a valid token exists
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
request.isLoggedInUser = typeof request.cookies.token !== 'undefined' && fastify.jwt.verify(request.cookies.token);
|
||||
done();
|
||||
fastify.addHook('onRequest', async (request, reply) => {
|
||||
request.isLoggedInUser = false;
|
||||
if (typeof request.cookies.token !== 'undefined' && fastify.jwt.verify(request.cookies.token)) {
|
||||
const { id } = fastify.jwt.verify(request.cookies.token);
|
||||
const user = await fastify.models.User.findByPk(id).catch(ex => fastify.log(ex));
|
||||
if (!user) {
|
||||
console.log('Invalid user id from token');
|
||||
request.clearCookie('token', token, {
|
||||
path: '/',
|
||||
expires: new Date(Date.now() - 9999),
|
||||
maxAge: new Date(Date.now() - 9999), // Both are set as a "just in case"
|
||||
httpOnly: true, // Prevents JavaScript on the front end from grabbing it
|
||||
sameSite: true, // Prevents the cookie from being used outside of this site
|
||||
});
|
||||
} else {
|
||||
request.isLoggedInUser = true;
|
||||
request.user = user;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
@ -66,5 +97,7 @@ fastify.listen(fastify.siteConfig.port, function (err, address) {
|
|||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
fastify.log.info(`server listening on ${address}`);
|
||||
|
||||
fastify.decorate('canEmail', typeof fastify.nodemailer !== 'undefined');
|
||||
fastify.decorate('models', require('./getSequelizeModels')(fastify.sequelize));
|
||||
});
|
|
@ -1,39 +1,68 @@
|
|||
const Sequelize = require('sequelize');
|
||||
const faker = require('faker');
|
||||
const Account = require('../controllers/account');
|
||||
|
||||
async function routes(fastify, options) {
|
||||
fastify.get('/api/test-db-connect', async (request, reply) => {
|
||||
const User = fastify.sequelize.define('user', {
|
||||
email: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
test: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
}
|
||||
});
|
||||
User.sync();
|
||||
|
||||
return await User.findAll()
|
||||
.catch(async ex => {
|
||||
return await User.sync().then(() => {
|
||||
return [];
|
||||
});
|
||||
});
|
||||
|
||||
// return await User.sync().then(() => {
|
||||
// return User.create({
|
||||
// email: faker.internet.email(),
|
||||
// })
|
||||
// });
|
||||
fastify.get('/api/accounts/test', async (request, reply) => {
|
||||
return false;
|
||||
});
|
||||
|
||||
/*fastify.get('/login', async (request, reply) => {
|
||||
fastify.post('/api/account/create', async (request, reply) => {
|
||||
if (request.isLoggedInUser) {
|
||||
return reply.code(400).send({
|
||||
error: true,
|
||||
message: 'api.account_already_logged_in',
|
||||
});
|
||||
}
|
||||
|
||||
const formDataIsValid = Account.createAccountDataIsValid(request.body);
|
||||
if (formDataIsValid !== true) {
|
||||
return reply.code(400).send(formDataIsValid);
|
||||
}
|
||||
|
||||
const formData = Account.cleanCreateAccountFormData(request.body);
|
||||
|
||||
const account = new Account(fastify.models.User);
|
||||
|
||||
const canCreateUser = await account.canCreateUser(formData.email, formData.username);
|
||||
if (canCreateUser !== true) {
|
||||
return reply.code(400).send(canCreateUser);
|
||||
}
|
||||
|
||||
const result = await account.createUser(formData.email, formData.username, formData.displayName, formData.password, fastify.canEmail);
|
||||
|
||||
if (typeof result.error !== 'undefined') {
|
||||
return reply.code(400).send(result);
|
||||
}
|
||||
|
||||
if (fastify.canEmail) {
|
||||
// fastify.nodemailer.sendMail();
|
||||
return reply.send({
|
||||
error: false,
|
||||
message: 'api.account_create_success',
|
||||
});
|
||||
} else {
|
||||
const token = fastify.jwt.sign({ id: result.id });
|
||||
const expireTime = fastify.siteConfig.tokenExpireDays * (24 * 60 * 60e3); // The section in parentheses is milliseconds in a day
|
||||
|
||||
return reply
|
||||
.setCookie('token', token, {
|
||||
path: '/',
|
||||
expires: new Date(Date.now() + expireTime),
|
||||
maxAge: new Date(Date.now() + expireTime), // Both are set as a "just in case"
|
||||
httpOnly: true, // Prevents JavaScript on the front end from grabbing it
|
||||
sameSite: true, // Prevents the cookie from being used outside of this site
|
||||
})
|
||||
.send({
|
||||
error: false,
|
||||
message: 'api.account_create_success',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get('/api/login', async (request, reply) => {
|
||||
reply.view('login.hbs', { text: request.isLoggedInUser ? JSON.stringify(fastify.jwt.decode(request.cookies.token)) : 'you are NOT logged in' });
|
||||
});
|
||||
|
||||
fastify.post('/login-validate', async (request, reply) => {
|
||||
fastify.post('/api/login-validate', async (request, reply) => {
|
||||
if (typeof request.body.email === "undefined" || typeof request.body.password === "undefined") {
|
||||
reply.redirect('/login', 400);
|
||||
}
|
||||
|
@ -51,9 +80,9 @@ async function routes(fastify, options) {
|
|||
.redirect('/', 200);
|
||||
});
|
||||
|
||||
fastify.get('/logout', async (request, reply) => {
|
||||
fastify.get('/api/logout', async (request, reply) => {
|
||||
reply.clearCookie('token', { path: '/' }).redirect('/?loggedout');
|
||||
});*/
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = routes;
|
|
@ -3,6 +3,7 @@
|
|||
const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
const getSequelizeModels = require('./server/getSequelizeModels');
|
||||
const force = typeof process.argv[2] !== 'undefined' && process.argv[2] === 'force';
|
||||
let siteConfig;
|
||||
try {
|
||||
siteConfig = require('./server/config.json');
|
||||
|
@ -38,7 +39,7 @@ const sequelize = new Sequelize(sequelizeConfig);
|
|||
|
||||
const Models = getSequelizeModels(sequelize);
|
||||
|
||||
sequelize.sync().then(() => {
|
||||
sequelize.sync({ force }).then(() => {
|
||||
const promises = [ // Default status types to use in Statuses
|
||||
{ name: 'update' },
|
||||
{ name: 'progress' },
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -2786,6 +2786,14 @@ fastify-jwt@^1.0.0:
|
|||
jsonwebtoken "^8.3.0"
|
||||
steed "^1.1.3"
|
||||
|
||||
fastify-nodemailer@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fastify-nodemailer/-/fastify-nodemailer-4.1.1.tgz#8bb1bffc17546044a12f2531ca63eabf15b6ef6b"
|
||||
integrity sha512-VTsV5mERoXgjCbEMYnYTd0j9FDFpZ6cpHzjJ7+L9UiruC3obudkguZysaZhhlLOcmBCiIdJqQu1xh6hbPfV2mA==
|
||||
dependencies:
|
||||
fastify-plugin "^1.6.0"
|
||||
nodemailer "^6.0.0"
|
||||
|
||||
fastify-plugin@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-0.1.1.tgz#13a94f495a90fd5a7ae4bcd827c54ea5986c794f"
|
||||
|
@ -4662,6 +4670,11 @@ node-releases@^1.1.29:
|
|||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
nodemailer@^6.0.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.0.tgz#a89b0c62d3937bdcdeecbf55687bd7911b627e12"
|
||||
integrity sha512-TEHBNBPHv7Ie/0o3HXnb7xrPSSQmH1dXwQKRaMKDBGt/ZN54lvDVujP6hKkO/vjkIYL9rK8kHSG11+G42Nhxuw==
|
||||
|
||||
noop-logger@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
|
||||
|
|
Loading…
Add table
Reference in a new issue