🎉 able to login & logout

This commit is contained in:
Song
2021-01-17 14:26:33 +08:00
parent 22975603c6
commit b419c0f096
26 changed files with 729 additions and 244 deletions
+47 -4
View File
@@ -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
View File
@@ -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
View File
@@ -11,6 +11,7 @@ async function initializeTokenStore() {
appSecret: user.wechatAppSecret,
templateId: user.wechatTemplateId,
openId: user.wechatOpenId,
serverVerifyToken: user.wechatVerifyToken,
token: '',
});
}
-33
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -41,6 +41,7 @@ User.init(
wechatAppSecret: DataTypes.STRING,
wechatTemplateId: DataTypes.STRING,
wechatOpenId: DataTypes.STRING,
wechatVerifyToken: DataTypes.STRING,
},
{ sequelize }
);
+7 -2
View File
@@ -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": {
View File
+246
View File
@@ -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;
}
View File
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
+72 -71
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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') %>
-11
View File
@@ -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>
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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">
+50
View File
@@ -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
View File
@@ -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
View File
@@ -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') %>