Промпт: ревью pull request по diff
Ревью изменений с приоритетами: блокирующие ошибки, безопасность, тесты, готовые комментарии. Готовый промт на русском: скопируйте, подставьте свои данные и вставьте в GigaChat, YandexGPT, ChatGPT, Claude или Gemini.
Текст промпта
Что подставить в промт
| Поле | Пример |
|---|---|
| {goal} | эндпоинт загрузки аватара пользователя |
| {rules} | Node.js 20, Express, TypeScript, хранилище Yandex Object Storage через S3 API, входные данные валидируем через zod, авторизация через middleware requireAuth |
| {diff} | + router.post('/users/:id/avatar', upload.single('file'), async (req, res) => { + const user = await db.user.findUnique({ where: { id: req.params.id } }) + const key = 'avatars/' + req.params.id + '/' + req.file.originalname + await s3.putObject({ Bucket: BUCKET, Key: key, Body: req.file.buffer }) + await db.user.update({ where: { id: user.id }, data: { avatarKey: key } }) + res.json({ url: CDN + '/' + key }) + }) + + const upload = multer({ storage: multer.memoryStorage() }) |
Ответы нейросетей на этот промпт
Итог: Request Changes
Технически функция работает, но есть серьёзные проблемы безопасности и корректности работы, которые требуют исправления перед слиянием.
Блокирующие замечания 🔥
Строка: const key = 'avatars/' + req.params.id + '/' + req.file.originalname
📌 Что не так: Используется оригинальное имя файла (originalname), которое легко подделывается злоумышленником — это создаёт уязвимость типа Path Traversal и открывает возможность атак XSS и CSRF. Необходимо проверить расширение файла и генерировать уникальное имя самостоятельно.
✅ Исправление: Генерируйте случайный хеш имени файла вместе с расширением и проверяйте допустимые типы файлов заранее.
import crypto from 'crypto';
// Допустимые расширения изображений
const validExtensions = ['jpg', 'jpeg', 'png'];
const extension = path.extname(req.file.originalname);
if (!validExtensions.includes(extension)) throw new Error('Invalid file type');
const hashName = crypto.randomBytes(16).toString('hex') + extension;
const key = `avatars/${req.params.id}/${hashName}`;
Строка: await db.user.update({ where: { id: user.id }, data: { avatarKey: key } })
📌 Что не так: Нет проверки, совпадает ли параметр id запроса с идентификатором текущего аутентифицированного пользователя. Это позволяет загружать файл за другого пользователя.
✅ Исправление: Проверять соответствие ID пользователя из токена и параметра маршрута.
const currentUserId = getCurrentUserFromToken(); // Метод получения пользователя из middleware auth
if (currentUserId !== Number(req.params.id)) return res.status(403).json({ error: 'Forbidden' });
Строка: upload.single('file')
📌 Что не так: Отсутствует ограничение размера загружаемого файла. Если не ограничить размер файла, сервер рискует перегрузкой памяти и отказом в обслуживании.
✅ Исправление: Добавьте ограничение максимального размера файла.
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 } // Ограничение до 5MB
});
Важные замечания ⚠️
Строка: router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {...})
📌 Что не так: Ошибки внутри обработчика запросов обрабатываются плохо — потенциальная ошибка будет возвращаться пользователю как внутренняя ошибка сервера (500 Internal Server Error). В случае ошибки клиент должен получать осмысленные HTTP-коды и понятные описания ошибок.
✅ Исправление: Обрабатывайте ошибки централизованно либо используйте try-catch блок вокруг асинхронной части.
try {
// основная логика здесь...
} catch(err) {
console.error(err);
if (err instanceof KnownErrorType) {
return res.status(err.httpCode || 500).json({ message: err.message });
}
return res.status(500).send("Internal server error");
}
Отсутствуют: Валидаторы входных данных
📌 Что не так: Не проводится предварительная проверка полей (например, MIME-типа файла, допустимых размеров изображения) с помощью библиотеки Zod. Из-за этого возможен приём некорректных данных.
✅ Исправление: Создать схему Zod для проверок поля file.
import { z } from 'zod';
export const AvatarUploadSchema = z.object({
file: z.instanceof(File)
}).refine((data) => {
const isValidImage = ['image/jpeg', 'image/png'].includes(data.file.mimetype);
return isValidImage && data.file.size <= 5 * 1024 * 1024;
}, {
message: 'File must be a JPEG or PNG image under 5 MB'
});
Мелкие замечания ✨
Строка: await db.user.findUnique({ where: { id: req.params.id } })
📌 Что не так: Использован метод .findUnique, который предполагает наличие уникального ключа. Лучше использовать более точный термин .findFirst.
✅ Исправление: Замените на db.user.findFirst(...)
Какие тесты отсутствуют? 🎯
- Тесты интеграционного уровня для всей цепочки обработки запроса (от посылаемого POST-запроса до обновления базы данных и возвращения результата клиенту).
- Тесты покрытия исключительных ситуаций (некорректные форматы файлов, превышение лимитов, отсутствие пользователя в БД).
- Тесты взаимодействия с внешним сервисом хранения (S3/Yandex Object Storage): mocking для внешних зависимостей и проверки успешности операций.
Что реализовано хорошо?
- Логика хранения файлов сделана правильно: используется временная память (multer memory storage), что минимизирует риски блокировки файловых ресурсов и упрощает работу с большими файлами.
- Простая структура маршрутов и хорошая организация промежуточных функций (
requireAuth, middleware).
1. Итог
Request changes. В текущем виде эндпоинт нельзя безопасно выкатывать: нет requireAuth и проверки прав на пользователя, есть runtime-баг с порядком объявления upload, не обрабатываются req.file/user === null, ключ строится из originalname, а загрузка в память не ограничена по размеру и типу файла.
2. Замечания по приоритету
Блокирующие
+ router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
Что не так
upload используется до объявления:
const upload = multer({ storage: multer.memoryStorage() })
Так как const находится ниже, при регистрации роутера будет ReferenceError: Cannot access 'upload' before initialization.
Как исправить
Перенести объявление upload выше регистрации роутера:
const upload = multer({
storage: multer.memoryStorage(),
})
router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
// ...
})
+ router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
Что не так
На эндпоинте нет requireAuth. Также нужно проверить, что авторизованный пользователь имеет право менять аватар именно этого пользователя. Иначе любой клиент сможет загрузить аватар другому пользователю, зная id.
Как исправить
Добавить middleware авторизации и проверку прав. Конкретное поле с пользователем зависит от реализации requireAuth, это нужно посмотреть в коде проекта. Примерно:
router.post(
'/users/:id/avatar',
requireAuth,
upload.single('file'),
async (req, res) => {
if (req.user.id !== req.params.id) {
return res.status(403).json({ error: 'Forbidden' })
}
// ...
},
)
Если админам разрешено менять чужой аватар, условие нужно расширить:
if (req.user.id !== req.params.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' })
}
+ const user = await db.user.findUnique({ where: { id: req.params.id } })
Что не так
Входной id из params не валидируется через zod, хотя это правило команды. Также дальше используется user.id, но findUnique может вернуть null, что приведёт к падению с Cannot read properties of null.
Как исправить
Добавить zod-схему для params и обработать отсутствие пользователя:
const avatarParamsSchema = z.object({
id: z.string().uuid(),
})
router.post(
'/users/:id/avatar',
requireAuth,
upload.single('file'),
async (req, res) => {
const params = avatarParamsSchema.parse(req.params)
const user = await db.user.findUnique({
where: { id: params.id },
})
if (!user) {
return res.status(404).json({ error: 'User not found' })
}
// ...
},
)
Если id в проекте не UUID, нужно использовать соответствующую схему, например z.string().min(1) или z.coerce.number().int().positive().
+ const key = 'avatars/' + req.params.id + '/' + req.file.originalname
Что не так
req.file может быть undefined, если файл не передан или поле называется не file. Тогда будет runtime-ошибка. Кроме того, originalname приходит от клиента и не должен использоваться как часть ключа без очистки: в нём могут быть спецсимволы, пробелы, ../, очень длинные имена, коллизии. Также повторная загрузка файла с тем же именем перезапишет объект.
Как исправить
Проверить наличие файла и генерировать серверный ключ. Расширение лучше брать из разрешённого MIME-типа, а не доверять имени файла:
const avatarFileSchema = z.object({
mimetype: z.enum(['image/jpeg', 'image/png', 'image/webp']),
buffer: z.instanceof(Buffer),
size: z.number().max(5 * 1024 * 1024),
})
const mimeToExt: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/webp': 'webp',
}
router.post(
'/users/:id/avatar',
requireAuth,
upload.single('file'),
async (req, res) => {
const params = avatarParamsSchema.parse(req.params)
if (!req.file) {
return res.status(400).json({ error: 'File is required' })
}
const file = avatarFileSchema.parse(req.file)
const ext = mimeToExt[file.mimetype]
const key = `avatars/${params.id}/${crypto.randomUUID()}.${ext}`
// ...
},
)
Нужно не забыть импортировать crypto:
import crypto from 'node:crypto'
+ await s3.putObject({ Bucket: BUCKET, Key: key, Body: req.file.buffer })
Что не так
Не передаётся ContentType, поэтому CDN/браузер может отдавать файл как application/octet-stream. Также сейчас загружать можно любой файл, включая не-изображения, если это не ограничено в multer.
Как исправить
Передавать ContentType и ограничить типы файлов на уровне multer и zod:
const allowedMimeTypes = new Set(['image/jpeg', 'image/png', 'image/webp'])
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024,
files: 1,
},
fileFilter: (_req, file, cb) => {
if (!allowedMimeTypes.has(file.mimetype)) {
return cb(new Error('Unsupported file type'))
}
cb(null, true)
},
})
И при загрузке:
await s3.putObject({
Bucket: BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
})
+ const upload = multer({ storage: multer.memoryStorage() })
Что не так
memoryStorage() без лимита размера позволяет загрузить большой файл в память процесса и потенциально положить сервис по OOM.
Как исправить
Задать лимиты:
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024,
files: 1,
},
})
Размер нужно согласовать с продуктовым требованием, но лимит должен быть.
Важные
+ await s3.putObject({ Bucket: BUCKET, Key: key, Body: req.file.buffer })
+ await db.user.update({ where: { id: user.id }, data: { avatarKey: key } })
Что не так
Если загрузка в S3 прошла успешно, а db.user.update упал, в бакете останется неиспользуемый объект. Также старый аватар пользователя не удаляется, если это требуется продуктово, из-за чего будут копиться orphan-файлы.
Как исправить
Минимально — удалить новый объект при ошибке обновления БД:
await s3.putObject({
Bucket: BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
})
try {
await db.user.update({
where: { id: user.id },
data: { avatarKey: key },
})
} catch (error) {
await s3.deleteObject({
Bucket: BUCKET,
Key: key,
})
throw error
}
Если по требованиям старый аватар нужно удалять, сохранить старый ключ и удалить его после успешного обновления:
const oldAvatarKey = user.avatarKey
await db.user.update({
where: { id: user.id },
data: { avatarKey: key },
})
if (oldAvatarKey) {
await s3.deleteObject({
Bucket: BUCKET,
Key: oldAvatarKey,
})
}
Здесь нужно уточнить ожидаемое поведение: хранить историю аватаров или заменять файл.
+ router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
Что не так
Нет явной обработки ошибок в async handler. Если в проекте есть общий wrapper для async routes — нужно использовать его. Если нет, rejected promise может уйти в стандартный error handler не так, как принято в API.
Как исправить
Если в проекте есть asyncHandler, обернуть:
router.post(
'/users/:id/avatar',
requireAuth,
upload.single('file'),
asyncHandler(async (req, res) => {
// ...
}),
)
Если такого helper нет, стоит добавить или использовать принятый в проекте способ передачи ошибки в next.
+ res.json({ url: CDN + '/' + key })
Что не так
Конкатенация URL может дать двойной слэш или некорректный URL, если CDN уже заканчивается на /. Кроме того, key лучше URL-энкодить по сегментам, если в нём потенциально бывают спецсимволы. При генерации ключа через UUID проблема почти исчезает, но всё равно лучше централизовать сборку URL.
Как исправить
const url = new URL(key, CDN.endsWith('/') ? CDN : `${CDN}/`).toString()
res.json({ url })
Если CDN не абсолютный URL, нужно использовать helper, принятый в проекте.
+ await s3.putObject({ Bucket: BUCKET, Key: key, Body: req.file.buffer })
Что не так
Не видно, какой тип s3 используется. Для AWS SDK v3 вызов обычно делается через client.send(new PutObjectCommand(...)), а не s3.putObject(...). Если в проекте используется wrapper — всё ок, но это нужно сверить с существующим кодом.
Как исправить
Если это AWS SDK v3 без wrapper:
await s3.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
}),
)
Если это wrapper, замечание неактуально.
Мелкие
+ const key = 'avatars/' + req.params.id + '/' + req.file.originalname
Что не так
Конкатенация строк хуже читается и проще ошибиться со слэшами.
Как исправить
const key = `avatars/${params.id}/${crypto.randomUUID()}.${ext}`
+ router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
Что не так
После добавления валидации и проверок handler станет довольно большим. Лучше вынести схему, настройки multer и построение ключа рядом, но отдельно от тела route handler.
Как исправить
Например:
const avatarParamsSchema = z.object({
id: z.string().uuid(),
})
const buildAvatarKey = (userId: string, ext: string) =>
`avatars/${userId}/${crypto.randomUUID()}.${ext}`
3. Каких тестов не хватает
- Авторизация
401, если пользователь не авторизован.403, если пользователь пытается загрузить аватар другому пользователю.- успешная загрузка для своего пользователя.
- Валидация params
- невалидный
idвозвращает400. - несуществующий пользователь возвращает
404.
- Валидация файла
- нет файла в поле
file→400. - неверное имя поля →
400. - неподдерживаемый MIME-type →
400. - файл больше лимита →
413или принятый в проекте код ошибки. - валидные
jpeg/png/webpпроходят.
- Интеграция с Object Storage
putObjectвызывается с правильнымиBucket,Key,Body,ContentType.- ключ не строится из
originalname. - в ответе возвращается корректный CDN URL.
- Ошибки и консистентность
- если
putObjectпадает,db.user.updateне вызывается. - если
db.user.updateпадает после успешногоputObject, новый объект удаляется из бакета. - если по требованиям старый аватар должен удаляться, проверить удаление старого объекта после успешного обновления.
- Регрессия по безопасности
- загрузка файла с
originalnameвроде../../x.pngне влияет на итоговыйKey. - повторная загрузка не перезаписывает предыдущий объект из-за одинакового имени файла.
4. Что сделано хорошо
- Используется
multer.memoryStorage(), что подходит для прямой передачи файла в S3/Object Storage без временного файла на диске. - Логика в целом простая и понятная: загрузка объекта в storage, сохранение ключа в профиле пользователя и возврат CDN URL.
Ревью PR: загрузка аватара пользователя
1. Итог: Request changes
Есть блокирующие проблемы с безопасностью (отсутствует авторизация и IDOR, неконтролируемый размер/тип файла, доверие к originalname пользователя) и потенциальный краш на непустых кейсах (req.file/user не провалидированы). Мержить в таком виде нельзя.
2. Замечания
🔴 Блокирующие
router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
Отсутствует requireAuth. По конвенции команды все пользовательские эндпоинты должны идти через middleware авторизации — сейчас загрузить аватар может кто угодно, даже не залогиненный.
router.post('/users/:id/avatar', requireAuth, upload.single('file'), async (req, res) => {
Там же
Даже если авторизация будет добавлена, нет проверки, что req.params.id совпадает с id авторизованного пользователя (или что у него есть права на изменение чужого профиля). Это classic IDOR — любой залогиненный юзер сможет подменить чужой аватар.
if (req.user.id !== req.params.id /* && !req.user.isAdmin */) {
return res.status(403).json({ error: 'forbidden' })
}
const key = 'avatars/' + req.params.id + '/' + req.file.originalname
Два момента:
req.fileможет бытьundefined, если файл не передан (multer это допускает) — упадёт с TypeError и 500 вместо адекватного 400.originalname— это пользовательский ввод, его нельзя подставлять в путь напрямую: path traversal (../../), спецсимволы, коллизии имён. Нужно валидировать через zod (поле + метаданные) и генерировать собственное имя файла, расширение брать из mimetype, а не доверять клиенту.
if (!req.file) {
return res.status(400).json({ error: 'file is required' })
}
const ext = mimeToExt(req.file.mimetype) // своя мапа, не доверяем originalname
const key = `avatars/${req.params.id}/${randomUUID()}.${ext}`
const upload = multer({ storage: multer.memoryStorage() })
Нет limits (размер файла) и fileFilter (тип файла). Сейчас можно залить файл произвольного размера в память процесса (memoryStorage) — прямой путь к OOM/DoS, и файл произвольного типа (не изображение) выдать за аватар.
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp']
cb(null, allowed.includes(file.mimetype))
},
})
Плюс стоит явно обрабатывать ошибку multer (LIMIT_FILE_SIZE и т.п.) через error-middleware, иначе она уйдёт как необработанное исключение.
const user = await db.user.findUnique({ where: { id: req.params.id } })
Результат нигде не проверяется на null. Если юзера с таким id нет — упадём на user.id чуть ниже с 500, хотя должно быть 404. Плюс сейчас мы делаем upload в S3 до проверки существования юзера — тратим время/трафик впустую.
const user = await db.user.findUnique({ where: { id: req.params.id } })
if (!user) {
return res.status(404).json({ error: 'user not found' })
}
🟡 Важные
Отсутствует try/catch / обработка ошибок.
Весь хендлер — голый async без try/catch. Нужно посмотреть, есть ли глобальный error-handling middleware в приложении (не видно из диффа). Если нет — ошибки S3 (putObject) или БД улетят как unhandled rejection. Нужно обернуть и вернуть осмысленный статус:
try {
// ...
} catch (err) {
req.log?.error(err)
res.status(502).json({ error: 'failed to upload avatar' })
}
Несогласованность между S3 и БД.
Если putObject прошёл успешно, а db.user.update упал — в S3 остаётся «осиротевший» файл, а avatarKey в БД не обновлён. Нужна компенсирующая логика (удаление объекта из S3 при ошибке update) либо обратный порядок операций с сохранением ключа заранее.
Старый аватар не удаляется.
При повторной загрузке старый объект в S3 так и остаётся висеть — накопление мусора в сторадже и расходы на хранение. Стоит удалять user.avatarKey (если был) после успешного обновления.
Порядок объявления upload.
const upload = multer({ storage: multer.memoryStorage() })
объявлена через const после использования в router.post(...). Из-за temporal dead zone это упадёт с ReferenceError, если роут регистрируется в момент выполнения модуля (а не в колбэке). Нужно поднять объявление upload выше роута.
⚪ Мелкие
- Нет валидации
req.params.idчерез zod (ожидается по конвенции команды — например, проверка формата uuid), сейчас строка из URL летит напрямую в Prisma и в ключ S3. - Формирование
keyиurlстоит вынести в отдельные хелперы (buildAvatarKey,buildCdnUrl) — пригодится для переиспользования при удалении старого аватара. - Не помешало бы логирование успешной загрузки (userId, key, размер файла) для последующего дебага.
3. Каких тестов не хватает
- Happy path: авторизованный пользователь загружает свой аватар — 200, корректный
url, запись в БД обновлена. - Запрос без авторизации — 401.
- Попытка загрузить аватар другому пользователю (чужой
id) — 403. - Запрос без файла (
fileотсутствует) — 400, без падения в 500. - Файл превышает лимит размера — 400/413.
- Файл с недопустимым mimetype (например,
application/pdf) — 400. originalnameсодержит../или спецсимволы — ключ в S3 не должен позволять path traversal.- Юзер с указанным
idне существует — 404, без обращения к S3. s3.putObjectпадает с ошибкой — корректный ответ (5xx), запись в БД не создаётся/не обновляется.db.user.updateпадает после успешногоputObject— проверка компенсирующей логики (если будет реализована) или хотя бы корректного ответа клиенту.- Повторная загрузка аватара — старый объект в S3 удаляется (после реализации).
4. Что сделано хорошо
- Используется
multer.memoryStorage()— корректный выбор для последующей отправки буфера в S3, без лишней записи временных файлов на диск сервера. - Общая структура эндпоинта логична и укладывается в REST-соглашения (
POST /users/:id/avatar), легко читается и расширяется.
Отлично, давайте проведем ревью этого PR.
Итог: Request Changes
Запрос на изменение, так как есть несколько критических проблем, связанных с безопасностью, надежностью и обработкой ошибок, которые требуют немедленного исправления.
Замечания по приоритету:
Блокирующие:
- Строка:
const user = await db.user.findUnique({ where: { id: req.params.id } })
Что не так: Отсутствует проверка, что пользователь, для которого загружается аватар, действительно существует. Если пользователь не найден, user будет null, и последующая операция db.user.update вызовет ошибку, но до этого может произойти загрузка файла в S3.
Как исправить: Добавить проверку существования пользователя и возвращать 404, если он не найден.
``diff``
+ router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
+ const user = await db.user.findUnique({ where: { id: req.params.id } })
+ if (!user) {
+ return res.status(404).json({ message: 'User not found' });
+ }
+ // ...
- Строка:
const key = 'avatars/' + req.params.id + '/' + req.file.originalname
Что не так: Использование req.file.originalname напрямую в качестве имени файла в хранилище S3 может привести к ряду проблем:
- Коллизии: Если пользователь загрузит два файла с одинаковым именем, второй перезапишет первый.
- Безопасность: Имена файлов могут содержать специальные символы, пути обхода (
../), или быть очень длинными, что может вызвать проблемы в файловой системе или при отображении. - Кэширование: Если пользователь загрузит новый аватар с тем же именем, CDN может отдать старую версию из кэша.
Как исправить: Генерировать уникальное имя файла, например, с использованием UUID или комбинации ID пользователя и временной метки, сохраняя при этом оригинальное расширение.
``diff${req.params.id}-${Date.now()}.${fileExtension}
+ router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
+ // ...
+ if (!req.file) {
+ return res.status(400).json({ message: 'No file uploaded' });
+ }
+ const fileExtension = req.file.originalname.split('.').pop();
+ const uniqueFileName = ; // Или использовать UUIDavatars/${req.params.id}/${uniqueFileName}
+ const key = ;``
+ // ...
- Строка:
await s3.putObject({ Bucket: BUCKET, Key: key, Body: req.file.buffer })
Что не так: Отсутствует обработка ошибок при загрузке файла в S3. Если загрузка не удастся (например, проблемы с сетью, некорректные учетные данные S3), приложение упадет или продолжит выполнение, но запись в базу данных будет некорректной, так как файл фактически не загружен.
Как исправить: Обернуть операцию S3 в try-catch блок.
``diff``
+ router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
+ // ...
+ try {
+ await s3.putObject({ Bucket: BUCKET, Key: key, Body: req.file.buffer });
+ } catch (error) {
+ console.error('Error uploading file to S3:', error);
+ return res.status(500).json({ message: 'Failed to upload avatar' });
+ }
+ // ...
- Строка:
await db.user.update({ where: { id: user.id }, data: { avatarKey: key } })
Что не так: Отсутствует обработка ошибок при обновлении записи в базе данных. Если обновление не удастся, файл в S3 будет загружен, но в базе данных информация об аватаре не обновится, что приведет к рассинхронизации данных.
Как исправить: Обернуть операцию обновления БД в try-catch блок. В случае ошибки, возможно, потребуется рассмотреть стратегию компенсации (удаление файла из S3).
``diff``
+ router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
+ // ...
+ try {
+ await s3.putObject({ Bucket: BUCKET, Key: key, Body: req.file.buffer });
+ await db.user.update({ where: { id: user.id }, data: { avatarKey: key } });
+ res.json({ url: CDN + '/' + key });
+ } catch (error) {
+ console.error('Error processing avatar upload:', error);
+ // Если файл был загружен, но БД не обновилась, возможно, стоит его удалить из S3.
+ // Это требует более сложной логики и может быть реализовано в отдельной функции.
+ // Например: if (s3UploadSuccess) await s3.deleteObject({ Bucket: BUCKET, Key: key });
+ return res.status(500).json({ message: 'Failed to update user avatar' });
+ }
+ })
- Строка:
router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
Что не так: Отсутствует авторизация. Согласно правилам команды, должна использоваться middleware requireAuth. Это позволяет любому пользователю загрузить аватар для любого id пользователя.
Как исправить: Добавить middleware requireAuth. Также необходимо убедиться, что пользователь, загружающий аватар, имеет право изменять аватар для указанного id (например, это его собственный id или он является администратором).
``diff``
+ router.post('/users/:id/avatar', requireAuth, async (req, res) => {
+ // Дополнительная проверка: req.user.id === req.params.id или req.user.role === 'admin'
+ if (req.user.id !== req.params.id && req.user.role !== 'admin') {
+ return res.status(403).json({ message: 'Forbidden' });
+ }
+ // ...
- Строка:
router.post('/users/:id/avatar', upload.single('file'), async (req, res) => {
Что не так: Отсутствует валидация входных данных для req.params.id и для загружаемого файла. req.params.id должен быть валидным ID пользователя (например, UUID или число). Также необходимо валидировать сам файл: его наличие, размер, тип.
Как исправить: Использовать zod для валидации req.params.id и добавить middleware для валидации файла.
``diff``
+ // Предполагаем, что у нас есть middleware для zod валидации параметров
+ import { z } from 'zod';
+
+ const userIdSchema = z.object({
+ id: z.string().uuid(), // Или z.string().regex(/^[0-9a-fA-F]{24}$/) для MongoDB ObjectId
+ });
+
+ // Middleware для валидации параметров
+ const validateParams = (schema: z.ZodObject<any>) => (req: Request, res: Response, next: NextFunction) => {
+ try {
+ schema.parse(req.params);
+ next();
+ } catch (error) {
+ res.status(400).json({ message: 'Invalid request parameters', errors: error.errors });
+ }
+ };
+
+ router.post('/users/:id/avatar', requireAuth, validateParams(userIdSchema), upload.single('file'), async (req, res) => {
+ if (!req.file) {
+ return res.status(400).json({ message: 'No file uploaded' });
+ }
+ // Дополнительная валидация файла:
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+ const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
+
+ if (req.file.size > MAX_FILE_SIZE) {
+ return res.status(400).json({ message: 'File too large' });
+ }
+ if (!ALLOWED_MIME_TYPES.includes(req.file.mimetype)) {
+ return res.status(400).json({ message: 'Invalid file type. Only JPEG, PNG, WEBP are allowed.' });
+ }
+ // ...
Важные:
- Строка:
const upload = multer({ storage: multer.memoryStorage() })
Что не так: multer инициализируется внутри файла, где определен роут. Это не критично, но обычно multer конфигурируется один раз в отдельном файле или в файле конфигурации, чтобы его можно было переиспользовать и централизованно управлять настройками (например, лимитами размера файла, фильтрацией типов).
Как исправить: Перенести инициализацию multer в отдельный файл или в глобальную конфигурацию, а затем импортировать его.
```typescript
// src/config/multer.ts
import multer from 'multer';
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
fileFilter: (req, file, cb) => {
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, WEBP are allowed.'));
}
},
});
export default upload;
// В файле роута:
// import upload from '../config/multer';
// router.post('/users/:id/avatar', requireAuth, validateParams(userIdSchema), upload.single('file'), async (req, res) => {
// // ...
// })
``fileFilter
Примечание: Если используется в multer, то ручная проверка req.file.mimetype и req.file.size в контроллере становится избыточной, но обработка ошибок от multer (например, MulterError`) все равно нужна.
- Строка:
res.json({ url: CDN + '/' + key })
Что не так: URL аватара формируется путем конкатенации CDN + '/' + key. Это может быть не всегда корректно, если CDN уже содержит слэш в конце или key начинается со слэша. Лучше использовать path.join или URL-конструктор для надежного формирования URL.
Как исправить: Использовать URL API или path.join (если это для путей, а не URL).
``diff`
+ import { URL } from 'url';
+ // ...
+ const avatarUrl = new URL(key, CDN).toString(); // Или new URL(key, new URL(CDN)).toString();
+ res.json({ url: avatarUrl });
CDN
Или просто убедиться, что не заканчивается на / и key не начинается с /`.
Мелкие:
- Строка:
const user = await db.user.findUnique({ where: { id: req.params.id } })
Что не так: Переменная user не используется после проверки user.id в db.user.update. Можно использовать req.params.id напрямую, что делает код немного чище.
Как исправить: Использовать req.params.id напрямую.
``diff``
+ const user = await db.user.findUnique({ where: { id: req.params.id } })
+ if (!user) {
+ return res.status(404).json({ message: 'User not found' });
+ }
+ // ...
+ await db.user.update({ where: { id: req.params.id }, data: { avatarKey: key } })
Каких тестов не хватает:
- Успешная загрузка аватара:
- Загрузка файла корректного типа и размера.
- Проверка, что файл появился в S3 с ожидаемым уникальным именем.
- Проверка, что
avatarKeyобновился в базе данных для соответствующего пользователя. - Проверка, что ответ содержит корректный URL.
- Обработка ошибок и граничных случаев:
- Попытка загрузки аватара для несуществующего пользователя (ожидается 404).
- Попытка загрузки без авторизации (ожидается 401/403).
- Попытка загрузки аватара для другого пользователя без прав администратора (ожидается 403).
- Загрузка файла слишком большого размера (ожидается 400).
- Загрузка файла неподдерживаемого типа (ожидается 400).
- Попытка загрузки без файла (
req.fileотсутствует) (ожидается 400). - Имитация сбоя S3 (например, через мокирование
s3.putObject) и проверка, что возвращается 500 и база данных не обновляется. - Имитация сбоя обновления базы данных (через мокирование
db.user.update) и проверка, что возвращается 500 (и, возможно, файл удаляется из S3). - Загрузка файла с именем, содержащим специальные символы или длинным именем.
- Валидация параметров:
- Запрос с некорректным
idпользователя в URL (например, не UUID) (ожидается 400).
Что в PR сделано хорошо:
- Использование
multer.memoryStorage(): Это хорошее решение для загрузки файлов небольшого размера, так как позволяет избежать записи на диск и сразу получить доступ к буферу файла, что удобно для прямой загрузки в S3. - Обновление
avatarKeyв базе данных: Сохранение ключа файла в S3 в базе данных пользователя является правильным подходом для связывания пользователя с его аватаром и легкого получения URL.
Советы
- Давайте модели описание задачи PR и правила команды: без них ревью скатывается в замечания по стилю.
- Блокирующие замечания модели перепроверьте сами: иногда она принимает за баг то, что обрабатывается в коде вне diff.
- Скопируйте промпт кнопкой выше.
- Замените поля в фигурных скобках своими данными.
- Отправьте в нейросеть и сравните ответ с примером на этой странице.
Подробнее о структуре хорошего запроса: гид AI University.
Похожие промпты
Все 435 промптов и 6 наборов
172 промптов открыты бесплатно. Остальные и наборы-цепочки открывает доступ к библиотеке за 1 490 ₽. Полный доступ за 4 900 ₽: все курсы AI University на русском и библиотека промптов. Разовый платёж, новые промпты входят.