fix(services): restrict GET /uploads/messages/:file to authorized users

This commit is contained in:
Divlo 2022-01-06 18:13:13 +01:00
parent 03946f26e7
commit 97b1d04261
No known key found for this signature in database
GPG Key ID: 8F9478F220CE65E9
23 changed files with 1314 additions and 2534 deletions

View File

@ -1,6 +1,6 @@
{
"extends": ["standard-with-typescript", "eslint-config-prettier"],
"plugins": ["unicorn", "eslint-plugin-prettier"],
"extends": ["standard-with-typescript", "prettier"],
"plugins": ["unicorn", "import", "prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},
@ -10,6 +10,11 @@
},
"rules": {
"prettier/prettier": "error",
"import/order": [
"error",
{ "groups": ["builtin", "external", "internal"] }
],
"import/extensions": ["error", "always"],
"unicorn/prefer-node-protocol": "error",
"unicorn/prevent-abbreviations": "error"
}

View File

@ -21,13 +21,27 @@ jobs:
- name: 'Install'
run: 'npm install'
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- run: 'npm run lint:editorconfig'
- run: 'npm run lint:markdown'
- run: 'npm run lint:docker'
- run: 'npm run lint:typescript'
- name: 'lint:commit'
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- name: 'dotenv-linter'
- name: 'lint:editorconfig'
run: 'npm run lint:editorconfig'
- name: 'lint:markdown'
run: 'npm run lint:markdown'
- name: 'lint:typescript'
run: 'npm run lint:typescript'
- name: 'lint:prettier'
run: 'npm run lint:prettier'
- name: 'lint:dotenv'
uses: 'dotenv-linter/action-dotenv-linter@v2'
with:
github_token: ${{ secrets.github_token }}
- name: 'lint:docker'
uses: 'hadolint/hadolint-action@v1.6.0'
with:
dockerfile: './Dockerfile'

View File

@ -6,6 +6,5 @@
"jest --findRelatedTests"
],
"*.{yml,json}": ["prettier --write"],
"*.{md}": ["prettier --write", "markdownlint --dot --fix"],
"./Dockerfile": ["dockerfilelint"]
"*.{md}": ["prettier --write", "markdownlint --dot --fix"]
}

View File

@ -3,3 +3,4 @@ node_modules
coverage
package.json
package-lock.json
*.hbs

View File

@ -1,5 +1,6 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifierEnding": "js",
"prettier.configPath": ".prettierrc.json",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {

View File

@ -1,22 +1,23 @@
FROM node:16.13.1 AS dependencies
WORKDIR /usr/src/app
COPY ./package*.json ./
RUN npm clean-install
RUN npm install
FROM node:16.13.1 AS builder
WORKDIR /usr/src/app
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
COPY ./ ./
RUN npx prisma generate
RUN npm run build
RUN npm run prisma:generate && npm run build
FROM node:16.13.1 AS runner
WORKDIR /usr/src/app
ENV NODE_ENV=production
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/start.sh ./start.sh
COPY --from=builder /usr/src/app/package.json ./package.json
COPY --from=builder /usr/src/app/email ./email
COPY --from=builder /usr/src/app/build ./build
COPY --from=builder /usr/src/app/prisma ./prisma
COPY --from=builder /usr/src/app/uploads ./uploads
USER node
CMD npm run prisma:migrate:deploy && node build/index.js
CMD ["./docker-start.sh"]

4
docker-start.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
npm run prisma:migrate:deploy
node build/index.js

3538
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,10 +17,10 @@
"dev": "concurrently -k -n \"TypeScript,Node\" -p \"[{name}]\" -c \"blue,green\" \"swc ./src --out-dir ./build --watch\" \"cross-env NODE_ENV=development nodemon -e js,json,yaml build/index.js\"",
"generate": "plop",
"lint:commit": "commitlint",
"lint:docker": "dockerfilelint './Dockerfile'",
"lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'",
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
"lint:prettier": "prettier '.' --check",
"lint:staged": "lint-staged",
"test": "jest",
"prisma:generate": "prisma generate",
@ -52,14 +52,14 @@
"ms": "2.1.3",
"nodemailer": "6.7.2",
"read-pkg": "5.2.0",
"socket.io": "4.4.0"
"socket.io": "4.4.1"
},
"devDependencies": {
"@commitlint/cli": "16.0.1",
"@commitlint/config-conventional": "16.0.0",
"@saithodev/semantic-release-backmerge": "2.1.0",
"@swc/cli": "0.1.55",
"@swc/core": "1.2.124",
"@swc/core": "1.2.127",
"@swc/jest": "0.2.15",
"@types/bcryptjs": "2.4.2",
"@types/busboy": "0.3.1",
@ -68,26 +68,25 @@
"@types/jest": "27.4.0",
"@types/jsonwebtoken": "8.5.6",
"@types/ms": "0.7.31",
"@types/node": "17.0.5",
"@types/node": "17.0.8",
"@types/nodemailer": "6.4.4",
"@typescript-eslint/eslint-plugin": "4.33.0",
"concurrently": "6.5.1",
"concurrently": "7.0.0",
"cross-env": "7.0.3",
"dockerfilelint": "1.8.0",
"editorconfig-checker": "4.0.2",
"eslint": "7.32.0",
"eslint-config-prettier": "8.3.0",
"eslint-config-standard-with-typescript": "21.0.1",
"eslint-plugin-import": "2.25.3",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-promise": "5.1.1",
"eslint-plugin-unicorn": "40.0.0",
"husky": "7.0.4",
"jest": "27.4.5",
"jest": "27.4.7",
"jest-mock-extended": "2.0.4",
"jest-ts-webcompat-resolver": "1.0.0",
"lint-staged": "12.1.4",
"lint-staged": "12.1.5",
"markdownlint-cli": "0.30.0",
"nodemon": "2.0.15",
"plop": "3.0.5",

View File

@ -1,6 +1,5 @@
import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset } from 'jest-mock-extended'
import { DeepMockProxy } from 'jest-mock-extended/lib/cjs/Mock'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
import prisma from '../tools/database/prisma.js'

View File

@ -6,8 +6,8 @@ import { userSettingsExample } from '../../models/UserSettings.js'
import {
generateAccessToken,
generateRefreshToken
} from '../../tools/utils/jwtToken'
import { prismaMock } from '../setup'
} from '../../tools/utils/jwtToken.js'
import { prismaMock } from '../setup.js'
export const authenticateUserTest = async (): Promise<{
accessToken: string

View File

@ -1,7 +1,7 @@
import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import { prismaMock } from '../../../../__test__/setup.js'
import { memberExample } from '../../../../models/Member'
import { memberExample } from '../../../../models/Member.js'
import { channelExample } from '../../../../models/Channel.js'
describe('GET /channels/[channelId]', () => {

View File

@ -44,7 +44,6 @@ const postServiceSchema: FastifySchema = {
401: fastifyErrors[401],
403: fastifyErrors[403],
404: fastifyErrors[404],
431: fastifyErrors[431],
500: fastifyErrors[500]
}
} as const

View File

@ -42,6 +42,7 @@ const postServiceSchema: FastifySchema = {
401: fastifyErrors[401],
403: fastifyErrors[403],
404: fastifyErrors[404],
431: fastifyErrors[431],
500: fastifyErrors[500]
}
} as const

View File

@ -1,8 +1,8 @@
import { application } from '../../../../../application.js'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import { prismaMock } from '../../../../../__test__/setup.js'
import { memberExample } from '../../../../../models/Member'
import { guildExample } from '../../../../../models/Guild'
import { memberExample } from '../../../../../models/Member.js'
import { guildExample } from '../../../../../models/Guild.js'
import { channelExample } from '../../../../../models/Channel.js'
describe('GET /guilds/[guildId]/channels', () => {

View File

@ -1,9 +1,9 @@
import { Static, Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import fastifyMultipart from 'fastify-multipart'
import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { fastifyErrors } from '../../../../models/utils.js'
import fastifyMultipart from 'fastify-multipart'
import prisma from '../../../../tools/database/prisma.js'
import { uploadFile } from '../../../../tools/utils/uploadFile.js'
import { guildSchema } from '../../../../models/Guild.js'

View File

@ -0,0 +1,40 @@
import path from 'node:path'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox'
import { fastifyErrors } from '../../../models/utils.js'
const parameters = Type.Object({
file: Type.String()
})
type Parameters = Static<typeof parameters>
export const getServiceSchema: FastifySchema = {
tags: ['uploads'] as string[],
params: parameters,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
export const getGuildsUploadsService: FastifyPluginAsync = async (fastify) => {
fastify.route<{
Params: Parameters
}>({
method: 'GET',
url: '/uploads/guilds/:file',
schema: getServiceSchema,
handler: async (request, reply) => {
const { file } = request.params
return await reply.sendFile(path.join('guilds', file))
}
})
}

View File

@ -1,58 +1,11 @@
import path from 'node:path'
import { FastifyPluginAsync } from 'fastify'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox'
import { fastifyErrors } from '../../models/utils'
const parametersUploadsSchema = Type.Object({
image: Type.String()
})
type ParametersUploadsSchemaType = Static<typeof parametersUploadsSchema>
const getUploadsSchema: FastifySchema = {
tags: ['uploads'] as string[],
params: parametersUploadsSchema,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
import { getGuildsUploadsService } from './guilds/get.js'
import { getMessagesUploadsService } from './messages/get.js'
import { getUsersUploadsService } from './users/get.js'
export const uploadsService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ Params: ParametersUploadsSchemaType }>({
method: 'GET',
url: '/uploads/users/:image',
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('users', image))
}
})
fastify.route<{ Params: ParametersUploadsSchemaType }>({
method: 'GET',
url: '/uploads/guilds/:image',
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('guilds', image))
}
})
fastify.route<{ Params: ParametersUploadsSchemaType }>({
method: 'GET',
url: '/uploads/messages/:image',
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('messages', image))
}
})
await fastify.register(getGuildsUploadsService)
await fastify.register(getMessagesUploadsService)
await fastify.register(getUsersUploadsService)
}

View File

@ -0,0 +1,76 @@
import path from 'node:path'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox'
import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import prisma from '../../../tools/database/prisma.js'
const parameters = Type.Object({
file: Type.String()
})
type Parameters = Static<typeof parameters>
export const getServiceSchema: FastifySchema = {
tags: ['uploads'] as string[],
security: [
{
bearerAuth: []
}
] as Array<{ [key: string]: [] }>,
params: parameters,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
401: fastifyErrors[401],
403: fastifyErrors[403],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
export const getMessagesUploadsService: FastifyPluginAsync = async (
fastify
) => {
await fastify.register(authenticateUser)
fastify.route<{
Params: Parameters
}>({
method: 'GET',
url: '/uploads/messages/:file',
schema: getServiceSchema,
handler: async (request, reply) => {
if (request.user == null) {
throw fastify.httpErrors.forbidden()
}
const { file } = request.params
const message = await prisma.message.findFirst({
where: { value: `/uploads/messages/${file}` },
include: {
member: {
select: { guildId: true }
}
}
})
if (message == null) {
throw fastify.httpErrors.notFound('Message not found')
}
const member = await prisma.member.findFirst({
where: {
guildId: message.member?.guildId,
userId: request.user.current.id
}
})
if (member == null) {
throw fastify.httpErrors.notFound('Member not found')
}
return await reply.sendFile(path.join('messages', file))
}
})
}

View File

@ -0,0 +1,40 @@
import path from 'node:path'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox'
import { fastifyErrors } from '../../../models/utils.js'
const parameters = Type.Object({
file: Type.String()
})
type Parameters = Static<typeof parameters>
export const getServiceSchema: FastifySchema = {
tags: ['uploads'] as string[],
params: parameters,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
export const getUsersUploadsService: FastifyPluginAsync = async (fastify) => {
fastify.route<{
Params: Parameters
}>({
method: 'GET',
url: '/uploads/users/:file',
schema: getServiceSchema,
handler: async (request, reply) => {
const { file } = request.params
return await reply.sendFile(path.join('users', file))
}
})
}

View File

@ -1,9 +1,9 @@
import { Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import fastifyMultipart from 'fastify-multipart'
import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { fastifyErrors } from '../../../../models/utils.js'
import fastifyMultipart from 'fastify-multipart'
import prisma from '../../../../tools/database/prisma.js'
import { uploadFile } from '../../../../tools/utils/uploadFile.js'
import {

View File

@ -1,6 +1,6 @@
import dotenv from 'dotenv'
import nodemailer from 'nodemailer'
import SMTPTransport from 'nodemailer/lib/smtp-transport'
import type SMTPTransport from 'nodemailer/lib/smtp-transport.js'
dotenv.config()
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)

View File

@ -1,4 +1,4 @@
import { parseStringNullish } from '../parseStringNullish'
import { parseStringNullish } from '../parseStringNullish.js'
const defaultString = 'defaultString'