🎉 able to login & logout
This commit is contained in:
@@ -1,17 +1,25 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
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 cookieParser = require('cookie-parser');
|
||||||
const logger = require('morgan');
|
const logger = require('morgan');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
const serveStatic = require('serve-static');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
|
||||||
const indexRouter = require('./routers/index');
|
const indexRouter = require('./routers/index');
|
||||||
const messageRouter = require('./routers/message');
|
const messageRouter = require('./routers/message');
|
||||||
|
const userRouter = require('./routers/user');
|
||||||
const { refreshToken } = require('./common/wechat');
|
const { refreshToken } = require('./common/wechat');
|
||||||
const { initializeTokenStore } = require('./common/token');
|
const { initializeTokenStore } = require('./common/token');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
app.locals.isLogged = false;
|
||||||
|
app.locals.isAdmin = false;
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
// TODO: Here we need an improvement! I have tried EventEmitter but it's not working. :(
|
// TODO: Here we need an improvement! I have tried EventEmitter but it's not working. :(
|
||||||
await initializeTokenStore();
|
await initializeTokenStore();
|
||||||
@@ -23,14 +31,49 @@ app.set('views', path.join(__dirname, 'views'));
|
|||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'ejs');
|
||||||
app.set('trust proxy', true);
|
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(logger('dev'));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: false }));
|
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(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use(
|
||||||
app.use('/', indexRouter);
|
'/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('/message', messageRouter);
|
||||||
|
app.use('/', indexRouter);
|
||||||
|
app.use('/', userRouter);
|
||||||
|
|
||||||
const server = http.createServer(app);
|
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 = {
|
module.exports = {
|
||||||
async processMessage(userPrefix, message) {
|
processMessage,
|
||||||
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;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ async function initializeTokenStore() {
|
|||||||
appSecret: user.wechatAppSecret,
|
appSecret: user.wechatAppSecret,
|
||||||
templateId: user.wechatTemplateId,
|
templateId: user.wechatTemplateId,
|
||||||
openId: user.wechatOpenId,
|
openId: user.wechatOpenId,
|
||||||
|
serverVerifyToken: user.wechatVerifyToken,
|
||||||
token: '',
|
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) {
|
async function pushWeChatMessage(userPrefix, message) {
|
||||||
// Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl
|
// Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl
|
||||||
let user = tokenStore.get(userPrefix);
|
let user = tokenStore.get(userPrefix);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `tokenStore 中不存在该前缀(${userPrefix})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
let access_token = user.token;
|
let access_token = user.token;
|
||||||
let request_data = {
|
let request_data = {
|
||||||
touser: user.wechatOpenId,
|
touser: user.wechatOpenId,
|
||||||
template_id: user.wechatTemplateId,
|
template_id: user.wechatTemplateId,
|
||||||
};
|
};
|
||||||
if (message.content) {
|
if (message.content) {
|
||||||
// TODO
|
request_data.url = `${config.href}${userPrefix}/${message.id}`;
|
||||||
// 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 } };
|
request_data.data = { text: { value: message.description } };
|
||||||
let requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`;
|
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,
|
port: process.env.PORT || 3000,
|
||||||
database: 'data.db',
|
database: 'data.db',
|
||||||
href: 'https://github.com/',
|
href: 'https://github.com/',
|
||||||
|
session_secret: 'change this',
|
||||||
|
cookie_secret: 'change this',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ User.init(
|
|||||||
wechatAppSecret: DataTypes.STRING,
|
wechatAppSecret: DataTypes.STRING,
|
||||||
wechatTemplateId: DataTypes.STRING,
|
wechatTemplateId: DataTypes.STRING,
|
||||||
wechatOpenId: DataTypes.STRING,
|
wechatOpenId: DataTypes.STRING,
|
||||||
|
wechatVerifyToken: DataTypes.STRING,
|
||||||
},
|
},
|
||||||
{ sequelize }
|
{ sequelize }
|
||||||
);
|
);
|
||||||
|
|||||||
+7
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wechat-message-push",
|
"name": "message-pusher",
|
||||||
"version": "0.0.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./app.js",
|
"start": "node ./app.js",
|
||||||
@@ -8,13 +8,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"connect-flash": "^0.1.1",
|
||||||
"cookie-parser": "~1.4.4",
|
"cookie-parser": "~1.4.4",
|
||||||
"debug": "~2.6.9",
|
"debug": "~2.6.9",
|
||||||
"ejs": "^3.1.5",
|
"ejs": "^3.1.5",
|
||||||
"express": "~4.16.1",
|
"express": "~4.16.1",
|
||||||
|
"express-rate-limit": "^5.2.3",
|
||||||
|
"express-session": "^1.17.1",
|
||||||
"marked": "^1.2.7",
|
"marked": "^1.2.7",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
"sequelize": "^6.3.5",
|
"sequelize": "^6.3.5",
|
||||||
|
"serve-static": "^1.14.1",
|
||||||
"sqlite3": "^5.0.1"
|
"sqlite3": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const crypto = require('crypto');
|
const { User } = require('../models');
|
||||||
const fs = require('fs');
|
const { tokenStore } = require('../common/token');
|
||||||
|
|
||||||
router.all('/', (req, res, next) => {
|
router.get('/', (req, res, next) => {
|
||||||
fs.promises
|
res.render('index', {
|
||||||
.access('./.env')
|
message: '',
|
||||||
.then(() => {
|
});
|
||||||
res.render('info', {
|
|
||||||
message: '服务已在运行。',
|
|
||||||
});
|
|
||||||
// pushMessage(
|
|
||||||
// req,
|
|
||||||
// res,
|
|
||||||
// `请注意,ip 地址为 ${req.ip} 的用户访问了你的消息通知服务,如果非你本人,则你的私有消息通知服务可能已被泄露,当前版本无法阻止其他用户通过本系统向你发送消息。`
|
|
||||||
// );
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
res.render('configure');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/configure', (req, res, next) => {
|
router.get('/login', (req, res, next) => {
|
||||||
fs.promises
|
res.render('login', {
|
||||||
.access('./.env')
|
message: '',
|
||||||
.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.all('/verify', (req, res, next) => {
|
router.post('/login', async (req, res, next) => {
|
||||||
// 验证消息来自微信服务器:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
|
let user = {
|
||||||
const { signature, timestamp, nonce, echostr } = req.query;
|
username: req.body.username,
|
||||||
const token = process.env.TOKEN;
|
password: req.body.password,
|
||||||
let tmp_array = [token, timestamp, nonce].sort();
|
};
|
||||||
let tmp_string = tmp_array.join('');
|
let message = '';
|
||||||
tmp_string = crypto.createHash('sha1').update(tmp_string).digest('hex');
|
try {
|
||||||
if (tmp_string === signature) {
|
user = await User.findOne({ where: user });
|
||||||
res.send(echostr);
|
if (user) {
|
||||||
} else {
|
req.session.user = user;
|
||||||
res.send('verification failed');
|
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) => {
|
router.post('/configure', async (req, res, next) => {
|
||||||
let content = req.query.content || req.body.content;
|
let user = {
|
||||||
pushMessage(req, res, content);
|
username: req.body.username,
|
||||||
});
|
password: req.body.password,
|
||||||
|
accessToken: req.body.accessToken,
|
||||||
router.get('/favicon.ico', (req, res, next) => {
|
email: req.body.email,
|
||||||
res.sendStatus(404);
|
prefix: req.body.prefix,
|
||||||
});
|
wechatAppId: req.body.wechatAppId,
|
||||||
|
wechatAppSecret: req.body.wechatAppSecret,
|
||||||
router.all('/:content', (req, res, next) => {
|
wechatTemplateId: req.body.wechatTemplateId,
|
||||||
let content = req.params.content;
|
wechatOpenId: req.body.wechatOpenId,
|
||||||
pushMessage(req, res, content);
|
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;
|
module.exports = router;
|
||||||
|
|||||||
+10
-43
@@ -1,9 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const lexer = require('marked').lexer;
|
const lexer = require('marked').lexer;
|
||||||
const parser = require('marked').parser;
|
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();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -11,49 +8,19 @@ function md2html(markdown) {
|
|||||||
return parser(lexer(markdown));
|
return parser(lexer(markdown));
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/:description', (req, res, next) => {
|
router.get('/delete/:id', (req, res, next) => {
|
||||||
req.query.description = req.params.description;
|
// TODO: delete message
|
||||||
next();
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Ok',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.all('/', (req, res) => {
|
router.get('/:id', (req, res, next) => {
|
||||||
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) => {
|
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
Message.getById(id)
|
// TODO: show article
|
||||||
.then((value) => {
|
req.query.description = req.params.description;
|
||||||
console.log(value);
|
res.render('article');
|
||||||
res.render('message', value[0]);
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
res.render('info', {
|
|
||||||
message: '获取该消息时发生了错误:' + reason,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
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') %>
|
<%- include('./partials/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>
|
|
||||||
|
|
||||||
<div class="field">
|
<div class="normal-container">
|
||||||
<label class="label">APP SECRET</label>
|
<div>
|
||||||
<div class="control">
|
<h2 class="title">配置页面</h2>
|
||||||
<input class="input" name="APP_SECRET" type="text" required placeholder="请输入 APP SECRET">
|
<form action="/configure" method="post">
|
||||||
</div>
|
<div class="field">
|
||||||
</div>
|
<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">
|
<div class="field">
|
||||||
<label class="label">OPEN ID</label>
|
<label class="label">APP SECRET</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" name="OPEN_ID" type="text" required placeholder="请输入你的 OPEN ID">
|
<input class="input" name="APP_SECRET" type="text" required placeholder="请输入 APP SECRET">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">TEMPLATE ID</label>
|
<label class="label">OPEN ID</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" name="TEMPLATE_ID" type="text" required placeholder="请输入一个模板通知 ID">
|
<input class="input" name="OPEN_ID" type="text" required placeholder="请输入你的 OPEN ID">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">TOKEN</label>
|
<label class="label">TEMPLATE ID</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" name="TOKEN" type="text" required placeholder="请输入你的 TOKEN">
|
<input class="input" name="TEMPLATE_ID" type="text" required placeholder="请输入一个模板通知 ID">
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
<%- include('./footer') %>
|
|
||||||
|
|
||||||
|
<%- 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') %>
|
<%- include('./partials/header') %>
|
||||||
<article class="info is-primary">
|
<article class="message is-danger">
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
<p>注意</p>
|
<p><%= title %></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
|
<%= message %>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</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