Wire real WeChat app config, prefer activity cover over posters, and add admin cover cropping for the home banner. Co-authored-by: Cursor <cursoragent@cursor.com>
239 lines
7.9 KiB
JavaScript
239 lines
7.9 KiB
JavaScript
const express = require('express')
|
|
const db = require('../lib/db')
|
|
const wechat = require('../lib/wechat')
|
|
const { uploader } = require('../lib/upload')
|
|
const { userAuth, requireUserAdmin } = require('../lib/auth')
|
|
const { HttpError, uid, token, ok, fail, wrap, str, has, PHONE_RE } = require('../lib/http')
|
|
const {
|
|
publicActivity,
|
|
currentActivity,
|
|
pastActivities,
|
|
nextPastTheme,
|
|
setPinned,
|
|
publicProfile,
|
|
DEFAULT_BRAND
|
|
} = require('../lib/serialize')
|
|
|
|
const router = express.Router()
|
|
|
|
router.post(
|
|
'/login',
|
|
wrap(async (req, res) => {
|
|
const body = req.body || {}
|
|
const code = str(body.code)
|
|
if (!code) throw new HttpError(400, '缺少 code')
|
|
const session = await wechat.code2session(code, str(body.deviceId))
|
|
|
|
const profile = db.update((data) => {
|
|
let user = data.users.find((item) => item.openid === session.openid)
|
|
if (!user) {
|
|
user = {
|
|
openid: session.openid,
|
|
nickName: '',
|
|
avatarUrl: '',
|
|
name: '',
|
|
phone: '',
|
|
company: '',
|
|
position: '',
|
|
createdAt: new Date().toISOString()
|
|
}
|
|
data.users.push(user)
|
|
}
|
|
user.token = token()
|
|
return Object.assign({ token: user.token, demo: session.demo }, publicProfile(user, data))
|
|
})
|
|
ok(res, profile)
|
|
})
|
|
)
|
|
|
|
router.get('/user/profile', userAuth, (req, res) => {
|
|
ok(res, publicProfile(req.user))
|
|
})
|
|
|
|
const PROFILE_LIMITS = { nickName: 32, name: 20, phone: 11, company: 40, position: 40 }
|
|
|
|
router.post('/user/profile', userAuth, (req, res) => {
|
|
const body = req.body || {}
|
|
if (has(body, 'phone') && str(body.phone) && !PHONE_RE.test(str(body.phone))) {
|
|
return fail(res, 400, '请填写正确的手机号')
|
|
}
|
|
const profile = db.update((data) => {
|
|
const user = req.user
|
|
Object.keys(PROFILE_LIMITS).forEach((key) => {
|
|
if (has(body, key)) user[key] = str(body[key], PROFILE_LIMITS[key])
|
|
})
|
|
// Only server-hosted avatars are accepted; temp paths like http://tmp/... are useless to others.
|
|
const avatarUrl = str(body.avatarUrl)
|
|
if (avatarUrl && avatarUrl.startsWith('/static/uploads/')) user.avatarUrl = avatarUrl
|
|
return publicProfile(user, data)
|
|
})
|
|
ok(res, profile)
|
|
})
|
|
|
|
router.post('/user/avatar', userAuth, uploader({ imagesOnly: true }))
|
|
|
|
router.get('/activity/current', (req, res) => {
|
|
const data = db.read()
|
|
const activity = currentActivity(data)
|
|
ok(res, activity ? publicActivity(activity, data.posters) : null)
|
|
})
|
|
|
|
router.get('/activity/past', (req, res) => {
|
|
const data = db.read()
|
|
ok(res, pastActivities(data).map((item) => publicActivity(item, data.posters)))
|
|
})
|
|
|
|
router.get('/activity/:id', (req, res) => {
|
|
const data = db.read()
|
|
const activity = data.activities.find((item) => item.id === req.params.id && item.status !== 'draft')
|
|
if (!activity) return fail(res, 404, '活动不存在')
|
|
ok(res, publicActivity(activity, data.posters))
|
|
})
|
|
|
|
router.post('/activity/past', userAuth, requireUserAdmin, (req, res) => {
|
|
const body = req.body || {}
|
|
const title = str(body.title, 40)
|
|
const startAt = str(body.startAt, 32)
|
|
if (!title || !startAt) return fail(res, 400, '请填写名称和时间')
|
|
const activity = db.update((data) => {
|
|
const now = new Date().toISOString()
|
|
const next = {
|
|
id: uid('p'),
|
|
title,
|
|
brandName: DEFAULT_BRAND,
|
|
brandDesc: '',
|
|
cover: str(body.cover),
|
|
video: str(body.video),
|
|
startAt,
|
|
address: '',
|
|
latitude: null,
|
|
longitude: null,
|
|
status: 'past',
|
|
isCurrent: false,
|
|
pinned: false,
|
|
pinnedAt: '',
|
|
theme: nextPastTheme(data),
|
|
createdAt: now,
|
|
updatedAt: now
|
|
}
|
|
if (body.pinned) setPinned(next, true)
|
|
data.activities.push(next)
|
|
return next
|
|
})
|
|
ok(res, publicActivity(activity))
|
|
})
|
|
|
|
function findPast(data, id) {
|
|
return data.activities.find((item) => item.id === id && item.status === 'past')
|
|
}
|
|
|
|
router.post('/activity/past/:id/pin', userAuth, requireUserAdmin, (req, res) => {
|
|
const updated = db.update((data) => {
|
|
const activity = findPast(data, req.params.id)
|
|
return activity ? setPinned(activity, !!(req.body && req.body.pinned)) : null
|
|
})
|
|
if (!updated) return fail(res, 404, '往期活动不存在')
|
|
ok(res, publicActivity(updated))
|
|
})
|
|
|
|
router.delete('/activity/past/:id', userAuth, requireUserAdmin, (req, res) => {
|
|
const removed = db.update((data) => {
|
|
const activity = findPast(data, req.params.id)
|
|
if (!activity) return null
|
|
data.activities = data.activities.filter((item) => item.id !== activity.id)
|
|
data.posters = data.posters.filter((item) => item.activityId !== activity.id)
|
|
return activity.id
|
|
})
|
|
if (!removed) return fail(res, 404, '往期活动不存在')
|
|
ok(res, { id: removed })
|
|
})
|
|
|
|
router.get('/subscribe/config', (req, res) => {
|
|
const settings = db.read().settings || {}
|
|
const tmplId = str(settings.subscribeTmplId)
|
|
ok(res, {
|
|
enabled: !!tmplId,
|
|
tmplIds: tmplId ? [tmplId] : [],
|
|
notice: '提交即视为同意接收活动提醒'
|
|
})
|
|
})
|
|
|
|
router.post('/signup', userAuth, (req, res) => {
|
|
const body = req.body || {}
|
|
const activityId = str(body.activityId)
|
|
const name = str(body.name, 20)
|
|
const phone = str(body.phone)
|
|
let sourceSharer = str(body.sourceSharer, 64)
|
|
// accept / reject / ban / filter / none — matches wx.requestSubscribeMessage result values
|
|
const remindStatus = str(body.remindStatus || 'none', 16) || 'none'
|
|
if (!activityId || !name || !phone) return fail(res, 400, '请填写姓名和手机号')
|
|
if (!PHONE_RE.test(phone)) return fail(res, 400, '请填写正确的手机号')
|
|
if (sourceSharer === req.user.openid) sourceSharer = ''
|
|
|
|
const result = db.update((data) => {
|
|
const activity = data.activities.find((item) => item.id === activityId)
|
|
if (!activity || activity.status === 'draft') return { error: '活动不存在' }
|
|
if (activity.status === 'past') return { error: '这场活动已结束' }
|
|
const exists = data.signups.some((item) => item.activityId === activityId && item.openid === req.user.openid)
|
|
if (exists) return { error: '你已经报名过了' }
|
|
const signup = {
|
|
id: uid('s'),
|
|
activityId,
|
|
activityTitle: activity.title,
|
|
openid: req.user.openid,
|
|
name,
|
|
phone,
|
|
company: str(body.company, 40),
|
|
position: str(body.position, 40),
|
|
sourceSharer,
|
|
remindStatus,
|
|
remindAccepted: remindStatus === 'accept',
|
|
createdAt: new Date().toISOString(),
|
|
checkedIn: false,
|
|
checkedInAt: ''
|
|
}
|
|
data.signups.push(signup)
|
|
return { signup }
|
|
})
|
|
if (result.error) return fail(res, 400, result.error)
|
|
ok(res, result.signup)
|
|
})
|
|
|
|
router.get('/signup/mine', userAuth, (req, res) => {
|
|
const signups = db
|
|
.read()
|
|
.signups.filter((item) => item.openid === req.user.openid)
|
|
.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
|
|
ok(res, { signups, checkins: signups.filter((item) => item.checkedIn) })
|
|
})
|
|
|
|
router.post('/checkin/scan', userAuth, (req, res) => {
|
|
const body = req.body || {}
|
|
const result = db.update((data) => {
|
|
let activityId = ''
|
|
if (body.simulate) {
|
|
const activity = currentActivity(data)
|
|
if (!activity) return { error: '当前没有进行中的活动' }
|
|
activityId = activity.id
|
|
} else {
|
|
const code = data.checkinCodes.find((item) => item.token === str(body.token))
|
|
if (!code) return { error: '签到码无效' }
|
|
if (code.expiresAt && Date.parse(code.expiresAt) < Date.now()) return { error: '签到码已过期' }
|
|
activityId = code.activityId
|
|
}
|
|
const signup = data.signups.find((item) => item.activityId === activityId && item.openid === req.user.openid)
|
|
if (!signup) return { error: '请先报名再签到' }
|
|
if (!signup.checkedIn) {
|
|
signup.checkedIn = true
|
|
signup.checkedInAt = new Date().toISOString()
|
|
}
|
|
return { signup }
|
|
})
|
|
if (result.error) return fail(res, 400, result.error)
|
|
ok(res, result.signup)
|
|
})
|
|
|
|
router.post('/upload', userAuth, requireUserAdmin, uploader())
|
|
|
|
module.exports = router
|