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.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) 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), sourceSharer, 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