文件
jw-beauty/server/src/routes/user.js
T
Guoguo 0c65ef95f8 fix: cross-audit fixes — file validation, type safety, dedup
- Avatar upload: whitelist image MIME types and extensions (jpg/png/gif/webp)
- Normalize device_id to String for strict comparison in treatment sync
- Add ORDER BY expire_time DESC to purchase/adminCreate subscription queries
- Deduplicate readBearer: middleware imports from lib/auth.js
- Parameterize createTrial INTERVAL instead of string concatenation
- Add rate limiting (20/15min) to avatar upload and phone auth endpoints
2026-05-11 06:22:24 -07:00

77 行
3.3 KiB
JavaScript

const router = require('express').Router()
const multer = require('multer')
const COS = require('cos-nodejs-sdk-v5')
const { ok, fail } = require('../lib/response')
const { requireUser } = require('../middleware/auth')
const { getPhoneNumber } = require('../lib/wechat')
const config = require('../config')
const userDao = require('../dao/user.dao')
const deviceDao = require('../dao/device.dao')
const logDao = require('../dao/log.dao')
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 2 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
cb(null, ALLOWED_IMAGE_TYPES.includes(file.mimetype))
}
})
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
router.get('/user/profile', requireUser, wrap(async (req, res) => {
const user = req.user
const devices = await deviceDao.listByUser(user.user_id)
res.json(ok({
user_id: String(user.user_id),
nickname: user.nickname || '用户' + String(user.user_id),
avatar: user.avatar || '',
phone: user.phone || '',
gender: user.gender || 0,
bind_time: null,
device_count: devices.length
}))
}))
router.put('/user/profile', requireUser, wrap(async (req, res) => {
await userDao.updateProfile(req.user.user_id, {
nickname: req.body.nickname || null,
avatar: req.body.avatar || req.body.avatar_url || null,
gender: req.body.gender === undefined ? null : req.body.gender
})
await logDao.write({ user_id: req.user.user_id, action: 'user_update', detail: '更新用户资料', ip: req.ip })
res.json(ok({ message: 'success' }))
}))
router.post('/user/avatar', requireUser, upload.single('file'), wrap(async (req, res) => {
if (!req.file) return res.json(fail(2001, 'file required, only jpg/png/gif/webp allowed'))
const ALLOWED_EXTS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
const ext = (req.file.originalname || '').split('.').pop().toLowerCase().replace(/[^a-z]/g, '') || 'jpg'
if (!ALLOWED_EXTS.includes(ext)) return res.json(fail(2001, 'unsupported image format'))
const key = 'avatars/' + req.user.user_id + '_' + Date.now() + '.' + ext
const cos = new COS({ SecretId: config.cos.secretId, SecretKey: config.cos.secretKey })
await new Promise((resolve, reject) => {
cos.putObject({
Bucket: config.cos.bucket,
Region: config.cos.region,
Key: key,
Body: req.file.buffer,
ContentType: req.file.mimetype
}, (err, data) => err ? reject(err) : resolve(data))
})
const avatarUrl = 'https://' + (config.cos.cdnDomain || 'tx.vsai.net.cn') + '/' + key
res.json(ok({ avatar: avatarUrl }))
}))
router.post('/user/phone', requireUser, wrap(async (req, res) => {
const code = String(req.body.code || '').trim()
if (!code) return res.json(fail(2001, 'phone code required'))
const phoneInfo = await getPhoneNumber(code)
if (!phoneInfo || !phoneInfo.phoneNumber) return res.json(fail(2001, 'phone authorization failed'))
await userDao.updatePhone(req.user.user_id, phoneInfo.phoneNumber)
await logDao.write({ user_id: req.user.user_id, action: 'user_phone_bind', detail: '授权手机号', ip: req.ip })
res.json(ok({ phone: phoneInfo.phoneNumber, pure_phone_number: phoneInfo.purePhoneNumber || '', country_code: phoneInfo.countryCode || '' }))
}))
module.exports = router