Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ec55bed10 | |||
| b51d22f1a1 | |||
| 6b3db67a29 | |||
| 81da8ca422 |
@@ -1,5 +1,6 @@
|
|||||||
WEB_PROTOCOL=http
|
WEB_PROTOCOL=http
|
||||||
WEB_ADDRESS=localhost
|
WEB_ADDRESS=localhost
|
||||||
|
WEB_RESET_ADDRESS=localhost/reset
|
||||||
WEB_PORT=3200
|
WEB_PORT=3200
|
||||||
|
|
||||||
DB_HOSTNAME=database
|
DB_HOSTNAME=database
|
||||||
|
|||||||
@@ -83,4 +83,28 @@ Content-Type: application/json
|
|||||||
{
|
{
|
||||||
"password": "helloworld"
|
"password": "helloworld"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//DOCS: Send the link to recover a forgotten password
|
||||||
|
POST /auth/recover
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "kayneruse@gmail.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//DOCS: Redirect the link to recover a password to the front-end
|
||||||
|
GET /auth/reset?token=<token>
|
||||||
|
|
||||||
|
//Result
|
||||||
|
301 -> ${WEB_RESET_ADDRESS}?email=<email>&token=<token>
|
||||||
|
|
||||||
|
|
||||||
|
//DOCS: Resets a password for the given email, correct token is required
|
||||||
|
PATCH /auth/reset?email=<email>&token=<token>
|
||||||
|
|
||||||
|
{
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const question = (prompt, def = null) => {
|
|||||||
//project configuration
|
//project configuration
|
||||||
const appName = await question('App Name', 'auth');
|
const appName = await question('App Name', 'auth');
|
||||||
const appWebAddress = await question('Web Addr', `${appName}.example.com`);
|
const appWebAddress = await question('Web Addr', `${appName}.example.com`);
|
||||||
|
const resetAddress = await question('Reset Addr', `example.com/reset`);
|
||||||
const appPort = await question('App Port', '3200');
|
const appPort = await question('App Port', '3200');
|
||||||
|
|
||||||
const appDBUser = await question('DB User', appName);
|
const appDBUser = await question('DB User', appName);
|
||||||
@@ -69,6 +70,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- WEB_PROTOCOL=https
|
- WEB_PROTOCOL=https
|
||||||
- WEB_ADDRESS=${appWebAddress}
|
- WEB_ADDRESS=${appWebAddress}
|
||||||
|
- WEB_RESET_ADDRESS=${resetAddress}
|
||||||
- WEB_PORT=${appPort}
|
- WEB_PORT=${appPort}
|
||||||
- DB_HOSTNAME=database
|
- DB_HOSTNAME=database
|
||||||
- DB_DATABASE=${appName}
|
- DB_DATABASE=${appName}
|
||||||
|
|||||||
+36
-36
@@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "auth-server",
|
"name": "auth-server",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"description": "An API centric auth server. Uses Sequelize and mariaDB by default.",
|
"description": "An API centric auth server. Uses Sequelize and mariaDB by default.",
|
||||||
"main": "server/server.js",
|
"main": "server/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server/server.js",
|
"start": "node server/server.js",
|
||||||
"dev": "npm run watch:server",
|
"dev": "npm run watch:server",
|
||||||
"watch:server": "nodemon . --ext js,jsx,json --ignore 'node_modules/*'"
|
"watch:server": "nodemon . --ext js,jsx,json --ignore 'node_modules/*'"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/krgamestudios/auth-server.git"
|
"url": "git+https://github.com/krgamestudios/auth-server.git"
|
||||||
},
|
},
|
||||||
"author": "Kayne Ruse",
|
"author": "Kayne Ruse",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/krgamestudios/auth-server/issues"
|
"url": "https://github.com/krgamestudios/auth-server/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/krgamestudios/auth-server#readme",
|
"homepage": "https://github.com/krgamestudios/auth-server#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^8.6.0",
|
"dotenv": "^8.6.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mariadb": "^2.5.4",
|
"mariadb": "^2.5.4",
|
||||||
"node-cron": "^2.0.3",
|
"node-cron": "^2.0.3",
|
||||||
"nodemailer": "^6.6.3",
|
"nodemailer": "^6.6.3",
|
||||||
"sequelize": "^6.6.5"
|
"sequelize": "^6.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^2.0.12"
|
"nodemon": "^2.0.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ router.post('/login', require('./login'));
|
|||||||
//refresh token
|
//refresh token
|
||||||
router.post('/token', require('./token'));
|
router.post('/token', require('./token'));
|
||||||
|
|
||||||
|
//password recover and reset
|
||||||
|
router.post('/recover', require('./password-recover'));
|
||||||
|
router.get('/reset', require('./password-redirect'));
|
||||||
|
router.patch('/reset', require('./password-reset'));
|
||||||
|
|
||||||
//middleware
|
//middleware
|
||||||
router.use(tokenAuth);
|
router.use(tokenAuth);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
//libraries
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
const { accounts, recovery } = require('../database/models');
|
||||||
|
|
||||||
|
//utilities
|
||||||
|
const uuid = require('../utilities/uuid');
|
||||||
|
const validateEmail = require('../utilities/validate-email');
|
||||||
|
|
||||||
|
//auth/recover
|
||||||
|
const route = async (req, res) => {
|
||||||
|
//validate details
|
||||||
|
const validateErr = await validateDetails(req.body);
|
||||||
|
if (validateErr) {
|
||||||
|
return res.status(401).end(validateErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
//recovery token
|
||||||
|
const token = uuid(32);
|
||||||
|
|
||||||
|
//send the recovery email
|
||||||
|
const emailErr = await sendRecoveryEmail(req.body.email, token);
|
||||||
|
if (emailErr) {
|
||||||
|
return res.status(500).send(emailErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
//save the token
|
||||||
|
await recovery.upsert({
|
||||||
|
email: req.body.email,
|
||||||
|
token: token
|
||||||
|
});
|
||||||
|
|
||||||
|
//finally
|
||||||
|
res.status(200).send("Recovery email sent!");
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateDetails = async (body) => {
|
||||||
|
//basic formatting
|
||||||
|
if (!validateEmail(body.email)) {
|
||||||
|
return 'Invalid email';
|
||||||
|
}
|
||||||
|
|
||||||
|
//check for existing email
|
||||||
|
const emailRecord = await accounts.findOne({
|
||||||
|
where: {
|
||||||
|
email: body.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!emailRecord) {
|
||||||
|
return 'Invalid email';
|
||||||
|
}
|
||||||
|
|
||||||
|
//OK
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendRecoveryEmail = async (email, token) => {
|
||||||
|
const addr = `${process.env.WEB_PROTOCOL}://${process.env.WEB_ADDRESS}/auth/reset?token=${token}`;
|
||||||
|
const msg = `Hello,
|
||||||
|
|
||||||
|
Please visit the following link to reset your password: ${addr}
|
||||||
|
|
||||||
|
If you did not request a password reset, you can safely ignore this message.
|
||||||
|
`;
|
||||||
|
|
||||||
|
let transporter, info;
|
||||||
|
|
||||||
|
//what exactly is a transport?
|
||||||
|
try {
|
||||||
|
transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.MAIL_SMTP,
|
||||||
|
port: 465,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: process.env.MAIL_USERNAME,
|
||||||
|
pass: process.env.MAIL_PASSWORD
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
return `failed to create a mail transport: ${e}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send mail with defined transport object
|
||||||
|
try {
|
||||||
|
info = await transporter.sendMail({
|
||||||
|
from: `recovery@${process.env.WEB_ADDRESS}`, //WARNING: google overwrites this
|
||||||
|
to: email,
|
||||||
|
subject: 'Password Recovery',
|
||||||
|
text: msg
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
return `failed to send validation mail: ${e}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.accepted[0] != email) {
|
||||||
|
return 'recovery email failed to send';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = route;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const { accounts, recovery } = require('../database/models');
|
||||||
|
|
||||||
|
//auth/reset
|
||||||
|
const route = async (req, res) => {
|
||||||
|
//verify the recovery record exists
|
||||||
|
const record = await recovery.findOne({
|
||||||
|
where: {
|
||||||
|
token: req.query.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return res.status(401).end('Failed to recover a password');
|
||||||
|
}
|
||||||
|
|
||||||
|
//redirect to the front-end
|
||||||
|
res.redirect(`${process.env.WEB_PROTOCOL}://${process.env.WEB_RESET_ADDRESS}?email=${record.email}&token=${record.token}`);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = route;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//libraries
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
const { accounts, recovery } = require('../database/models');
|
||||||
|
|
||||||
|
//auth/reset
|
||||||
|
const route = async (req, res) => {
|
||||||
|
//validate the given details
|
||||||
|
const validateErr = await validateDetails(req.query, req.body);
|
||||||
|
if (validateErr) {
|
||||||
|
return res.status(401).send(validateErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
//generate the password hash
|
||||||
|
const hash = await bcrypt.hash(req.body.password, await bcrypt.genSalt(11));
|
||||||
|
|
||||||
|
//update the account data
|
||||||
|
await accounts.update({
|
||||||
|
hash: hash
|
||||||
|
}, {
|
||||||
|
where: {
|
||||||
|
email: req.query.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//delete from the recovery table
|
||||||
|
await recovery.destroy({
|
||||||
|
where: {
|
||||||
|
email: req.query.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).end();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateDetails = async (query, body) => {
|
||||||
|
//verify the recovery record exists
|
||||||
|
const record = await recovery.findOne({
|
||||||
|
where: {
|
||||||
|
email: query.email,
|
||||||
|
token: query.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return 'Failed to recover a password';
|
||||||
|
}
|
||||||
|
|
||||||
|
//validate password
|
||||||
|
if (!body.password) {
|
||||||
|
return 'Missing password';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.password.length < 8) {
|
||||||
|
return 'Password too short';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = route;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
tokens: require('./tokens'),
|
tokens: require('./tokens'),
|
||||||
accounts: require('./accounts'),
|
accounts: require('./accounts'),
|
||||||
pendingSignups: require('./pending-signups')
|
pendingSignups: require('./pending-signups'),
|
||||||
|
recovery: require('./recovery')
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
const sequelize = require('..');
|
||||||
|
|
||||||
|
module.exports = sequelize.define('recovery', {
|
||||||
|
token: 'varchar(320)',
|
||||||
|
email: 'varchar(320)'
|
||||||
|
});
|
||||||
+32
-32
@@ -1,32 +1,32 @@
|
|||||||
//environment variables
|
//environment variables
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
//create the server
|
//create the server
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = require('http').Server(app);
|
const server = require('http').Server(app);
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
|
|
||||||
//config
|
//config
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
//database connection
|
//database connection
|
||||||
const database = require('./database');
|
const database = require('./database');
|
||||||
|
|
||||||
//access the admin
|
//access the admin
|
||||||
app.use('/admin', require('./admin'));
|
app.use('/admin', require('./admin'));
|
||||||
|
|
||||||
//access the auth
|
//access the auth
|
||||||
app.use('/auth', require('./auth'));
|
app.use('/auth', require('./auth'));
|
||||||
|
|
||||||
//error on access
|
//error on access
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
res.redirect('https://github.com/krgamestudios/auth-server');
|
res.redirect('https://github.com/krgamestudios/auth-server');
|
||||||
});
|
});
|
||||||
|
|
||||||
//startup
|
//startup
|
||||||
server.listen(process.env.WEB_PORT || 3200, async (err) => {
|
server.listen(process.env.WEB_PORT || 3200, async (err) => {
|
||||||
await database.sync();
|
await database.sync();
|
||||||
console.log(`listening to localhost:${process.env.WEB_PORT || 3200}`);
|
console.log(`listening to localhost:${process.env.WEB_PORT || 3200}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user