🎉 able to login & logout
This commit is contained in:
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ async function initializeTokenStore() {
|
||||
appSecret: user.wechatAppSecret,
|
||||
templateId: user.wechatTemplateId,
|
||||
openId: user.wechatOpenId,
|
||||
serverVerifyToken: user.wechatVerifyToken,
|
||||
token: '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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;
|
||||
|
||||
@@ -41,6 +41,7 @@ User.init(
|
||||
wechatAppSecret: DataTypes.STRING,
|
||||
wechatTemplateId: DataTypes.STRING,
|
||||
wechatOpenId: DataTypes.STRING,
|
||||
wechatVerifyToken: DataTypes.STRING,
|
||||
},
|
||||
{ sequelize }
|
||||
);
|
||||
|
||||
+7
-2
@@ -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": {
|
||||
|
||||
+246
@@ -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 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
+72
-71
@@ -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.get('/register', (req, res, next) => {
|
||||
res.render('register');
|
||||
});
|
||||
|
||||
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('/push', (req, res, next) => {
|
||||
let content = req.query.content || req.body.content;
|
||||
pushMessage(req, res, content);
|
||||
});
|
||||
|
||||
router.get('/favicon.ico', (req, res, next) => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
|
||||
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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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') %>
|
||||
|
||||
|
||||
@@ -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/bulma@0.9.0/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>
|
||||
@@ -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') %>
|
||||
@@ -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
@@ -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') %>
|
||||
|
||||
@@ -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>
|
||||
@@ -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/bulma@0.9.1/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">
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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') %>
|
||||
|
||||
Reference in New Issue
Block a user