Skip to content

Commit

Permalink
Dynamic env variables on tests (#466)
Browse files Browse the repository at this point in the history
* fix: use getters when accessing env variables on config

* refactor: providers config

* fix: guarantee string

* refactor: headers config

* fix: auth getters

* fix: remove repeat registration

* fix: correctly use config, remove route removers

* feat: dynamic route guards

* fix: github -> google

* fix: providers

* fix: twitter provider

* feat: change-env route

* fix: type

* fix: remove repeated prefixes
  • Loading branch information
komninoschatzipapas authored Apr 12, 2021
1 parent 08f6870 commit 3d14fcc
Show file tree
Hide file tree
Showing 60 changed files with 694 additions and 545 deletions.
6 changes: 3 additions & 3 deletions src/limiter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MAX_REQUESTS, TIME_FRAME } from '@shared/config'
import { APPLICATION } from '@shared/config'
import rateLimit, { Message } from 'express-rate-limit'

/**
Expand All @@ -16,8 +16,8 @@ interface LimitMessage extends Message {
export const limiter = rateLimit({
headers: true,

max: MAX_REQUESTS,
windowMs: TIME_FRAME,
max: APPLICATION.MAX_REQUESTS,
windowMs: APPLICATION.TIME_FRAME,
skip: ({ path }) => {
// Don't limit health checks. See https://github.com/nhost/hasura-backend-plus/issues/175
if (path === '/healthz') return true
Expand Down
5 changes: 2 additions & 3 deletions src/middlewares/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Response, NextFunction } from 'express'
import { COOKIE_SECRET } from '@shared/config'
import { COOKIES } from '@shared/config'
import { RefreshTokenMiddleware, RequestExtended, PermissionVariables, Claims } from '@shared/types'
import { getClaims } from '@shared/jwt'
import { getPermissionVariablesFromCookie } from '@shared/helpers'
Expand Down Expand Up @@ -44,7 +44,7 @@ export function authMiddleware(req: RequestExtended, res: Response, next: NextFu
// -------------------------------------
// COOKIES
// -------------------------------------
const cookiesInUse = COOKIE_SECRET ? req.signedCookies : req.cookies
const cookiesInUse = COOKIES.SECRET ? req.signedCookies : req.cookies

if ('refresh_token' in cookiesInUse) {
refresh_token = {
Expand All @@ -58,6 +58,5 @@ export function authMiddleware(req: RequestExtended, res: Response, next: NextFu
req.permission_variables = getPermissionVariablesFromCookie(req)
}

// if (refresh_token) console.log('in auth middleware')
next()
}
10 changes: 5 additions & 5 deletions src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cors from 'cors'
import express from 'express'
import helmet from 'helmet'
import TMP from 'temp-dir'
import { HASURA_ENDPOINT, HASURA_GRAPHQL_ADMIN_SECRET, HOST, PORT } from '@shared/config'
import { APPLICATION } from '@shared/config'
import getJwks from './routes/auth/jwks'

const LOG_LEVEL = process.env.NODE_ENV === 'production' ? 'ERROR' : 'INFO'
Expand Down Expand Up @@ -58,7 +58,7 @@ export default async (
{ migrations, metadata }: Migration = { migrations: './migrations', metadata: './metadata' }
): Promise<void> => {
console.log('Checking migrations and metadata...')
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
const app = express()
app.use(helmet())
app.use(cors())
Expand All @@ -69,8 +69,8 @@ export default async (
* ! As we need Hasura to be up to run the migrations, we provide a temporary server with only the JWKS endpoint.
*/
try {
const server = app.listen(PORT, HOST, async () => {
const { protocol, host } = url.parse(HASURA_ENDPOINT)
const server = app.listen(APPLICATION.PORT, APPLICATION.HOST, async () => {
const { protocol, host } = url.parse(APPLICATION.HASURA_ENDPOINT)
const hasuraURL = `${protocol}//${host}`
// * Wait for GraphQL Engine to be ready
await waitFor(`${hasuraURL}/healthz`)
Expand All @@ -81,7 +81,7 @@ export default async (
`${TEMP_MIGRATION_DIR}/config.yaml`,
// * HBP uses config v1 so far
// `version: 2\nendpoint: ${hasuraURL}\nadmin_secret: ${HASURA_GRAPHQL_ADMIN_SECRET}\nmetadata_directory: metadata\nenable_telemetry: false`,
`version: 1\nendpoint: ${hasuraURL}\nadmin_secret: ${HASURA_GRAPHQL_ADMIN_SECRET}\nmetadata_directory: metadata\nenable_telemetry: false`,
`version: 1\nendpoint: ${hasuraURL}\nadmin_secret: ${APPLICATION.HASURA_GRAPHQL_ADMIN_SECRET}\nmetadata_directory: metadata\nenable_telemetry: false`,
{ encoding: 'utf8' }
)
if (migrations && (await pathExists(migrations))) {
Expand Down
18 changes: 11 additions & 7 deletions src/routes/auth/activate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { REDIRECT_URL_ERROR, REDIRECT_URL_SUCCESS } from '@shared/config'
import { APPLICATION, REGISTRATION } from '@shared/config'
import { Request, Response } from 'express'

import Boom from '@hapi/boom'
Expand All @@ -10,6 +10,10 @@ import { verifySchema } from '@shared/validation'
import { UpdateAccountData } from '@shared/types'

async function activateUser({ query }: Request, res: Response): Promise<unknown> {
if(REGISTRATION.AUTO_ACTIVATE_NEW_USERS) {
throw Boom.badImplementation(`Please set the AUTO_ACTIVATE_NEW_USERS env variable to false to use the auth/activate route.`)
}

let hasuraData: UpdateAccountData

const { ticket } = await verifySchema.validateAsync(query)
Expand All @@ -24,8 +28,8 @@ async function activateUser({ query }: Request, res: Response): Promise<unknown>
})
} catch (err) /* istanbul ignore next */ {
console.error(err)
if (REDIRECT_URL_ERROR) {
return res.redirect(302, REDIRECT_URL_ERROR as string)
if (APPLICATION.REDIRECT_URL_ERROR) {
return res.redirect(302, APPLICATION.REDIRECT_URL_ERROR as string)
}
throw err
}
Expand All @@ -35,15 +39,15 @@ async function activateUser({ query }: Request, res: Response): Promise<unknown>
if (!affected_rows) {
console.error('Invalid or expired ticket')

if (REDIRECT_URL_ERROR) {
return res.redirect(302, REDIRECT_URL_ERROR as string)
if (APPLICATION.REDIRECT_URL_ERROR) {
return res.redirect(302, APPLICATION.REDIRECT_URL_ERROR as string)
}
/* istanbul ignore next */
throw Boom.unauthorized('Invalid or expired ticket.')
}

if (REDIRECT_URL_SUCCESS) {
return res.redirect(302, REDIRECT_URL_SUCCESS as string)
if (APPLICATION.REDIRECT_URL_SUCCESS) {
return res.redirect(302, APPLICATION.REDIRECT_URL_SUCCESS as string)
}

res.status(200).send('Your account has been activated. You can close this window and login')
Expand Down
35 changes: 12 additions & 23 deletions src/routes/auth/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,7 @@
import 'jest-extended'
import { v4 as uuidv4 } from 'uuid'

import {
AUTO_ACTIVATE_NEW_USERS,
HIBP_ENABLE,
EMAILS_ENABLE,
REDIRECT_URL_ERROR,
JWT_CLAIMS_NAMESPACE,
HOST,
PORT,
ADMIN_SECRET_HEADER,
HASURA_GRAPHQL_ADMIN_SECRET
// ANONYMOUS_USERS_ENABLE
} from '@shared/config'
import { APPLICATION, JWT as CONFIG_JWT, REGISTRATION, HEADERS } from '@shared/config'
import { generateRandomString, selectAccountByEmail } from '@shared/helpers'
import { deleteMailHogEmail, mailHogSearch, deleteAccount } from '@test/test-utils'

Expand All @@ -36,7 +25,7 @@ const password = generateRandomString()

let request: SuperTest<Test>

const server = app.listen(PORT, HOST)
const server = app.listen(APPLICATION.PORT, APPLICATION.HOST)

beforeAll(async () => {
request = agent(server) // * Create the SuperTest agent
Expand All @@ -47,7 +36,7 @@ afterAll(async () => {
server.close()
})

const pwndPasswordIt = HIBP_ENABLE ? it : it.skip
const pwndPasswordIt = REGISTRATION.HIBP_ENABLE ? it : it.skip
pwndPasswordIt('should tell the password has been pwned', async () => {
const {
status,
Expand Down Expand Up @@ -124,18 +113,18 @@ it('should tell the account already exists', async () => {
})

// * Only run test if auto activation is disabled
const manualActivationIt = !AUTO_ACTIVATE_NEW_USERS ? it : it.skip
const manualActivationIt = !REGISTRATION.AUTO_ACTIVATE_NEW_USERS ? it : it.skip

manualActivationIt('should fail to activate an user from a wrong ticket', async () => {
const { status, redirect, header } = await request.get(`/auth/activate?ticket=${uuidv4()}`)
expect(
status === 500 || (status === 302 && redirect && header?.location === REDIRECT_URL_ERROR)
status === 500 || (status === 302 && redirect && header?.location === APPLICATION.REDIRECT_URL_ERROR)
).toBeTrue()
})

manualActivationIt('should activate the account from a valid ticket', async () => {
let ticket
if (EMAILS_ENABLE) {
if (APPLICATION.EMAILS_ENABLE) {
// Sends the email, checks if it's received and use the link for activation
const [message] = await mailHogSearch(email)
expect(message).toBeTruthy()
Expand Down Expand Up @@ -180,7 +169,7 @@ it('should sign the user in', async () => {
it('should not sign user in with invalid admin secret', async () => {
const { status } = await request
.post('/auth/login')
.set(ADMIN_SECRET_HEADER, 'invalidsecret')
.set(HEADERS.ADMIN_SECRET_HEADER, 'invalidsecret')
.send({ email, password: 'invalidpassword' })

expect(status).toEqual(401)
Expand All @@ -189,7 +178,7 @@ it('should not sign user in with invalid admin secret', async () => {
it('should sign in user with valid admin secret', async () => {
const { body, status } = await request
.post('/auth/login')
.set(ADMIN_SECRET_HEADER, HASURA_GRAPHQL_ADMIN_SECRET as string)
.set(HEADERS.ADMIN_SECRET_HEADER, APPLICATION.HASURA_GRAPHQL_ADMIN_SECRET as string)
.send({ email, password: 'invalidpassword' })

expect(status).toEqual(200)
Expand All @@ -199,9 +188,9 @@ it('should sign in user with valid admin secret', async () => {

it('should decode a valid custom user claim', async () => {
const decodedJwt = JWT.decode(jwtToken) as Token
expect(decodedJwt[JWT_CLAIMS_NAMESPACE]).toBeObject()
expect(decodedJwt[CONFIG_JWT.CLAIMS_NAMESPACE]).toBeObject()
// Test if the custom claims work
expect(decodedJwt[JWT_CLAIMS_NAMESPACE]['x-hasura-name']).toEqual('Test name')
expect(decodedJwt[CONFIG_JWT.CLAIMS_NAMESPACE]['x-hasura-name']).toEqual('Test name')
})

it('should logout', async () => {
Expand All @@ -228,9 +217,9 @@ describe('Tests without cookies', () => {

it('should decode a valid custom user claim', async () => {
const decodedJwt = JWT.decode(jwtToken) as Token
expect(decodedJwt[JWT_CLAIMS_NAMESPACE]).toBeObject()
expect(decodedJwt[CONFIG_JWT.CLAIMS_NAMESPACE]).toBeObject()
// Test if the custom claims work
expect(decodedJwt[JWT_CLAIMS_NAMESPACE]['x-hasura-name']).toEqual('Test name')
expect(decodedJwt[CONFIG_JWT.CLAIMS_NAMESPACE]['x-hasura-name']).toEqual('Test name')
})
})

Expand Down
6 changes: 6 additions & 0 deletions src/routes/auth/change-email/direct-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import { request } from '@shared/request'

import { getRequestInfo } from './utils'
import { RequestExtended } from '@shared/types'
import { AUTHENTICATION } from '@shared/config'
import Boom from '@hapi/boom'

async function requestChangeEmail(req: RequestExtended, res: Response): Promise<unknown> {
if(AUTHENTICATION.VERIFY_EMAILS) {
throw Boom.badImplementation(`Please set the VERIFY_EMAILS env variable to false to use the auth/change-email route.`)
}

const { user_id, new_email } = await getRequestInfo(req)

// * Email verification is not activated - change email straight away
Expand Down
6 changes: 3 additions & 3 deletions src/routes/auth/change-email/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { account, request } from '@test/test-mock-account'
import { mailHogSearch, deleteMailHogEmail } from '@test/test-utils'
import { JWT } from 'jose'
import { Token } from '@shared/types'
import { JWT_CLAIMS_NAMESPACE, NOTIFY_EMAIL_CHANGE, EMAILS_ENABLE } from '@shared/config'
import { AUTHENTICATION, APPLICATION, JWT as CONFIG_JWT } from '@shared/config'

let ticket: string
const new_email = `${generateRandomString()}@${generateRandomString()}.com`
Expand Down Expand Up @@ -39,13 +39,13 @@ it('should reconnect using the new email', async () => {
expect(jwt_token).toBeString()
expect(jwt_expires_in).toBeNumber()
const decodedJwt = JWT.decode(jwt_token) as Token
expect(decodedJwt[JWT_CLAIMS_NAMESPACE]).toBeObject()
expect(decodedJwt[CONFIG_JWT.CLAIMS_NAMESPACE]).toBeObject()
expect(status).toEqual(200)
account.token = jwt_token
})

it('should receive an email notifying the email account has been changed', async () => {
if (EMAILS_ENABLE && NOTIFY_EMAIL_CHANGE) {
if (APPLICATION.EMAILS_ENABLE && AUTHENTICATION.NOTIFY_EMAIL_CHANGE) {
const [message] = await mailHogSearch(account.email)
expect(message).toBeTruthy()
expect(message.Content.Headers.Subject).toInclude(
Expand Down
23 changes: 15 additions & 8 deletions src/routes/auth/change-email/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@ import { Router } from 'express'
import requestVerification from './request-verification'
import directChange from './direct-change'
import changeVerified from './verify-and-change'
import { EMAILS_ENABLE, NOTIFY_EMAIL_CHANGE, VERIFY_EMAILS } from '@shared/config'
import { APPLICATION, AUTHENTICATION } from '@shared/config'
import Boom from '@hapi/boom'

if (NOTIFY_EMAIL_CHANGE && !EMAILS_ENABLE)
if (AUTHENTICATION.NOTIFY_EMAIL_CHANGE && !APPLICATION.EMAILS_ENABLE)
console.warn(
"NOTIFY_EMAIL_CHANGE has been enabled but SMTP is not enabled. Email change notifications won't be sent."
)

const router = Router()

if (VERIFY_EMAILS) {
router.post('/request', requestVerification)
router.post('/change', changeVerified)
} else {
router.post('/', directChange)
}
router.use((req, res, next) => {
if(!AUTHENTICATION.CHANGE_EMAIL_ENABLE) {
throw Boom.badImplementation(`Please set the CHANGE_EMAIL_ENABLE env variable to true to use the auth/change-email routes.`)
} else {
return next();
}
})

router.post('/request', requestVerification)
router.post('/change', changeVerified)
router.post('/', directChange)


export default router
10 changes: 7 additions & 3 deletions src/routes/auth/change-email/request-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Boom from '@hapi/boom'

import { setNewTicket, setNewEmail } from '@shared/queries'
import { asyncWrapper } from '@shared/helpers'
import { EMAILS_ENABLE, SERVER_URL } from '@shared/config'
import { APPLICATION, AUTHENTICATION } from '@shared/config'
import { emailClient } from '@shared/email'
import { request } from '@shared/request'
import { SetNewEmailData } from '@shared/types'
Expand All @@ -13,10 +13,14 @@ import { getRequestInfo } from './utils'
import { RequestExtended } from '@shared/types'

async function requestChangeEmail(req: RequestExtended, res: Response): Promise<unknown> {
if(!AUTHENTICATION.VERIFY_EMAILS) {
throw Boom.badImplementation(`Please set the VERIFY_EMAILS env variable to true to use the auth/change-email/request route.`)
}

const { user_id, new_email } = await getRequestInfo(req)

// smtp must be enabled for request change password to work.
if (!EMAILS_ENABLE) {
if (!APPLICATION.EMAILS_ENABLE) {
throw Boom.badImplementation('SMTP settings unavailable')
}

Expand Down Expand Up @@ -52,7 +56,7 @@ async function requestChangeEmail(req: RequestExtended, res: Response): Promise<
template: 'change-email',
locals: {
ticket,
url: SERVER_URL,
url: APPLICATION.SERVER_URL,
display_name
},
message: {
Expand Down
10 changes: 7 additions & 3 deletions src/routes/auth/change-email/verify-and-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { request } from '@shared/request'
import { verifySchema } from '@shared/validation'
import { UpdateAccountData } from '@shared/types'
import { v4 as uuidv4 } from 'uuid'
import { NOTIFY_EMAIL_CHANGE, EMAILS_ENABLE, SERVER_URL } from '@shared/config'
import { APPLICATION, AUTHENTICATION } from '@shared/config'
import { emailClient } from '@shared/email'

async function changeEmail({ body }: Request, res: Response): Promise<unknown> {
if(!AUTHENTICATION.VERIFY_EMAILS) {
throw Boom.badImplementation(`Please set the VERIFY_EMAILS env variable to true to use the auth/change-email/change route.`)
}

const { ticket } = await verifySchema.validateAsync(body)

const { email, new_email, user } = await selectAccountByTicket(ticket)
Expand All @@ -26,12 +30,12 @@ async function changeEmail({ body }: Request, res: Response): Promise<unknown> {
throw Boom.unauthorized('Invalid or expired ticket.')
}

if (NOTIFY_EMAIL_CHANGE && EMAILS_ENABLE) {
if (AUTHENTICATION.NOTIFY_EMAIL_CHANGE && APPLICATION.EMAILS_ENABLE) {
try {
await emailClient.send({
template: 'notify-email-change',
locals: {
url: SERVER_URL,
url: APPLICATION.SERVER_URL,
display_name: user.display_name
},
message: {
Expand Down
8 changes: 2 additions & 6 deletions src/routes/auth/change-password/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Router } from 'express'

import { LOST_PASSWORD_ENABLE } from '@shared/config'

import lost from './lost'
import change from './change'
import reset from './reset'
Expand All @@ -10,9 +8,7 @@ const router = Router()

router.post('/', change)

if (LOST_PASSWORD_ENABLE) {
router.post('/request', lost)
router.post('/change', reset)
}
router.post('/request', lost)
router.post('/change', reset)

export default router
Loading

0 comments on commit 3d14fcc

Please sign in to comment.