소스 검색

:construction: refactoring is ongoing

Song 4 년 전
부모
커밋
22975603c6
20개의 변경된 파일328개의 추가작업 그리고 200개의 파일을 삭제
  1. 25 22
      app.js
  2. 9 0
      common/database.js
  3. 24 0
      common/message.js
  4. 25 0
      common/token.js
  5. 33 0
      common/utils.js
  6. 68 0
      common/wechat.js
  7. 7 0
      config.js
  8. 0 22
      knexfile.js
  9. 0 0
      middlewares/api_auth.js
  10. 0 0
      middlewares/web_auth.js
  11. 0 13
      migrations/20200913222330_initialize_database.js
  12. 24 0
      models/index.js
  13. 21 16
      models/message.js
  14. 48 0
      models/user.js
  15. 6 5
      package.json
  16. 24 30
      routers/index.js
  17. 14 14
      routers/message.js
  18. 0 9
      utils/database.js
  19. 0 33
      utils/utils.js
  20. 0 36
      utils/wechat.js

+ 25 - 22
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 indexRouter = require("./routers/index");
-const messageRouter = require("./routers/message");
-const requestToken = require("./utils/wechat").requestToken;
+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 { 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;

+ 9 - 0
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;

+ 24 - 0
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;
+  },
+};

+ 25 - 0
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,
+};

+ 33 - 0
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,
+};

+ 68 - 0
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,
+};

+ 7 - 0
config.js

@@ -0,0 +1,7 @@
+const config = {
+  port: process.env.PORT || 3000,
+  database: 'data.db',
+  href: 'https://github.com/',
+};
+
+module.exports = config;

+ 0 - 22
knexfile.js

@@ -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",
-    },
-  },
-};

+ 0 - 0
middlewares/api_auth.js


+ 0 - 0
middlewares/web_auth.js


+ 0 - 13
migrations/20200913222330_initialize_database.js

@@ -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) {};

+ 24 - 0
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;

+ 21 - 16
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;

+ 48 - 0
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;

+ 6 - 5
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
   }
 }

+ 24 - 30
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;

+ 14 - 14
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,
       });
     });
 });

+ 0 - 9
utils/database.js

@@ -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,
-};

+ 0 - 33
utils/utils.js

@@ -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,
-};

+ 0 - 36
utils/wechat.js

@@ -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
-    );
-  },
-};