refactor: migrate to Tencent Cloud backend
这个提交包含在:
@@ -0,0 +1,52 @@
|
||||
const crypto = require('crypto')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const config = require('../config')
|
||||
const { one } = require('./db')
|
||||
|
||||
function hashPassword(password, salt) {
|
||||
return crypto.createHash('sha256').update(String(password) + ':' + salt).digest('hex')
|
||||
}
|
||||
|
||||
function randomHex(bytes) {
|
||||
return crypto.randomBytes(bytes).toString('hex')
|
||||
}
|
||||
|
||||
function signUser(user) {
|
||||
return jwt.sign({ type: 'user', user_id: user.user_id, openid: user.openid }, config.jwt.secret, { expiresIn: config.jwt.expiresIn })
|
||||
}
|
||||
|
||||
function signAdmin(admin) {
|
||||
return jwt.sign({ type: 'admin', admin_id: admin.admin_id, username: admin.username, role: admin.role }, config.jwt.adminSecret, { expiresIn: config.jwt.expiresIn })
|
||||
}
|
||||
|
||||
function readBearer(headers) {
|
||||
const auth = headers.authorization || headers.Authorization || ''
|
||||
const match = auth.match(/^Bearer\s+(.+)$/i)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
async function requireUser(ctx) {
|
||||
const token = readBearer(ctx.headers)
|
||||
if (!token) return null
|
||||
try {
|
||||
const payload = jwt.verify(token, config.jwt.secret)
|
||||
if (payload.type !== 'user') return null
|
||||
return await one('SELECT * FROM users WHERE user_id = :user_id AND status = 1', { user_id: payload.user_id })
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function requireAdmin(ctx) {
|
||||
const token = readBearer(ctx.headers) || (ctx.body && ctx.body.token)
|
||||
if (!token) return null
|
||||
try {
|
||||
const payload = jwt.verify(token, config.jwt.adminSecret)
|
||||
if (payload.type !== 'admin') return null
|
||||
return await one('SELECT * FROM admin_accounts WHERE admin_id = :admin_id AND status = 1', { admin_id: payload.admin_id })
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { hashPassword, randomHex, signUser, signAdmin, requireUser, requireAdmin }
|
||||
@@ -0,0 +1,38 @@
|
||||
const COS = require('cos-nodejs-sdk-v5')
|
||||
const config = require('../config')
|
||||
|
||||
let client
|
||||
|
||||
function getClient() {
|
||||
if (!client) {
|
||||
client = new COS({
|
||||
SecretId: config.cos.secretId,
|
||||
SecretKey: config.cos.secretKey
|
||||
})
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
function getObjectUrl(key, expiresSeconds) {
|
||||
return getClient().getObjectUrl({
|
||||
Bucket: config.cos.bucket,
|
||||
Region: config.cos.region,
|
||||
Key: key,
|
||||
Sign: true,
|
||||
Expires: expiresSeconds || 600
|
||||
})
|
||||
}
|
||||
|
||||
function getPutObjectUrl(key, contentType, expiresSeconds) {
|
||||
return getClient().getObjectUrl({
|
||||
Bucket: config.cos.bucket,
|
||||
Region: config.cos.region,
|
||||
Key: key,
|
||||
Method: 'PUT',
|
||||
Sign: true,
|
||||
Expires: expiresSeconds || 600,
|
||||
Headers: contentType ? { 'Content-Type': contentType } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { getClient, getObjectUrl, getPutObjectUrl }
|
||||
@@ -0,0 +1,48 @@
|
||||
const mysql = require('mysql2/promise')
|
||||
const config = require('../config')
|
||||
|
||||
let pool
|
||||
|
||||
function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool({
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
user: config.db.user,
|
||||
password: config.db.password,
|
||||
database: config.db.database,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 5,
|
||||
namedPlaceholders: true,
|
||||
timezone: '+08:00'
|
||||
})
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
async function query(sql, params) {
|
||||
const [rows] = await getPool().execute(sql, params || {})
|
||||
return rows
|
||||
}
|
||||
|
||||
async function one(sql, params) {
|
||||
const rows = await query(sql, params)
|
||||
return rows[0] || null
|
||||
}
|
||||
|
||||
async function transaction(work) {
|
||||
const conn = await getPool().getConnection()
|
||||
try {
|
||||
await conn.beginTransaction()
|
||||
const result = await work(conn)
|
||||
await conn.commit()
|
||||
return result
|
||||
} catch (err) {
|
||||
await conn.rollback()
|
||||
throw err
|
||||
} finally {
|
||||
conn.release()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getPool, query, one, transaction }
|
||||
@@ -0,0 +1,16 @@
|
||||
const { query } = require('./db')
|
||||
|
||||
async function writeLog(options) {
|
||||
await query(
|
||||
'INSERT INTO operation_logs (user_id, admin_id, action, detail, ip) VALUES (:user_id, :admin_id, :action, :detail, :ip)',
|
||||
{
|
||||
user_id: options.user_id || null,
|
||||
admin_id: options.admin_id || null,
|
||||
action: options.action,
|
||||
detail: options.detail || '',
|
||||
ip: options.ip || ''
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = { writeLog }
|
||||
@@ -0,0 +1,45 @@
|
||||
function normalizeHeaders(headers) {
|
||||
const result = {}
|
||||
Object.keys(headers || {}).forEach(key => {
|
||||
result[key] = headers[key]
|
||||
result[key.toLowerCase()] = headers[key]
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function parseBody(event) {
|
||||
if (!event.body) return {}
|
||||
if (typeof event.body === 'object') return event.body
|
||||
const raw = event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString('utf8') : event.body
|
||||
if (!raw) return {}
|
||||
try { return JSON.parse(raw) } catch (err) { return {} }
|
||||
}
|
||||
|
||||
function parseQuery(event) {
|
||||
if (event.queryStringParameters) return event.queryStringParameters || {}
|
||||
if (event.query) return event.query || {}
|
||||
return {}
|
||||
}
|
||||
|
||||
function getPath(event) {
|
||||
return event.path || event.Path || event.requestContext && event.requestContext.path || '/'
|
||||
}
|
||||
|
||||
function getMethod(event) {
|
||||
return String(event.httpMethod || event.method || event.requestContext && event.requestContext.httpMethod || 'GET').toUpperCase()
|
||||
}
|
||||
|
||||
function createContext(event) {
|
||||
return {
|
||||
event,
|
||||
method: getMethod(event),
|
||||
path: getPath(event),
|
||||
headers: normalizeHeaders(event.headers),
|
||||
query: parseQuery(event),
|
||||
body: parseBody(event),
|
||||
params: {},
|
||||
ip: event.requestContext && event.requestContext.sourceIp || ''
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createContext }
|
||||
@@ -0,0 +1,23 @@
|
||||
function ok(data) {
|
||||
return { code: 0, message: 'success', data: data || {} }
|
||||
}
|
||||
|
||||
function fail(code, message, data) {
|
||||
return { code, message, data: data || {} }
|
||||
}
|
||||
|
||||
function http(statusCode, body, headers) {
|
||||
return {
|
||||
isBase64Encoded: false,
|
||||
statusCode,
|
||||
headers: Object.assign({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Device-Id, X-App-Version, X-Platform',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
}, headers || {}),
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ok, fail, http }
|
||||
@@ -0,0 +1,33 @@
|
||||
class Router {
|
||||
constructor() {
|
||||
this.routes = []
|
||||
}
|
||||
|
||||
add(method, pattern, handler) {
|
||||
const keys = []
|
||||
const regex = new RegExp('^' + pattern.replace(/\/:(\w+)/g, function (_, key) {
|
||||
keys.push(key)
|
||||
return '/([^/]+)'
|
||||
}) + '$')
|
||||
this.routes.push({ method, regex, keys, handler })
|
||||
}
|
||||
|
||||
get(pattern, handler) { this.add('GET', pattern, handler) }
|
||||
post(pattern, handler) { this.add('POST', pattern, handler) }
|
||||
put(pattern, handler) { this.add('PUT', pattern, handler) }
|
||||
delete(pattern, handler) { this.add('DELETE', pattern, handler) }
|
||||
|
||||
match(method, path) {
|
||||
for (const route of this.routes) {
|
||||
if (route.method !== method) continue
|
||||
const match = path.match(route.regex)
|
||||
if (!match) continue
|
||||
const params = {}
|
||||
route.keys.forEach((key, index) => { params[key] = decodeURIComponent(match[index + 1]) })
|
||||
return { handler: route.handler, params }
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Router
|
||||
@@ -0,0 +1,76 @@
|
||||
const https = require('https')
|
||||
const config = require('../config')
|
||||
|
||||
function requestJson(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(url, res => {
|
||||
let raw = ''
|
||||
res.on('data', chunk => { raw += chunk })
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(raw)) } catch (err) { reject(err) }
|
||||
})
|
||||
}).on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
function postJson(url, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = JSON.stringify(body || {})
|
||||
const req = https.request(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
}
|
||||
}, res => {
|
||||
let raw = ''
|
||||
res.on('data', chunk => { raw += chunk })
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(raw)) } catch (err) { reject(err) }
|
||||
})
|
||||
})
|
||||
req.on('error', reject)
|
||||
req.write(data)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
let accessTokenCache = null
|
||||
|
||||
async function getAccessToken() {
|
||||
if (accessTokenCache && accessTokenCache.expiresAt > Date.now() + 60000) return accessTokenCache.token
|
||||
if (!config.wechat.appid || !config.wechat.secret) throw new Error('WECHAT_APPID or WECHAT_SECRET is not configured')
|
||||
const url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + encodeURIComponent(config.wechat.appid) + '&secret=' + encodeURIComponent(config.wechat.secret)
|
||||
const data = await requestJson(url)
|
||||
if (!data.access_token) throw new Error(data.errmsg || 'wechat access_token failed')
|
||||
accessTokenCache = {
|
||||
token: data.access_token,
|
||||
expiresAt: Date.now() + (Number(data.expires_in || 7200) * 1000)
|
||||
}
|
||||
return accessTokenCache.token
|
||||
}
|
||||
|
||||
async function code2Session(code) {
|
||||
if (config.nodeEnv === 'development' && (!code || code === 'local' || String(code).indexOf('dev_') === 0)) {
|
||||
return { openid: 'dev_openid_' + String(code || 'local').slice(-8) }
|
||||
}
|
||||
if (!config.wechat.appid || !config.wechat.secret) {
|
||||
if (config.nodeEnv === 'development') return { openid: 'dev_openid_' + String(code || 'local').slice(-8) }
|
||||
throw new Error('WECHAT_APPID or WECHAT_SECRET is not configured')
|
||||
}
|
||||
const url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' + encodeURIComponent(config.wechat.appid) + '&secret=' + encodeURIComponent(config.wechat.secret) + '&js_code=' + encodeURIComponent(code) + '&grant_type=authorization_code'
|
||||
const data = await requestJson(url)
|
||||
if (!data.openid) throw new Error(data.errmsg || 'wechat login failed')
|
||||
return data
|
||||
}
|
||||
|
||||
async function getPhoneNumber(code) {
|
||||
if (!code) throw new Error('phone code required')
|
||||
const token = await getAccessToken()
|
||||
const url = 'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=' + encodeURIComponent(token)
|
||||
const data = await postJson(url, { code })
|
||||
if (data.errcode) throw new Error(data.errmsg || 'get phone number failed')
|
||||
return data.phone_info || null
|
||||
}
|
||||
|
||||
module.exports = { code2Session, getPhoneNumber }
|
||||
在新工单中引用
屏蔽一个用户