浏览代码

:tada: able to login & logout

Song 4 年之前
父节点
当前提交
b419c0f096

+ 47 - 4
app.js

@@ -1,17 +1,25 @@
 const express = require('express');
 const path = require('path');
+const session = require('express-session');
+const flash = require('connect-flash');
+const rateLimit = require('express-rate-limit');
+const compression = require('compression');
 const cookieParser = require('cookie-parser');
 const logger = require('morgan');
 const http = require('http');
+const serveStatic = require('serve-static');
 const config = require('./config');
-
 const indexRouter = require('./routers/index');
 const messageRouter = require('./routers/message');
+const userRouter = require('./routers/user');
 const { refreshToken } = require('./common/wechat');
 const { initializeTokenStore } = require('./common/token');
 
 const app = express();
 
+app.locals.isLogged = false;
+app.locals.isAdmin = false;
+
 setTimeout(async () => {
   // TODO: Here we need an improvement! I have tried EventEmitter but it's not working. :(
   await initializeTokenStore();
@@ -23,14 +31,49 @@ app.set('views', path.join(__dirname, 'views'));
 app.set('view engine', 'ejs');
 app.set('trust proxy', true);
 
+app.use(
+  rateLimit({
+    windowMs: 30 * 1000,
+    max: 30,
+  })
+);
+app.use(
+  '/login',
+  rateLimit({
+    windowMs: 60 * 1000,
+    max: 5,
+  })
+);
+app.use(compression());
 app.use(logger('dev'));
 app.use(express.json());
 app.use(express.urlencoded({ extended: false }));
-app.use(cookieParser());
+app.use(cookieParser(config.cookie_secret));
+app.use(
+  session({
+    resave: true,
+    saveUninitialized: true,
+    secret: config.session_secret,
+  })
+);
+app.use(flash());
 app.use(express.static(path.join(__dirname, 'public')));
-
-app.use('/', indexRouter);
+app.use(
+  '/public',
+  serveStatic(path.join(__dirname, `public`), {
+    maxAge: '600000',
+  })
+);
+app.use('*', (req, res, next) => {
+  if (req.session.user !== undefined) {
+    res.locals.isLogged = true;
+    res.locals.isAdmin = req.session.user.isAdmin;
+  }
+  next();
+});
 app.use('/message', messageRouter);
+app.use('/', indexRouter);
+app.use('/', userRouter);
 
 const server = http.createServer(app);
 

+ 27 - 21
common/message.js

@@ -1,24 +1,30 @@
-const { pushWechatMessage } = require('./wechat');
+const { pushWeChatMessage } = require('./wechat');
+const Message = require('../models/message').Message;
+
+async function processMessage(userPrefix, message) {
+  if (message.content) {
+    message = await Message.create(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;
+}
 
 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;
-  },
+  processMessage,
 };

+ 1 - 0
common/token.js

@@ -11,6 +11,7 @@ async function initializeTokenStore() {
         appSecret: user.wechatAppSecret,
         templateId: user.wechatTemplateId,
         openId: user.wechatOpenId,
+        serverVerifyToken: user.wechatVerifyToken,
         token: '',
       });
     }

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

+ 7 - 4
common/wechat.js

@@ -28,16 +28,19 @@ async function requestToken(appId, appSecret) {
 async function pushWeChatMessage(userPrefix, message) {
   // Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl
   let user = tokenStore.get(userPrefix);
+  if (!user) {
+    return {
+      success: false,
+      message: `tokenStore 中不存在该前缀(${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.url = `${config.href}${userPrefix}/${message.id}`;
   }
   request_data.data = { text: { value: message.description } };
   let requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`;

+ 2 - 0
config.js

@@ -2,6 +2,8 @@ const config = {
   port: process.env.PORT || 3000,
   database: 'data.db',
   href: 'https://github.com/',
+  session_secret: 'change this',
+  cookie_secret: 'change this',
 };
 
 module.exports = config;

+ 1 - 0
models/user.js

@@ -41,6 +41,7 @@ User.init(
     wechatAppSecret: DataTypes.STRING,
     wechatTemplateId: DataTypes.STRING,
     wechatOpenId: DataTypes.STRING,
+    wechatVerifyToken: DataTypes.STRING,
   },
   { sequelize }
 );

+ 7 - 2
package.json

@@ -1,6 +1,6 @@
 {
-  "name": "wechat-message-push",
-  "version": "0.0.0",
+  "name": "message-pusher",
+  "version": "0.2.0",
   "private": true,
   "scripts": {
     "start": "node ./app.js",
@@ -8,13 +8,18 @@
   },
   "dependencies": {
     "axios": "^0.21.1",
+    "compression": "^1.7.4",
+    "connect-flash": "^0.1.1",
     "cookie-parser": "~1.4.4",
     "debug": "~2.6.9",
     "ejs": "^3.1.5",
     "express": "~4.16.1",
+    "express-rate-limit": "^5.2.3",
+    "express-session": "^1.17.1",
     "marked": "^1.2.7",
     "morgan": "~1.9.1",
     "sequelize": "^6.3.5",
+    "serve-static": "^1.14.1",
     "sqlite3": "^5.0.1"
   },
   "devDependencies": {

+ 0 - 0
public/favicon.ico


+ 246 - 0
public/main.css

@@ -0,0 +1,246 @@
+body {
+    font-family: Verdana, Candara, Arial, Helvetica, Microsoft YaHei, sans-serif;
+    line-height: 1.6;
+    margin: 0;
+}
+
+nav {
+    margin-bottom: 16px;
+}
+
+a {
+    text-decoration: none;
+}
+
+.wrapper {
+    max-width: 960px;
+    margin: 0 auto;
+}
+
+#page-container {
+    position: relative;
+    min-height: 97vh;
+}
+
+#content-wrap {
+    padding-bottom: 4rem;
+}
+
+#footer {
+    height: 4rem;
+}
+
+#footer a {
+    color: black;
+}
+
+code {
+    font-family: Consolas, 'Courier New', monospace;
+}
+
+.page-card-list {
+    margin: 8px 8px;
+}
+
+.page-card-title {
+    font-size: x-large;
+    font-weight: 500;
+    color: #000000;
+    text-decoration: none;
+}
+
+.page-card-text {
+    margin-top: 8px;
+}
+
+.pagination {
+    margin: 16px 4px;
+}
+
+.pagination a {
+    border: none;
+    overflow: hidden;
+}
+
+.shadow {
+    box-shadow: 0 0.5em 1em -0.125em rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.02);
+}
+
+.nav-shadow {
+    box-shadow: 0 2px 3px rgba(26,26,26,.1);
+}
+
+.box article {
+    overflow-wrap: break-word;
+    font-size: larger;
+    word-break: break-word;
+    line-height: 1.6;
+    padding: 16px;
+    margin-bottom: 16px;
+    background-color: #ffffff;
+}
+
+img {
+    max-width: 100%;
+    max-height: 100%;
+}
+
+.normal-container {
+    margin: auto;
+    max-width: 960px;
+    padding: 16px 16px;
+    overflow-wrap: break-word;
+    word-break: break-word;
+    line-height: 1.6;
+    font-size: larger;
+}
+
+.narrow-container {
+    margin: auto;
+    max-width: 560px;
+    padding: 16px 16px;
+    overflow-wrap: break-word;
+    word-break: break-word;
+    line-height: 1.6;
+    font-size: larger;
+}
+
+article a {
+    color: #368CCB;
+    text-decoration: none;
+}
+
+article a:hover {
+    color: #368CCB;
+    text-decoration: none;
+}
+
+article h2,
+article h3,
+article h4,
+article h5,
+article h6 {
+    font-weight: 700;
+    line-height: 1.5;
+    margin: 20px 0 15px;
+    margin-block-start: 1em;
+    margin-block-end: 0.2em;
+}
+
+article h1 {
+    font-size: 1.7em
+}
+
+article h2 {
+    font-size: 1.6em
+}
+
+article h3 {
+    font-size: 1.45em
+}
+
+article h4 {
+    font-size: 1.25em;
+}
+
+article h5 {
+    font-size: 1.1em;
+}
+article h6 {
+    font-size: 1em;
+    font-weight: bold
+}
+
+@media screen and (max-width: 960px) {
+    article h1 {
+        font-size: 1.5em
+    }
+
+    article h2 {
+        font-size: 1.35em
+    }
+
+    article h3 {
+        font-size: 1.3em
+    }
+
+    article h4 {
+        font-size: 1.2em;
+    }
+}
+
+article p {
+    margin-top: 0;
+    margin-bottom: 1.25rem;
+}
+
+article table {
+    margin: auto;
+    border-collapse: collapse;
+    border-spacing: 0;
+    vertical-align: middle;
+    text-align: left;
+    min-width: 66%;
+}
+
+article table td,
+article table th {
+    padding: 5px 8px;
+    border: 1px solid #bbb;
+}
+
+article blockquote {
+    margin-left: 0;
+    padding: 0 1em;
+    font-size: smaller;
+    border-left: 5px solid #ddd;
+}
+
+article pre {
+    overflow-x: auto;
+    padding: 0;
+    font-size: 16px;
+    margin-top: 12px;
+    margin-bottom: 12px;
+}
+
+article ol {
+    text-decoration: none;
+    padding-inline-start: 40px;
+    margin-bottom: 1.25rem;
+}
+
+article code {
+    color: #bc9458;
+    padding: .065em .4em;
+}
+
+article .copyright{
+    display: none;
+}
+
+.info {
+    font-size: 14px;
+    line-height: 28px;
+    text-align: left;
+    color: #738292;
+    margin-bottom: 3em
+}
+
+.info a {
+    text-decoration: none;
+    color: inherit;
+}
+
+.links {
+    margin: 16px;
+}
+
+span.line {
+    display: inline-block;
+}
+
+.toc {
+    position: sticky;
+    top: 24px;
+}

+ 0 - 0
public/main.js


+ 2 - 0
public/robots.txt

@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /

+ 68 - 67
routers/index.js

@@ -1,84 +1,85 @@
 const express = require('express');
 const router = express.Router();
-const crypto = require('crypto');
-const fs = require('fs');
+const { User } = require('../models');
+const { tokenStore } = require('../common/token');
 
-router.all('/', (req, res, next) => {
-  fs.promises
-    .access('./.env')
-    .then(() => {
-      res.render('info', {
-        message: '服务已在运行。',
-      });
-      // pushMessage(
-      //   req,
-      //   res,
-      //   `请注意,ip 地址为 ${req.ip} 的用户访问了你的消息通知服务,如果非你本人,则你的私有消息通知服务可能已被泄露,当前版本无法阻止其他用户通过本系统向你发送消息。`
-      // );
-    })
-    .catch(() => {
-      res.render('configure');
-    });
+router.get('/', (req, res, next) => {
+  res.render('index', {
+    message: '',
+  });
 });
 
-router.post('/configure', (req, res, next) => {
-  fs.promises
-    .access('./.env')
-    .then(() => {
-      res.render('message', {
-        message: '.env 文件已经存在,请手动删除该文件后重试!',
-      });
-    })
-    .catch(() => {
-      let content =
-        `APP_ID=${req.body.APP_ID}\n` +
-        `APP_SECRET=${req.body.APP_SECRET}\n` +
-        `TOKEN=${req.body.TOKEN}\n` +
-        `TEMPLATE_ID=${req.body.TEMPLATE_ID}\n` +
-        `OPEN_ID=${req.body.OPEN_ID}`;
-      fs.promises
-        .writeFile('./.env', content, 'utf8')
-        .then(() => {
-          res.render('message', {
-            message:
-              '.env 文件写入成功,程序即将自动关闭以应用写入的新的环境变量,需要进程守护程序自动重启应用或者手动重启。',
-          });
-          process.exit();
-        })
-        .catch((e) => {
-          res.render('info', {
-            message: '在尝试写入 .env 文件时发生错误:' + e,
-          });
-        });
-    });
+router.get('/login', (req, res, next) => {
+  res.render('login', {
+    message: '',
+  });
 });
 
-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');
-  if (tmp_string === signature) {
-    res.send(echostr);
-  } else {
-    res.send('verification failed');
+router.post('/login', async (req, res, next) => {
+  let user = {
+    username: req.body.username,
+    password: req.body.password,
+  };
+  let message = '';
+  try {
+    user = await User.findOne({ where: user });
+    if (user) {
+      req.session.user = user;
+      return res.redirect('/');
+    } else {
+      message = '用户名或密码错误';
+    }
+  } catch (e) {
+    console.error(e);
+    message = e.message;
   }
+  res.render('login', {
+    message,
+  });
 });
 
-router.all('/push', (req, res, next) => {
-  let content = req.query.content || req.body.content;
-  pushMessage(req, res, content);
+router.get('/register', (req, res, next) => {
+  res.render('register');
 });
 
-router.get('/favicon.ico', (req, res, next) => {
-  res.sendStatus(404);
+router.post('/register', async (req, res, next) => {
+  let user = {
+    username: req.body.username,
+    password: req.body.password,
+  };
+  try {
+    user = await User.create(user);
+  } catch (e) {
+    console.error(e);
+  }
 });
 
-router.all('/:content', (req, res, next) => {
-  let content = req.params.content;
-  pushMessage(req, res, content);
+router.post('/configure', async (req, res, next) => {
+  let user = {
+    username: req.body.username,
+    password: req.body.password,
+    accessToken: req.body.accessToken,
+    email: req.body.email,
+    prefix: req.body.prefix,
+    wechatAppId: req.body.wechatAppId,
+    wechatAppSecret: req.body.wechatAppSecret,
+    wechatTemplateId: req.body.wechatTemplateId,
+    wechatOpenId: req.body.wechatOpenId,
+    wechatVerifyToken: req.body.wechatVerifyToken,
+  };
+  try {
+    user = await User.create(user);
+    tokenStore.set(user.prefix, {
+      appId: user.wechatAppId,
+      appSecret: user.wechatAppSecret,
+      templateId: user.wechatTemplateId,
+      openId: user.wechatOpenId,
+      wechatVerifyToken: user.wechatVerifyToken,
+      token: '',
+    });
+  } catch (e) {
+    console.error(e);
+  }
 });
 
 module.exports = router;

+ 10 - 43
routers/message.js

@@ -1,9 +1,6 @@
 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,49 +8,19 @@ function md2html(markdown) {
   return parser(lexer(markdown));
 }
 
-router.get('/:description', (req, res, next) => {
-  req.query.description = req.params.description;
-  next();
+router.get('/delete/:id', (req, res, next) => {
+  // TODO: delete message
+  res.json({
+    success: true,
+    message: 'Ok',
+  });
 });
 
-router.all('/', (req, res) => {
-  let message = {
-    title: req.query.title || req.body.title || '无标题',
-    status: 1,
-    created_by: '系统', // TODO
-    created_time: formatTime(),
-    description: req.query.description || req.body.description,
-    content: md2html(req.query.content || req.body.content),
-  };
-
-  Message.create(message)
-    .then((value) => {
-      console.log(value);
-      pushWeChatMessage(message.description, message.link)
-        .then((response) => {
-          res.json(response);
-        })
-        .catch((reason) => {
-          res.json(reason);
-        });
-    })
-    .catch((reason) => {
-      res.json(reason);
-    });
-});
-
-router.get('/detail/:id', (req, res) => {
+router.get('/:id', (req, res, next) => {
   const id = req.params.id;
-  Message.getById(id)
-    .then((value) => {
-      console.log(value);
-      res.render('message', value[0]);
-    })
-    .catch((reason) => {
-      res.render('info', {
-        message: '获取该消息时发生了错误:' + reason,
-      });
-    });
+  // TODO: show article
+  req.query.description = req.params.description;
+  res.render('article');
 });
 
 module.exports = router;

+ 40 - 0
routers/user.js

@@ -0,0 +1,40 @@
+const express = require('express');
+const crypto = require('crypto');
+const Message = require('../models/message').Message;
+const { processMessage } = require('../common/message');
+const { tokenStore } = require('../common/token');
+
+const router = express.Router();
+
+router.get('/:userPrefix/configure', (req, res, next) => {
+  res.render('configure');
+});
+
+router.all('/:userPrefix/verify', (req, res, next) => {
+  // 验证消息来自微信服务器:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
+  const userPrefix = req.params.userPrefix;
+  const { signature, timestamp, nonce, echostr } = req.query;
+  const token = tokenStore.get(userPrefix).wechatVerifyToken;
+  let tmp_array = [token, timestamp, nonce].sort();
+  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');
+  }
+});
+
+router.all('/:userPrefix', async (req, res, next) => {
+  const userPrefix = req.params.userPrefix;
+  let message = {
+    type: req.query.type || req.body.type || '0',
+    title: req.query.title || req.body.title || '无标题',
+    description: req.query.description || req.body.description,
+    content: req.query.content || req.body.content,
+  };
+  let result = await processMessage(userPrefix, message);
+  res.json(result);
+});
+
+module.exports = router;

+ 34 - 0
views/article.ejs

@@ -0,0 +1,34 @@
+<%- include('./partials/header') %>
+
+<div class="normal-container">
+    <div class="columns is-desktop">
+        <div class="column">
+            <article id="article">
+                <%- include('./partials/page-info') %>
+                <%-page.converted_content%>
+                <p class="copyright"><strong>Links: <a href="<%-page.link%>"><%-page.link%></a></strong></p>
+                <%- config.copyright %>
+                <%- config.ad %>
+            </article>
+        </div>
+        <div class="column is-one-fifth-fullhd">
+            <aside class="menu toc">
+                <p class="menu-label">
+                    Table of Content
+                </p>
+                <ul class="menu-list" id="toc">
+                </ul>
+            </aside>
+        </div>
+    </div>
+    <%- include('./partials/prev-next') %>
+    <%- include('./partials/comment') %>
+</div>
+
+<script>
+  (function() {
+    generateTOC();
+  })();
+</script>
+
+<%- include('./partials/footer') %>

+ 51 - 50
views/configure.ejs

@@ -1,56 +1,57 @@
-<%- include('./header') %>
-<h2 class="subtitle">配置页面</h2>
-<article class="message is-warning">
-    <div class="message-header">
-        <p>警告</p>
-    </div>
-    <div class="message-body">
-        提交后将在程序的根目录创建一个 .env 文件,之后程序将自动关闭,请确保你已经使用了诸如 pm2 的进程守护程序来自动重启本程序。提交之后如果想要对配置内容进行修改,则你必须手动删除 .env 文件。
-    </div>
-</article>
-<form action="/configure" method="post">
-    <div class="field">
-        <label class="label">APP ID</label>
-        <div class="control">
-            <input class="input" name="APP_ID" type="text" required placeholder="请输入 APP ID">
-        </div>
-    </div>
+<%- include('./partials/header') %>
 
-    <div class="field">
-        <label class="label">APP SECRET</label>
-        <div class="control">
-            <input class="input" name="APP_SECRET" type="text" required placeholder="请输入 APP SECRET">
-        </div>
-    </div>
+<div class="normal-container">
+    <div>
+        <h2 class="title">配置页面</h2>
+        <form action="/configure" method="post">
+            <div class="field">
+                <label class="label">APP ID</label>
+                <div class="control">
+                    <input class="input" name="APP_ID" type="text" required placeholder="请输入 APP ID">
+                </div>
+            </div>
 
-    <div class="field">
-        <label class="label">OPEN ID</label>
-        <div class="control">
-            <input class="input" name="OPEN_ID" type="text" required placeholder="请输入你的 OPEN ID">
-        </div>
-    </div>
+            <div class="field">
+                <label class="label">APP SECRET</label>
+                <div class="control">
+                    <input class="input" name="APP_SECRET" type="text" required placeholder="请输入 APP SECRET">
+                </div>
+            </div>
 
-    <div class="field">
-        <label class="label">TEMPLATE ID</label>
-        <div class="control">
-            <input class="input" name="TEMPLATE_ID" type="text" required placeholder="请输入一个模板通知 ID">
-        </div>
-    </div>
+            <div class="field">
+                <label class="label">OPEN ID</label>
+                <div class="control">
+                    <input class="input" name="OPEN_ID" type="text" required placeholder="请输入你的 OPEN ID">
+                </div>
+            </div>
 
-    <div class="field">
-        <label class="label">TOKEN</label>
-        <div class="control">
-            <input class="input" name="TOKEN" type="text" required placeholder="请输入你的 TOKEN">
-        </div>
-    </div>
+            <div class="field">
+                <label class="label">TEMPLATE ID</label>
+                <div class="control">
+                    <input class="input" name="TEMPLATE_ID" type="text" required placeholder="请输入一个模板通知 ID">
+                </div>
+            </div>
+
+            <div class="field">
+                <label class="label">TOKEN</label>
+                <div class="control">
+                    <input class="input" name="TOKEN" type="text" required placeholder="请输入你的 TOKEN">
+                </div>
+            </div>
+
+            <div class="field is-grouped is-grouped-right">
+                <div class="control">
+                    <input type="submit" class="button is-link" value="提交">
+                </div>
+                <div class="control">
+                    <input type="reset" class="button is-link is-light" value="重置">
+                </div>
+            </div>
+        </form>
 
-    <div class="field is-grouped is-grouped-right">
-        <div class="control">
-            <input type="submit" class="button is-link" value="提交">
-        </div>
-        <div class="control">
-            <input type="reset" class="button is-link is-light" value="重置">
-        </div>
     </div>
-</form>
-<%- include('./footer') %>
+</div>
+
+
+<%- include('./partials/footer') %>
+

+ 0 - 11
views/header.ejs

@@ -1,11 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh">
-<head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
-    <title>WeChat Message Push Service</title>
-</head>
-<body style="width: 100%;">
-<div style="margin: auto; max-width: 960px">
-    <h1 class="title" style="margin-top: 16px"><a style="color: black; text-decoration: none">微信消息通知服务</a></h1>

+ 12 - 0
views/index.ejs

@@ -0,0 +1,12 @@
+<%- include('./partials/header') %>
+<div class="columns is-desktop">
+    <div class="column">
+        <div class="page-card-list">
+
+        </div>
+
+    </div>
+    </div>
+</div>
+
+<%- include('./partials/footer') %>

+ 41 - 0
views/login.ejs

@@ -0,0 +1,41 @@
+<%- include('./partials/header') %>
+
+<div class="narrow-container">
+    <div>
+        <h2 class="title">用户登录</h2>
+        <% if(message) { %>
+            <article class="message is-danger">
+                <div class="message-body">
+                    <%= message %>
+                </div>
+            </article>
+        <% }%>
+        <form action="/login" method="post">
+            <div class="field">
+                <label class="label">用户名</label>
+                <div class="control">
+                    <input class="input" name="username" type="text" required placeholder="请输入用户名">
+                </div>
+            </div>
+            <div class="field">
+                <label class="label">密码</label>
+                <div class="control">
+                    <input class="input" name="password" type="password" required placeholder="请输入你的密码">
+                </div>
+            </div>
+            <div class="field is-grouped is-grouped-right">
+                <div class="control">
+                    <input type="submit" class="button is-light" value="提交">
+                </div>
+                <div class="control">
+                    <input type="reset" class="button is-light" value="重置">
+                </div>
+            </div>
+        </form>
+
+    </div>
+</div>
+
+
+<%- include('./partials/footer') %>
+

+ 5 - 5
views/message.ejs

@@ -1,10 +1,10 @@
-<%- include('./header') %>
-<article class="info is-primary">
+<%- include('./partials/header') %>
+<article class="message is-danger">
     <div class="message-header">
-        <p>注意</p>
+        <p><%= title %></p>
     </div>
     <div class="message-body">
-
+        <%= message %>
     </div>
 </article>
-<%- include('./footer') %>
+<%- include('./partials/footer') %>

+ 12 - 0
views/partials/footer.ejs

@@ -0,0 +1,12 @@
+</div>
+<footer class="footer" style="background-color: white">
+    <div class="content has-text-centered" >
+        <p>
+            <a href="https://github.com/songquanpeng/message-pusher">消息推送服务</a> 由 <a href="https://github.com/songquanpeng">JustSong</a> 构建,源代码遵循
+            <a href="http://opensource.org/licenses/mit-license.php">MIT</a> 协议
+        </p>
+    </div>
+</footer>
+</div>
+</body>
+</html>

+ 16 - 0
views/partials/header.ejs

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>消息推送服务</title>
+    <meta name="theme-color" content="#ffffff"/>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
+    <link rel="stylesheet" href="/public/main.css">
+    <script src="/public/main.js"></script>
+</head>
+<body>
+
+<div>
+    <%- include('./nav') %>
+    <div class="container">

+ 50 - 0
views/partials/nav.ejs

@@ -0,0 +1,50 @@
+<nav class="navbar nav-shadow" role="navigation" aria-label="main navigation">
+    <div class="container">
+        <div class="navbar-brand">
+            <a class="navbar-item is-size-5" href="/" style="font-weight: bold">
+                消息推送服务
+            </a>
+
+            <a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
+               data-target="mainNavbar">
+                <span aria-hidden="true"></span>
+                <span aria-hidden="true"></span>
+                <span aria-hidden="true"></span>
+            </a>
+        </div>
+        <div id="mainNavbar" class="navbar-menu">
+            <div class="navbar-start">
+                <a class="navbar-item" href="/"> 首页 </a>
+                <a class="navbar-item" href="/status"> 状态 </a>
+                <a class="navbar-item" target="_blank" href="https://iamazing.cn/page/message-pusher"> 关于 </a>
+            </div>
+            <div class="navbar-end">
+                <div class="navbar-item">
+                    <div class="buttons">
+                        <% if (isLogged) { %>
+                            <a class="button is-light" href="/logout">退出</a>
+                        <% } else { %>
+                            <a class="button is-light" href="/register">注册</a>
+                            <a class="button is-light" href="/login">登录</a>
+                        <% } %>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</nav>
+<script>
+  document.addEventListener('DOMContentLoaded', () => {
+    const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
+    if ($navbarBurgers.length > 0) {
+      $navbarBurgers.forEach( el => {
+        el.addEventListener('click', () => {
+          const target = el.dataset.target;
+          const $target = document.getElementById(target);
+          el.classList.toggle('is-active');
+          $target.classList.toggle('is-active');
+        });
+      });
+    }
+  });
+</script>

+ 12 - 0
views/partials/page-info.ejs

@@ -0,0 +1,12 @@
+<h1 class="title is-3 is-4-mobile"><%-page.title%></h1>
+<div class="info">
+    <span class="line">
+        Tag:
+        <% page.tag.trim().split(" ").forEach(function (tag) {if (tag !== "") {%>
+            <a class="tag is-light" href='/tag/<%= tag %>'><%= tag %></a>
+        <% }}); %>
+    </span>
+    <span class="line">Posted on <span class="tag is-light"><%-page.post_time%></span></span>
+    <span class="line">Edited on <span class="tag is-light"><%-page.edit_time%></span></span>
+    <span class="line">Views: <span class="tag is-light"><%-page.view%></span></span>
+</div>

+ 34 - 0
views/register.ejs

@@ -0,0 +1,34 @@
+<%- include('./partials/header') %>
+
+<div class="narrow-container">
+    <div>
+        <h2 class="title">用户注册</h2>
+        <form action="/register" method="post">
+            <div class="field">
+                <label class="label">用户名</label>
+                <div class="control">
+                    <input class="input" name="username" type="text" required placeholder="请输入用户名">
+                </div>
+            </div>
+            <div class="field">
+                <label class="label">密码</label>
+                <div class="control">
+                    <input class="input" name="password" type="password" required placeholder="请输入你的密码">
+                </div>
+            </div>
+            <div class="field is-grouped is-grouped-right">
+                <div class="control">
+                    <input type="submit" class="button is-light" value="提交">
+                </div>
+                <div class="control">
+                    <input type="reset" class="button is-light" value="重置">
+                </div>
+            </div>
+        </form>
+
+    </div>
+</div>
+
+
+<%- include('./partials/footer') %>
+