fix: stricter ajv validation

This commit is contained in:
Divlo 2022-04-07 14:56:07 +00:00
parent 694ac58aad
commit 69c567cb66
No known key found for this signature in database
GPG Key ID: 8F9478F220CE65E9
19 changed files with 1075 additions and 1067 deletions

View File

@ -1,6 +1,7 @@
COMPOSE_PROJECT_NAME='thream-api' COMPOSE_PROJECT_NAME='thream-api'
HOST='0.0.0.0' HOST='0.0.0.0'
PORT='8080' PORT='8080'
NODE_ENV='development'
API_URL='http://localhost:8080' API_URL='http://localhost:8080'
DATABASE_URL='postgresql://user:password@thream-database:5432/thream' DATABASE_URL='postgresql://user:password@thream-database:5432/thream'
JWT_ACCESS_EXPIRES_IN='15 minutes' JWT_ACCESS_EXPIRES_IN='15 minutes'

2012
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"", "lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"",
"lint:prettier": "prettier \".\" --check", "lint:prettier": "prettier \".\" --check",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test": "c8 tap", "test": "cross-env NODE_ENV=test c8 tap",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:studio": "prisma studio", "prisma:studio": "prisma studio",
"prisma:migrate:dev": "prisma migrate dev", "prisma:migrate:dev": "prisma migrate dev",
@ -34,14 +34,14 @@
"postinstall": "husky install" "postinstall": "husky install"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "3.11.0", "@prisma/client": "3.12.0",
"@sinclair/typebox": "0.23.4", "@sinclair/typebox": "0.23.4",
"@thream/socketio-jwt": "2.2.1", "@thream/socketio-jwt": "3.0.0",
"axios": "0.26.1", "axios": "0.26.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"dotenv": "16.0.0", "dotenv": "16.0.0",
"ejs": "3.1.6", "ejs": "3.1.6",
"fastify": "3.27.4", "fastify": "3.28.0",
"fastify-cors": "6.0.3", "fastify-cors": "6.0.3",
"fastify-helmet": "7.0.1", "fastify-helmet": "7.0.1",
"fastify-multipart": "5.3.1", "fastify-multipart": "5.3.1",
@ -49,7 +49,7 @@
"fastify-rate-limit": "5.8.0", "fastify-rate-limit": "5.8.0",
"fastify-sensible": "3.1.2", "fastify-sensible": "3.1.2",
"fastify-static": "4.6.1", "fastify-static": "4.6.1",
"fastify-swagger": "5.0.0", "fastify-swagger": "5.1.0",
"fastify-url-data": "3.0.3", "fastify-url-data": "3.0.3",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
@ -62,41 +62,41 @@
"@commitlint/cli": "16.2.3", "@commitlint/cli": "16.2.3",
"@commitlint/config-conventional": "16.2.1", "@commitlint/config-conventional": "16.2.1",
"@saithodev/semantic-release-backmerge": "2.1.2", "@saithodev/semantic-release-backmerge": "2.1.2",
"@swc/cli": "0.1.56", "@swc/cli": "0.1.57",
"@swc/core": "1.2.159", "@swc/core": "1.2.164",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/busboy": "1.3.0", "@types/busboy": "1.5.0",
"@types/ejs": "3.1.0", "@types/ejs": "3.1.0",
"@types/http-errors": "1.8.2", "@types/http-errors": "1.8.2",
"@types/jsonwebtoken": "8.5.8", "@types/jsonwebtoken": "8.5.8",
"@types/ms": "0.7.31", "@types/ms": "0.7.31",
"@types/node": "17.0.21", "@types/node": "17.0.23",
"@types/nodemailer": "6.4.4", "@types/nodemailer": "6.4.4",
"@types/sinon": "10.0.11", "@types/sinon": "10.0.11",
"@types/tap": "15.0.6", "@types/tap": "15.0.6",
"@typescript-eslint/eslint-plugin": "5.15.0", "@typescript-eslint/eslint-plugin": "5.18.0",
"c8": "7.11.0", "c8": "7.11.0",
"concurrently": "7.0.0", "concurrently": "7.1.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"editorconfig-checker": "4.0.2", "editorconfig-checker": "4.0.2",
"eslint": "8.11.0", "eslint": "8.12.0",
"eslint-config-conventions": "1.1.2", "eslint-config-conventions": "2.0.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "4.0.0", "eslint-plugin-prettier": "4.0.0",
"eslint-plugin-promise": "6.0.0", "eslint-plugin-promise": "6.0.0",
"eslint-plugin-unicorn": "41.0.1", "eslint-plugin-unicorn": "42.0.0",
"husky": "7.0.4", "husky": "7.0.4",
"lint-staged": "12.3.7", "lint-staged": "12.3.7",
"markdownlint-cli": "0.31.1", "markdownlint-cli": "0.31.1",
"nodemon": "2.0.15", "nodemon": "2.0.15",
"plop": "3.0.5", "plop": "3.0.5",
"prettier": "2.6.0", "prettier": "2.6.2",
"prisma": "3.11.0", "prisma": "3.12.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"semantic-release": "19.0.2", "semantic-release": "19.0.2",
"sinon": "13.0.1", "sinon": "13.0.1",
"tap": "16.0.0", "tap": "16.0.1",
"typescript": "4.6.2" "typescript": "4.6.3"
} }
} }

View File

@ -44,7 +44,7 @@ export const authenticateUserTest = async (): Promise<{
sinon.stub(prisma, 'oAuth').value(oAuthStubValue) sinon.stub(prisma, 'oAuth').value(oAuthStubValue)
sinon.stub(prisma, 'refreshToken').value(refreshTokenStubValue) sinon.stub(prisma, 'refreshToken').value(refreshTokenStubValue)
const userJWT: UserJWT = { const userJWT: UserJWT = {
currentStrategy: 'local', currentStrategy: 'Local',
id: 1 id: 1
} }
const accessToken = generateAccessToken(userJWT) const accessToken = generateAccessToken(userJWT)

View File

@ -17,7 +17,12 @@ import { UPLOADS_URL } from './tools/configurations/index.js'
dotenv.config() dotenv.config()
export const application = fastify({ export const application = fastify({
logger: process.env.NODE_ENV === 'development' logger: process.env.NODE_ENV === 'development',
ajv: {
customOptions: {
format: 'full'
}
}
}) })
await application.register(fastifyCors) await application.register(fastifyCors)
@ -33,7 +38,7 @@ await application.register(fastifySocketIo, {
}) })
await application.register(fastifyHelmet) await application.register(fastifyHelmet)
await application.register(fastifyRateLimit, { await application.register(fastifyRateLimit, {
max: 150, max: 200,
timeWindow: '1 minute' timeWindow: '1 minute'
}) })
await application.register(fastifyStatic, { await application.register(fastifyStatic, {

View File

@ -2,8 +2,8 @@ import { Type } from '@sinclair/typebox'
import { date, id } from './utils.js' import { date, id } from './utils.js'
export const providers = ['google', 'github', 'discord'] as const export const providers = ['Google', 'GitHub', 'Discord'] as const
export const strategies = [...providers, 'local'] as const export const strategies = [...providers, 'Local'] as const
export const strategiesTypebox = strategies.map((strategy) => export const strategiesTypebox = strategies.map((strategy) =>
Type.Literal(strategy) Type.Literal(strategy)

View File

@ -27,7 +27,7 @@ export const userSchema = {
website: Type.String({ website: Type.String({
minLength: 1, minLength: 1,
maxLength: 255, maxLength: 255,
format: 'uri-reference' format: 'uri'
}), }),
isConfirmed: Type.Boolean({ default: false }), isConfirmed: Type.Boolean({ default: false }),
temporaryToken: Type.String(), temporaryToken: Type.String(),

View File

@ -21,7 +21,7 @@ await tap.test('GET /users/current', async (t) => {
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) t.equal(response.statusCode, 200)
t.equal(responseJson.user.name, user.name) t.equal(responseJson.user.name, user.name)
t.strictSame(responseJson.user.strategies, ['local']) t.strictSame(responseJson.user.strategies, ['Local'])
}) })
await t.test('fails with unauthenticated user', async (t) => { await t.test('fails with unauthenticated user', async (t) => {

View File

@ -44,7 +44,7 @@ export const getCurrentUser: FastifyPluginAsync = async (fastify) => {
return oauth.provider return oauth.provider
}) })
if (user.current.password != null) { if (user.current.password != null) {
strategies.push('local') strategies.push('Local')
} }
reply.statusCode = 200 reply.statusCode = 200
return { return {

View File

@ -52,7 +52,7 @@ export const putCurrentUser: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Body: BodyPutServiceSchemaType Body: BodyPutServiceSchemaType
Params: QueryPutCurrentUserSchemaType Querystring: QueryPutCurrentUserSchemaType
}>({ }>({
method: 'PUT', method: 'PUT',
url: '/users/current', url: '/users/current',
@ -62,7 +62,7 @@ export const putCurrentUser: FastifyPluginAsync = async (fastify) => {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { name, email, status, biography, website } = request.body const { name, email, status, biography, website } = request.body
const { redirectURI } = request.params const { redirectURI } = request.query
const userValidation = await prisma.user.findFirst({ const userValidation = await prisma.user.findFirst({
where: { where: {
OR: [ OR: [
@ -90,9 +90,9 @@ export const putCurrentUser: FastifyPluginAsync = async (fastify) => {
return oauth.provider return oauth.provider
}) })
if (request.user.current.password != null) { if (request.user.current.password != null) {
strategies.push('local') strategies.push('Local')
} }
if (email === null && strategies.includes('local')) { if (email === null && strategies.includes('Local')) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
'You must have an email to sign in.' 'You must have an email to sign in.'
) )

View File

@ -53,7 +53,7 @@ export const deleteProviderService: FastifyPluginAsync = async (fastify) => {
return oauth.provider return oauth.provider
}) })
if (user.current.password != null) { if (user.current.password != null) {
strategies.push('local') strategies.push('Local')
} }
const oauthProvider = OAuths.find((oauth) => oauth.provider === provider) const oauthProvider = OAuths.find((oauth) => oauth.provider === provider)
if (oauthProvider == null) { if (oauthProvider == null) {

View File

@ -4,8 +4,8 @@ import axios from 'axios'
import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js' import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js'
export const DISCORD_PROVIDER = 'discord' export const DISCORD_PROVIDER = 'Discord'
export const DISCORD_BASE_URL = 'https://discord.com/api/v6' export const DISCORD_BASE_URL = 'https://discord.com/api/v10'
export const DISCORD_CLIENT_ID = export const DISCORD_CLIENT_ID =
process.env.DISCORD_CLIENT_ID ?? 'DISCORD_CLIENT_ID' process.env.DISCORD_CLIENT_ID ?? 'DISCORD_CLIENT_ID'
export const DISCORD_CLIENT_SECRET = export const DISCORD_CLIENT_SECRET =

View File

@ -4,7 +4,7 @@ import axios from 'axios'
import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js' import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js'
export const GITHUB_PROVIDER = 'github' export const GITHUB_PROVIDER = 'GitHub'
export const GITHUB_BASE_URL = 'https://github.com' export const GITHUB_BASE_URL = 'https://github.com'
export const GITHUB_API_BASE_URL = 'https://api.github.com' export const GITHUB_API_BASE_URL = 'https://api.github.com'
export const GITHUB_CLIENT_ID = export const GITHUB_CLIENT_ID =

View File

@ -4,7 +4,7 @@ import axios from 'axios'
import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js' import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js'
export const GOOGLE_PROVIDER = 'google' export const GOOGLE_PROVIDER = 'Google'
export const GOOGLE_BASE_URL = 'https://accounts.google.com/o/oauth2/v2/auth' export const GOOGLE_BASE_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
export const GOOGLE_OAUTH2_TOKEN = 'https://oauth2.googleapis.com/token' export const GOOGLE_OAUTH2_TOKEN = 'https://oauth2.googleapis.com/token'
export const GOOGLE_USERINFO = export const GOOGLE_USERINFO =

View File

@ -53,11 +53,11 @@ export const postSigninUser: FastifyPluginAsync = async (fastify) => {
throw fastify.httpErrors.badRequest('Invalid credentials.') throw fastify.httpErrors.badRequest('Invalid credentials.')
} }
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
currentStrategy: 'local', currentStrategy: 'Local',
id: user.id id: user.id
}) })
const refreshToken = await generateRefreshToken({ const refreshToken = await generateRefreshToken({
currentStrategy: 'local', currentStrategy: 'Local',
id: user.id id: user.id
}) })
reply.statusCode = 200 reply.statusCode = 200

View File

@ -58,7 +58,7 @@ await tap.test('POST /users/signup', async (t) => {
url: '/users/signup', url: '/users/signup',
payload: { payload: {
...payload, ...payload,
email: 'incorrect-email' email: 'incorrect-email@abc'
} }
}) })
t.equal(response.statusCode, 400) t.equal(response.statusCode, 400)

View File

@ -22,7 +22,7 @@ await tap.test(
return userExample return userExample
} }
}) })
const currentStrategy = 'local' const currentStrategy = 'Local'
sinon.stub(jwt, 'verify').value(() => { sinon.stub(jwt, 'verify').value(() => {
return { id: userExample.id, currentStrategy } return { id: userExample.id, currentStrategy }
}) })
@ -89,7 +89,7 @@ await tap.test(
} }
}) })
sinon.stub(jwt, 'verify').value(() => { sinon.stub(jwt, 'verify').value(() => {
return { id: userExample.id, currentStrategy: 'local' } return { id: userExample.id, currentStrategy: 'Local' }
}) })
await t.rejects(getUserWithBearerToken('Bearer token'), BadRequest) await t.rejects(getUserWithBearerToken('Bearer token'), BadRequest)
} }

View File

@ -33,7 +33,7 @@ export const getUserWithBearerToken = async (
throw new Forbidden() throw new Forbidden()
} }
if (!user.isConfirmed && payload.currentStrategy === 'local') { if (!user.isConfirmed && payload.currentStrategy === 'Local') {
throw new BadRequest( throw new BadRequest(
'You should have a confirmed account, please check your email and follow the instructions to verify your account' 'You should have a confirmed account, please check your email and follow the instructions to verify your account'
) )

View File

@ -7,7 +7,7 @@ import { OAuthStrategy } from '../OAuthStrategy.js'
import prisma from '../../database/prisma.js' import prisma from '../../database/prisma.js'
import { refreshTokenExample } from '../../../models/RefreshToken.js' import { refreshTokenExample } from '../../../models/RefreshToken.js'
const oauthStrategy = new OAuthStrategy('discord') const oauthStrategy = new OAuthStrategy('Discord')
await tap.test('tools/utils/OAuthStrategy', async (t) => { await tap.test('tools/utils/OAuthStrategy', async (t) => {
await t.test('callbackSignin', async (t) => { await t.test('callbackSignin', async (t) => {
@ -47,7 +47,7 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
return { return {
id: 1, id: 1,
userId: userExample.id, userId: userExample.id,
provider: 'discord', provider: 'Discord',
providerId: id, providerId: id,
updatedAt: new Date(), updatedAt: new Date(),
createdAt: new Date() createdAt: new Date()
@ -64,7 +64,7 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
oAuthCreateSpy.calledWith({ oAuthCreateSpy.calledWith({
data: { data: {
userId: userExample.id, userId: userExample.id,
provider: 'discord', provider: 'Discord',
providerId: id providerId: id
} }
}), }),
@ -73,7 +73,7 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
t.equal( t.equal(
oAuthFindFirstSpy.calledWith({ oAuthFindFirstSpy.calledWith({
where: { where: {
provider: 'discord', provider: 'Discord',
providerId: id providerId: id
} }
}), }),
@ -108,7 +108,7 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
return { return {
id: 1, id: 1,
userId: userExample.id, userId: userExample.id,
provider: 'discord', provider: 'Discord',
providerId: id, providerId: id,
updatedAt: new Date(), updatedAt: new Date(),
createdAt: new Date() createdAt: new Date()
@ -119,14 +119,14 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
const oAuthFindFirstSpy = sinon.spy(prisma.oAuth, 'findFirst') const oAuthFindFirstSpy = sinon.spy(prisma.oAuth, 'findFirst')
const result = await oauthStrategy.callbackAddStrategy( const result = await oauthStrategy.callbackAddStrategy(
{ id, name }, { id, name },
{ accessToken: '123', current: userExample, currentStrategy: 'local' } { accessToken: '123', current: userExample, currentStrategy: 'Local' }
) )
t.equal(result, 'success') t.equal(result, 'success')
t.equal( t.equal(
oAuthCreateSpy.calledWith({ oAuthCreateSpy.calledWith({
data: { data: {
userId: userExample.id, userId: userExample.id,
provider: 'discord', provider: 'Discord',
providerId: id providerId: id
} }
}), }),
@ -135,7 +135,7 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
t.equal( t.equal(
oAuthFindFirstSpy.calledWith({ oAuthFindFirstSpy.calledWith({
where: { where: {
provider: 'discord', provider: 'Discord',
providerId: id providerId: id
} }
}), }),
@ -153,7 +153,7 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
return { return {
id: 1, id: 1,
userId: 2, userId: 2,
provider: 'discord', provider: 'Discord',
providerId: id, providerId: id,
updatedAt: new Date(), updatedAt: new Date(),
createdAt: new Date() createdAt: new Date()
@ -163,13 +163,13 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
const oAuthFindFirstSpy = sinon.spy(prisma.oAuth, 'findFirst') const oAuthFindFirstSpy = sinon.spy(prisma.oAuth, 'findFirst')
const result = await oauthStrategy.callbackAddStrategy( const result = await oauthStrategy.callbackAddStrategy(
{ id, name }, { id, name },
{ accessToken: '123', current: userExample, currentStrategy: 'local' } { accessToken: '123', current: userExample, currentStrategy: 'Local' }
) )
t.equal(result, 'This account is already used by someone else') t.equal(result, 'This account is already used by someone else')
t.equal( t.equal(
oAuthFindFirstSpy.calledWith({ oAuthFindFirstSpy.calledWith({
where: { where: {
provider: 'discord', provider: 'Discord',
providerId: id providerId: id
} }
}), }),
@ -188,7 +188,7 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
return { return {
id: 1, id: 1,
userId: userExample.id, userId: userExample.id,
provider: 'discord', provider: 'Discord',
providerId: id, providerId: id,
updatedAt: new Date(), updatedAt: new Date(),
createdAt: new Date() createdAt: new Date()
@ -198,13 +198,13 @@ await tap.test('tools/utils/OAuthStrategy', async (t) => {
const oAuthFindFirstSpy = sinon.spy(prisma.oAuth, 'findFirst') const oAuthFindFirstSpy = sinon.spy(prisma.oAuth, 'findFirst')
const result = await oauthStrategy.callbackAddStrategy( const result = await oauthStrategy.callbackAddStrategy(
{ id, name }, { id, name },
{ accessToken: '123', current: userExample, currentStrategy: 'local' } { accessToken: '123', current: userExample, currentStrategy: 'Local' }
) )
t.equal(result, 'You are already using this account') t.equal(result, 'You are already using this account')
t.equal( t.equal(
oAuthFindFirstSpy.calledWith({ oAuthFindFirstSpy.calledWith({
where: { where: {
provider: 'discord', provider: 'Discord',
providerId: id providerId: id
} }
}), }),