diff --git a/app.js b/app.js index ed27c56..5c3abe7 100644 --- a/app.js +++ b/app.js @@ -1,36 +1,39 @@ -const express = require("express"); -const path = require("path"); -const cookieParser = require("cookie-parser"); -const logger = require("morgan"); -const http = require("http"); -require("dotenv").config(); +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const logger = require('morgan'); +const http = require('http'); +const config = require('./config'); -const indexRouter = require("./routers/index"); -const messageRouter = require("./routers/message"); -const requestToken = require("./utils/wechat").requestToken; +const indexRouter = require('./routers/index'); +const messageRouter = require('./routers/message'); +const { refreshToken } = require('./common/wechat'); +const { initializeTokenStore } = require('./common/token'); const app = express(); -requestToken(app); -setInterval(() => requestToken(app), 100 * 60 * 1000); +setTimeout(async () => { + // TODO: Here we need an improvement! I have tried EventEmitter but it's not working. :( + await initializeTokenStore(); + await refreshToken(app); + setInterval(async () => refreshToken(), 100 * 60 * 1000); +}, 1000); -const port = parseInt(process.env.PORT || "3000"); -app.set("port", port); -app.set("views", path.join(__dirname, "views")); -app.set("view engine", "ejs"); -app.set("trust proxy", true); +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); +app.set('trust proxy', true); -app.use(logger("dev")); +app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); -app.use(express.static(path.join(__dirname, "public"))); +app.use(express.static(path.join(__dirname, 'public'))); -app.use("/", indexRouter); -app.use("/message", messageRouter); +app.use('/', indexRouter); +app.use('/message', messageRouter); const server = http.createServer(app); -server.listen(port); +server.listen(config.port); module.exports = app; diff --git a/common/database.js b/common/database.js new file mode 100644 index 0000000..9da0d6f --- /dev/null +++ b/common/database.js @@ -0,0 +1,9 @@ +const { Sequelize } = require('sequelize'); + +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: 'data.db', + logging: false, +}); + +module.exports = sequelize; diff --git a/common/message.js b/common/message.js new file mode 100644 index 0000000..0314cec --- /dev/null +++ b/common/message.js @@ -0,0 +1,24 @@ +const { pushWechatMessage } = require('./wechat'); + +module.exports = { + async processMessage(userPrefix, message) { + let result = { + success: false, + message: `unsupported message type ${message.type}`, + }; + switch (message.type) { + case '0': // WeChat message + result = await pushWechatMessage(userPrefix, message); + break; + case '1': // Email message + // TODO: Email message + break; + case '2': // HTTP GET request + // TODO: HTTP GET request + break; + default: + break; + } + return result; + }, +}; diff --git a/common/token.js b/common/token.js new file mode 100644 index 0000000..dbb819f --- /dev/null +++ b/common/token.js @@ -0,0 +1,25 @@ +const { User } = require('../models'); + +const tokenStore = new Map(); + +async function initializeTokenStore() { + const users = await User.findAll(); + users.forEach((user) => { + if (user.wechatAppId) { + tokenStore.set(user.prefix, { + appId: user.wechatAppId, + appSecret: user.wechatAppSecret, + templateId: user.wechatTemplateId, + openId: user.wechatOpenId, + token: '', + }); + } + }); + console.debug(tokenStore); + console.log('Token store initialized.'); +} + +module.exports = { + initializeTokenStore, + tokenStore, +}; diff --git a/common/utils.js b/common/utils.js new file mode 100644 index 0000000..a3f206f --- /dev/null +++ b/common/utils.js @@ -0,0 +1,33 @@ +function formatTime(format) { + if (format === undefined) format = 'yyyy-MM-dd hh:mm:ss'; + const date = new Date(); + const o = { + 'M+': date.getMonth() + 1, + 'd+': date.getDate(), + 'h+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds(), + S: date.getMilliseconds(), + }; + + if (/(y+)/.test(format)) { + format = format.replace( + RegExp.$1, + (date.getFullYear() + '').substr(4 - RegExp.$1.length) + ); + } + + for (let k in o) { + if (new RegExp('(' + k + ')').test(format)) { + format = format.replace( + RegExp.$1, + RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length) + ); + } + } + return format; +} + +module.exports = { + formatTime, +}; diff --git a/common/wechat.js b/common/wechat.js new file mode 100644 index 0000000..fc8238c --- /dev/null +++ b/common/wechat.js @@ -0,0 +1,68 @@ +const axios = require('axios'); +const { tokenStore } = require('./token'); +const config = require('../config'); + +async function refreshToken() { + for (const item of tokenStore) { + item.token = await this.requestToken(); + } + console.log('Token refreshed.'); +} + +async function requestToken(appId, appSecret) { + let token = ''; + try { + let res = await axios.get( + `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}` + ); + console.log(res); + if (res && res.data && res.data.access_token) { + token = res.data.access_token; + } + } catch (e) { + console.error(e); + } + return token; +} + +async function pushWeChatMessage(userPrefix, message) { + // Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl + let user = tokenStore.get(userPrefix); + let access_token = user.token; + let request_data = { + touser: user.wechatOpenId, + template_id: user.wechatTemplateId, + }; + if (message.content) { + // TODO + // Generate html, save message to database and then return the id + let id = 'TODO'; + request_data.url = `${config.href}${userPrefix}/${id}`; + } + request_data.data = { text: { value: message.description } }; + let requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`; + try { + let response = await axios.post(requestUrl, request_data); + if (response && response.data && response.data.errcode === '40001') { + await requestToken(user.wechatAppId, user.wechatAppSecret); + await axios.post(requestUrl, request_data); + } + console.log(response.data); + return { + success: true, + message: '', + }; + } catch (e) { + console.error(e); + return { + success: false, + message: e.message, + }; + } +} + +module.exports = { + refreshToken, + requestToken, + pushWeChatMessage, +}; diff --git a/config.js b/config.js new file mode 100644 index 0000000..f771749 --- /dev/null +++ b/config.js @@ -0,0 +1,7 @@ +const config = { + port: process.env.PORT || 3000, + database: 'data.db', + href: 'https://github.com/', +}; + +module.exports = config; diff --git a/knexfile.js b/knexfile.js deleted file mode 100644 index 04a01eb..0000000 --- a/knexfile.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - development: { - client: "sqlite3", - connection: { - filename: "./data.db", - }, - }, - - staging: { - client: "sqlite3", - connection: { - filename: "./data.db", - }, - }, - - production: { - client: "sqlite3", - connection: { - filename: "./data.db", - }, - }, -}; diff --git a/middlewares/api_auth.js b/middlewares/api_auth.js new file mode 100644 index 0000000..e69de29 diff --git a/middlewares/web_auth.js b/middlewares/web_auth.js new file mode 100644 index 0000000..e69de29 diff --git a/migrations/20200913222330_initialize_database.js b/migrations/20200913222330_initialize_database.js deleted file mode 100644 index f3e1ef4..0000000 --- a/migrations/20200913222330_initialize_database.js +++ /dev/null @@ -1,13 +0,0 @@ -exports.up = function (knex) { - return knex.schema.createTable("messages", (tableBuilder) => { - tableBuilder.uuid("id").primary(); - tableBuilder.string("title"); - tableBuilder.string("status"); - tableBuilder.string("created_by"); - tableBuilder.string("created_time"); - tableBuilder.string("description"); - tableBuilder.text("content"); - }); -}; - -exports.down = function (knex) {}; diff --git a/models/index.js b/models/index.js new file mode 100644 index 0000000..31441f3 --- /dev/null +++ b/models/index.js @@ -0,0 +1,24 @@ +const User = require('./user'); +const Message = require('./message'); +const sequelize = require('../common/database'); + +Message.belongsTo(User); + +(async () => { + await sequelize.sync({ alter: true }); + console.log('Database initialized.'); + const isNoAdminExisted = + (await User.findOne({ where: { isAdmin: true } })) === null; + if (isNoAdminExisted) { + console.log('No admin user existed! Creating one for you.'); + await User.create({ + username: 'admin', + password: '123456', + isAdmin: true, + prefix: 'admin', + }); + } +})(); + +exports.User = User; +exports.Message = Message; diff --git a/models/message.js b/models/message.js index 1a573d0..59ac7e1 100644 --- a/models/message.js +++ b/models/message.js @@ -1,19 +1,24 @@ -const db = require("../utils/database").db; -const { v4: uuid } = require("uuid"); +const { DataTypes, Model } = require('sequelize'); +const sequelize = require('../common/database'); -class Message { - create(message) { - message.id = uuid(); - return db("messages").insert(message); - } +class Message extends Model {} - getById(id) { - return db("messages").where("id", id); - } +Message.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + title: DataTypes.STRING, + description: DataTypes.TEXT, + content: DataTypes.TEXT, + type: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + }, + { sequelize } +); - deleteById(id) { - return db("messages").where("id", id).del(); - } -} - -module.exports.Message = new Message(); +module.exports = Message; diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..6a56854 --- /dev/null +++ b/models/user.js @@ -0,0 +1,48 @@ +const { DataTypes, Model } = require('sequelize'); +const sequelize = require('../common/database'); + +class User extends Model {} + +User.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + username: { + type: DataTypes.STRING, + unique: true, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + }, + accessToken: { + type: DataTypes.STRING, + defaultValue: '', + }, + email: { + type: DataTypes.STRING, + }, + prefix: { + type: DataTypes.STRING, + unique: true, + }, + isAdmin: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + isBlocked: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + wechatAppId: DataTypes.STRING, + wechatAppSecret: DataTypes.STRING, + wechatTemplateId: DataTypes.STRING, + wechatOpenId: DataTypes.STRING, + }, + { sequelize } +); + +module.exports = User; diff --git a/package.json b/package.json index b1bc2ca..54b7415 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,18 @@ "axios": "^0.21.1", "cookie-parser": "~1.4.4", "debug": "~2.6.9", - "dotenv": "^8.2.0", "ejs": "^3.1.5", "express": "~4.16.1", - "knex": "^0.21.5", - "marked": "^1.1.1", + "marked": "^1.2.7", "morgan": "~1.9.1", - "sqlite3": "^5.0.0", - "uuid": "^8.3.0" + "sequelize": "^6.3.5", + "sqlite3": "^5.0.1" }, "devDependencies": { "nodemon": "^2.0.6", "prettier": "^2.1.1" + }, + "prettier": { + "singleQuote": true } } diff --git a/routers/index.js b/routers/index.js index 24ba140..4fb5969 100644 --- a/routers/index.js +++ b/routers/index.js @@ -1,35 +1,32 @@ -const express = require("express"); +const express = require('express'); const router = express.Router(); -const crypto = require("crypto"); -const fs = require("fs"); +const crypto = require('crypto'); +const fs = require('fs'); -router.all("/", (req, res, next) => { +router.all('/', (req, res, next) => { fs.promises - .access("./.env") + .access('./.env') .then(() => { - res.render("info", { - message: "服务已在运行。", + res.render('info', { + message: '服务已在运行。', }); -<<<<<<< HEAD:routers/index.js -======= // pushMessage( // req, // res, // `请注意,ip 地址为 ${req.ip} 的用户访问了你的消息通知服务,如果非你本人,则你的私有消息通知服务可能已被泄露,当前版本无法阻止其他用户通过本系统向你发送消息。` // ); ->>>>>>> master:routes.js }) .catch(() => { - res.render("configure"); + res.render('configure'); }); }); -router.post("/configure", (req, res, next) => { +router.post('/configure', (req, res, next) => { fs.promises - .access("./.env") + .access('./.env') .then(() => { - res.render("message", { - message: ".env 文件已经存在,请手动删除该文件后重试!", + res.render('message', { + message: '.env 文件已经存在,请手动删除该文件后重试!', }); }) .catch(() => { @@ -40,51 +37,48 @@ router.post("/configure", (req, res, next) => { `TEMPLATE_ID=${req.body.TEMPLATE_ID}\n` + `OPEN_ID=${req.body.OPEN_ID}`; fs.promises - .writeFile("./.env", content, "utf8") + .writeFile('./.env', content, 'utf8') .then(() => { - res.render("message", { + res.render('message', { message: - ".env 文件写入成功,程序即将自动关闭以应用写入的新的环境变量,需要进程守护程序自动重启应用或者手动重启。", + '.env 文件写入成功,程序即将自动关闭以应用写入的新的环境变量,需要进程守护程序自动重启应用或者手动重启。', }); process.exit(); }) .catch((e) => { - res.render("info", { - message: "在尝试写入 .env 文件时发生错误:" + e, + res.render('info', { + message: '在尝试写入 .env 文件时发生错误:' + e, }); }); }); }); -router.all("/verify", (req, res, next) => { +router.all('/verify', (req, res, next) => { // 验证消息来自微信服务器:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html const { signature, timestamp, nonce, echostr } = req.query; const token = process.env.TOKEN; let tmp_array = [token, timestamp, nonce].sort(); - let tmp_string = tmp_array.join(""); - tmp_string = crypto.createHash("sha1").update(tmp_string).digest("hex"); + let tmp_string = tmp_array.join(''); + tmp_string = crypto.createHash('sha1').update(tmp_string).digest('hex'); if (tmp_string === signature) { res.send(echostr); } else { - res.send("verification failed"); + res.send('verification failed'); } }); -<<<<<<< HEAD:routers/index.js -======= -router.all("/push", (req, res, next) => { +router.all('/push', (req, res, next) => { let content = req.query.content || req.body.content; pushMessage(req, res, content); }); -router.get("/favicon.ico", (req, res, next) => { +router.get('/favicon.ico', (req, res, next) => { res.sendStatus(404); }); -router.all("/:content", (req, res, next) => { +router.all('/:content', (req, res, next) => { let content = req.params.content; pushMessage(req, res, content); }); ->>>>>>> master:routes.js module.exports = router; diff --git a/routers/message.js b/routers/message.js index 5d56f88..256b06b 100644 --- a/routers/message.js +++ b/routers/message.js @@ -1,9 +1,9 @@ -const express = require("express"); -const lexer = require("marked").lexer; -const parser = require("marked").parser; -const Message = require("../models/message").Message; -const pushWeChatMessage = require("../utils/wechat").pushWeChatMessage; -const formatTime = require("../utils/utils").formatTime; +const express = require('express'); +const lexer = require('marked').lexer; +const parser = require('marked').parser; +const Message = require('../models/message').Message; +const pushWeChatMessage = require('../common/wechat').pushWeChatMessage; +const formatTime = require('../common/utils').formatTime; const router = express.Router(); @@ -11,16 +11,16 @@ function md2html(markdown) { return parser(lexer(markdown)); } -router.get("/:description", (req, res, next) => { +router.get('/:description', (req, res, next) => { req.query.description = req.params.description; next(); }); -router.all("/", (req, res) => { +router.all('/', (req, res) => { let message = { - title: req.query.title || req.body.title || "无标题", + title: req.query.title || req.body.title || '无标题', status: 1, - created_by: "系统", // TODO + created_by: '系统', // TODO created_time: formatTime(), description: req.query.description || req.body.description, content: md2html(req.query.content || req.body.content), @@ -42,16 +42,16 @@ router.all("/", (req, res) => { }); }); -router.get("/detail/:id", (req, res) => { +router.get('/detail/:id', (req, res) => { const id = req.params.id; Message.getById(id) .then((value) => { console.log(value); - res.render("message", value[0]); + res.render('message', value[0]); }) .catch((reason) => { - res.render("info", { - message: "获取该消息时发生了错误:" + reason, + res.render('info', { + message: '获取该消息时发生了错误:' + reason, }); }); }); diff --git a/utils/database.js b/utils/database.js deleted file mode 100644 index 59911ce..0000000 --- a/utils/database.js +++ /dev/null @@ -1,9 +0,0 @@ -const knex = require("knex"); -const environment = process.env.NODE_ENV || "development"; -const configuration = require("../knexfile")[environment]; - -const db = knex(configuration); - -module.exports = { - db: db, -}; diff --git a/utils/utils.js b/utils/utils.js deleted file mode 100644 index 04093f1..0000000 --- a/utils/utils.js +++ /dev/null @@ -1,33 +0,0 @@ -function formatTime(format) { - if (format === undefined) format = "yyyy-MM-dd hh:mm:ss"; - const date = new Date(); - const o = { - "M+": date.getMonth() + 1, - "d+": date.getDate(), - "h+": date.getHours(), - "m+": date.getMinutes(), - "s+": date.getSeconds(), - S: date.getMilliseconds(), - }; - - if (/(y+)/.test(format)) { - format = format.replace( - RegExp.$1, - (date.getFullYear() + "").substr(4 - RegExp.$1.length) - ); - } - - for (let k in o) { - if (new RegExp("(" + k + ")").test(format)) { - format = format.replace( - RegExp.$1, - RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length) - ); - } - } - return format; -} - -module.exports = { - formatTime, -}; diff --git a/utils/wechat.js b/utils/wechat.js deleted file mode 100644 index e08be9d..0000000 --- a/utils/wechat.js +++ /dev/null @@ -1,36 +0,0 @@ -const axios = require("axios"); - -module.exports = { - requestToken: function (app) { - let token = ""; - axios - .get( - `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${process.env.APP_ID}&secret=${process.env.APP_SECRET}` - ) - .then((res) => { - if (res.data && res.data.access_token) { - console.log("Token requested."); - token = res.data.access_token; - app.locals.access_token = token; - process.env.access_token = token; - } else { - console.error(res.data); - } - }); - return token; - }, - - pushWeChatMessage: function (description, link) { - // Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl - let access_token = process.env.access_token; - let request_data = { - touser: process.env.OPEN_ID, - template_id: process.env.TEMPLATE_ID, - }; - request_data.data = { text: { value: description } }; - return axios.post( - `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`, - request_data - ); - }, -};