-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from jeff-pedro/feature-week3
Week 3 release
- Loading branch information
Showing
33 changed files
with
3,097 additions
and
175 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { createClient } from 'redis'; | ||
import listHandler from './listHandler.js'; | ||
|
||
const allowlist = createClient({ prefix: 'allowlist-refresh-token: ' }); | ||
|
||
export default listHandler(allowlist); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import jwt from 'jsonwebtoken'; | ||
import { createHash } from 'crypto'; | ||
import { createClient } from 'redis'; | ||
import listHandler from './listHandler.js'; | ||
|
||
const blocklist = createClient({ prefix: 'blocklist-access-token: ' }); | ||
const blocklistHandler = listHandler(blocklist); | ||
|
||
function generateTokenHash(token) { | ||
return createHash('sha256') | ||
.update(token) | ||
.digest('hex'); | ||
} | ||
|
||
export default ({ | ||
add: async (token) => { | ||
const expirationDate = jwt.decode(token).exp; | ||
const tokenHash = generateTokenHash(token); | ||
await blocklistHandler.add(tokenHash, '', expirationDate); | ||
}, | ||
tokenExists: async (token) => { | ||
const tokenHash = generateTokenHash(token); | ||
return blocklistHandler.containKey(tokenHash); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { promisify } from 'util'; | ||
|
||
export default (list) => { | ||
const setAsync = promisify(list.set).bind(list); | ||
const existsAsync = promisify(list.exists).bind(list); | ||
const getAsync = promisify(list.get).bind(list); | ||
const delAsync = promisify(list.del).bind(list); | ||
|
||
return { | ||
async add(key, value, expirationDate) { | ||
await setAsync(key, value); | ||
list.expireat(key, expirationDate); | ||
}, | ||
async containKey(key) { | ||
const result = await existsAsync(key); | ||
return result === 1; | ||
}, | ||
async findValue(key) { | ||
return getAsync(key); | ||
}, | ||
async delete(key) { | ||
delAsync(key); | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,27 @@ | ||
import express from 'express'; | ||
import logger from 'morgan'; | ||
import bodyParser from 'body-parser'; | ||
import db from './config/db.js'; | ||
import routes from './routes/index.js'; | ||
|
||
// passport strategies to authenticate | ||
import './auth/strategies.js'; | ||
|
||
const app = express(); | ||
|
||
// database's connection | ||
db.on('error', console.error.bind(console, 'connection error:')); | ||
|
||
// use morgan to log at command line | ||
// use morgan to log requests | ||
app.use(logger('combined', { | ||
// don't show the log when it is test | ||
skip: (req, res) => process.env.NODE_ENV === 'test', | ||
})); | ||
|
||
// use body-parser to transform json to object | ||
app.use(bodyParser.json()); | ||
app.use(bodyParser.urlencoded({ extended: false })); | ||
|
||
routes(app); | ||
|
||
export default app; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import nodemailer from 'nodemailer'; | ||
|
||
const emailConfig = { | ||
host: process.env.EMAIL_HOST, | ||
auth: { | ||
user: process.env.EMAIL_USER, | ||
pass: process.env.EMAIL_PASS, | ||
}, | ||
secure: true, | ||
}; | ||
|
||
const emailConfigTest = (testAccount) => ({ | ||
host: 'smtp.ethereal.email', | ||
auth: testAccount, | ||
}); | ||
|
||
async function createEmailConfig() { | ||
if (process.env.NODE_ENV === 'prod') { | ||
return emailConfig; | ||
} | ||
const testAccount = await nodemailer.createTestAccount(); | ||
return emailConfigTest(testAccount); | ||
} | ||
|
||
class Email { | ||
async sendEmail() { | ||
const config = await createEmailConfig(); | ||
const transporter = nodemailer.createTransport(config); | ||
const info = await transporter.sendMail(this); | ||
|
||
if (process.env.NODE_ENV !== 'prod') { | ||
console.log(`Preview URL: ${nodemailer.getTestMessageUrl(info)}`); | ||
} | ||
} | ||
} | ||
|
||
export default class CheckEmail extends Email { | ||
constructor(user, url) { | ||
super(); | ||
this.from = '"Personal Finance API 👻" <no-reply@example.com>'; | ||
this.to = user.email; | ||
this.subject = 'Email checking ✔'; | ||
this.text = `Hello! Check your email here: ${url}`; | ||
this.html = `<h1>Hello!</h1> Check your email here: <a href="${url}">${url}</a>`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import passport from 'passport'; | ||
import Users from '../model/User.js'; | ||
import tokens from './tokens.js'; | ||
|
||
export default { | ||
local: (req, res, next) => { | ||
passport.authenticate( | ||
'local', | ||
{ session: false }, | ||
(_, user, err) => { | ||
if (err && err.message === 'Missing credentials') { | ||
return res.status(401).json({ error: err.message }); | ||
} | ||
|
||
if (err && err.message === 'Password or username is incorrect') { | ||
return res.status(401).json({ error: err.message }); | ||
} | ||
|
||
if (err) { | ||
return res.status(500).json({ error: err.message }); | ||
} | ||
|
||
if (!user) { | ||
return res.status(401).json(); | ||
} | ||
|
||
req.user = user; | ||
return next(); | ||
}, | ||
)(req, res, next); | ||
}, | ||
bearer: (req, res, next) => { | ||
passport.authenticate( | ||
'bearer', | ||
{ session: false }, | ||
(err, user, info) => { | ||
if (err && err.name === 'JsonWebTokenError') { | ||
return res.status(401).json({ error: err.message }); | ||
} | ||
|
||
if (err && err.name === 'TokenExpiredError') { | ||
return res.status(401).json({ error: 'token expired', expiredAt: err.expiredAt }); | ||
} | ||
|
||
// if (err && err.message === ) { | ||
// return res.status(500).json({ error: err.message }); | ||
// } | ||
|
||
if (err) { | ||
return res.status(500).json({ error: err.message }); | ||
} | ||
|
||
if (!user) { | ||
return res.status(401).json(); | ||
} | ||
|
||
req.token = info.token; | ||
req.user = user; | ||
return next(); | ||
}, | ||
)(req, res, next); | ||
}, | ||
refresh: async (req, res, next) => { | ||
try { | ||
const { refreshToken } = req.body; | ||
const id = await tokens.refresh.verify(refreshToken); | ||
await tokens.refresh.invalidate(refreshToken); | ||
const user = await Users.findById(id); | ||
req.user = user; | ||
return next(); | ||
} catch (err) { | ||
if (err.message === 'refresh token invalid') { | ||
return res.status(401).json({ error: err.message }); | ||
} | ||
|
||
if (err.message === 'no refresh token provided') { | ||
return res.status(401).json({ error: err.message }); | ||
} | ||
|
||
res.status(500).json({ error: err.message }); | ||
} | ||
}, | ||
emailChecking: async (req, res, next) => { | ||
try { | ||
const { token } = req.params; | ||
const id = await tokens.emailChecking.verify(token); | ||
const user = await Users.findById(id); | ||
req.user = user; | ||
next(); | ||
} catch (err) { | ||
res.status(500).json({ error: err.message }); | ||
} | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Strategy as LocalStrategy } from 'passport-local'; | ||
import { Strategy as BearerStrategy } from 'passport-http-bearer'; | ||
import passport from 'passport'; | ||
import Users from '../model/User.js'; | ||
import tokens from './tokens.js'; | ||
|
||
passport.use( | ||
new LocalStrategy({ session: false }, Users.authenticate()), | ||
); | ||
|
||
passport.use( | ||
new BearerStrategy( | ||
async (token, done) => { | ||
try { | ||
const id = await tokens.access.verify(token); | ||
const user = await Users.findById(id); | ||
done(null, user, { token }); | ||
} catch (err) { | ||
done(err); | ||
} | ||
}, | ||
), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import jwt from 'jsonwebtoken'; | ||
import moment from 'moment'; | ||
import { randomBytes } from 'crypto'; | ||
import blocklistAccessToken from '../../redis/blocklistAccessToken.js'; | ||
import allowlistRefreshToken from '../../redis/allowlistRefreshToken.js'; | ||
|
||
async function checkTokenInBlocklist(token, name, blocklist) { | ||
if (!blocklist) { | ||
return; | ||
} | ||
|
||
const tokenInBlocklist = await blocklist.tokenExists(token); | ||
if (tokenInBlocklist) { | ||
throw new jwt.JsonWebTokenError(`invalid ${name} by logout`); | ||
} | ||
} | ||
|
||
function checkUser(id, name) { | ||
if (!id) { | ||
throw new Error(`${name} invalid`); | ||
} | ||
} | ||
|
||
function checkToken(token, name) { | ||
if (!token) { | ||
throw new Error(`no ${name} provided`); | ||
} | ||
} | ||
|
||
function createTokenJWT(id, [expAmount, expUnit]) { | ||
const payload = { id }; | ||
const token = jwt.sign(payload, process.env.JWT_KEY, { expiresIn: expAmount + expUnit }); | ||
return token; | ||
} | ||
|
||
async function verifyTokenJWT(token, name, blocklist) { | ||
await checkTokenInBlocklist(token, name, blocklist); | ||
const { id } = jwt.verify(token, process.env.JWT_KEY); | ||
return id; | ||
} | ||
|
||
async function invalidateTokenJWT(token, blocklist) { | ||
await blocklist.add(token); | ||
} | ||
|
||
async function createOpaqueToken(id, [expAmount, expUnit], allowlist) { | ||
const opaqueToken = randomBytes(24).toString('hex'); | ||
const expirationDate = moment().add(expAmount, expUnit).unix(); | ||
await allowlist.add(opaqueToken, id, expirationDate); | ||
return opaqueToken; | ||
} | ||
|
||
async function verifyOpaqueToken(token, name, allowlist) { | ||
checkToken(token, name); | ||
const id = await allowlist.findValue(token); | ||
checkUser(id, name); | ||
return id; | ||
} | ||
|
||
async function invalidateOpaqueToken(token, allowlist) { | ||
await allowlist.delete(token); | ||
} | ||
|
||
export default { | ||
access: { | ||
name: 'access token', | ||
list: blocklistAccessToken, | ||
expiration: [15, 'm'], | ||
create(id) { | ||
return createTokenJWT(id, this.expiration); | ||
}, | ||
async verify(token) { | ||
return verifyTokenJWT(token, this.name, this.list); | ||
}, | ||
async invalidate(token) { | ||
return invalidateTokenJWT(token, this.list); | ||
}, | ||
}, | ||
refresh: { | ||
name: 'refresh token', | ||
list: allowlistRefreshToken, | ||
expiration: [5, 'd'], | ||
create(id) { | ||
return createOpaqueToken(id, this.expiration, this.list); | ||
}, | ||
verify(token) { | ||
return verifyOpaqueToken(token, this.name, this.list); | ||
}, | ||
invalidate(token) { | ||
return invalidateOpaqueToken(token, this.list); | ||
}, | ||
}, | ||
emailChecking: { | ||
name: 'email checking token', | ||
expiration: [1, 'h'], | ||
create(id) { | ||
return createTokenJWT(id, this.expiration); | ||
}, | ||
verify(token) { | ||
return verifyTokenJWT(token, this.name); | ||
}, | ||
}, | ||
}; |
Oops, something went wrong.