add user login and comment management

dependabot/npm_and_yarn/mixin-deep-1.3.2
panjunwen 7 years ago
parent a94072fe1b
commit 0ce7ff2686
  1. 61
      app.js
  2. 28
      cloud.js
  3. 4
      package.json
  4. 151
      public/stylesheets/style.css
  5. 79
      routes/comments.js
  6. 40
      routes/todos.js
  7. 91
      utilities/check-spam.js
  8. 71
      utilities/send-mail.js
  9. 50
      views/comments.ejs
  10. 26
      views/index.ejs
  11. 19
      views/todos.ejs

@ -26,28 +26,50 @@ app.use(AV.express());
app.enable('trust proxy');
// 需要重定向到 HTTPS 可去除下一行的注释。
// app.use(AV.Cloud.HttpsRedirect());
app.use(AV.Cloud.HttpsRedirect());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(AV.Cloud.CookieSession({ secret: 'my secret', maxAge: 3600000, fetchUser: true }));
app.get('/', function(req, res) {
res.render('index', { currentTime: new Date() });
if (req.currentUser) {
res.redirect('/comments');
} else {
res.render('index');
}
});
// 可以将一类的路由单独保存在一个文件中
app.use('/todos', require('./routes/todos'));
app.use('/comments', require('./routes/comments'));
app.use(function(req, res, next) {
// 如果任何一个路由都没有返回响应,则抛出一个 404 异常给后续的异常处理器
if (!res.headersSent) {
var err = new Error('Not Found');
err.status = 404;
next(err);
}
// 处理登录请求(可能来自登录界面中的表单)
app.post('/login', function(req, res) {
AV.User.logIn(req.body.username, req.body.password).then(function(user) {
res.saveCurrentUser(user); // 保存当前用户到 Cookie
res.redirect('/comments'); // 跳转到个人资料页面
}, function(error) {
//登录失败,跳转到登录页面
res.redirect('/');
});
});
// 登出账号
app.get('/logout', function(req, res) {
req.currentUser.logOut();
res.clearCurrentUser(); // 从 Cookie 中删除用户
res.redirect('/');
});
app.use(function(req, res, next) {
// 如果任何一个路由都没有返回响应,则抛出一个 404 异常给后续的异常处理器
if (!res.headersSent) {
var err = new Error('Not Found');
err.status = 404;
next(err);
}
});
// error handlers
app.use(function(err, req, res, next) {
if (req.timedout && req.headers.upgrade === 'websocket') {
@ -75,4 +97,23 @@ app.use(function(err, req, res, next) {
});
});
app.locals.dateFormat = function (date) {
var vDay = padWithZeros(date.getDate(), 2);
var vMonth = padWithZeros(date.getMonth() + 1, 2);
var vYear = padWithZeros(date.getFullYear(), 2);
var vHour = padWithZeros(date.getHours(), 2);
var vMinute = padWithZeros(date.getMinutes(), 2);
var vSecond = padWithZeros(date.getSeconds(), 2);
// return `${vYear}-${vMonth}-${vDay}`;
return `${vYear}-${vMonth}-${vDay} ${vHour}:${vMinute}:${vSecond}`;
};
const padWithZeros = (vNumber, width) => {
var numAsString = vNumber.toString();
while (numAsString.length < width) {
numAsString = '0' + numAsString;
}
return numAsString;
};
module.exports = app;

@ -1,8 +1,24 @@
var AV = require('leanengine');
const AV = require('leanengine');
const mail = require('./utilities/send-mail');
const spam = require('./utilities/check-spam');
/**
* 一个简单的云代码方法
*/
AV.Cloud.define('hello', function(request) {
return 'Hello world!';
AV.Cloud.afterSave('Comment', function (request) {
let currentComment = request.object;
// 检查垃圾评论
spam.checkSpam(currentComment);
// 发送博主通知邮件
mail.notice(currentComment);
// AT评论通知
let rid = currentComment.get('rid');
if (!rid) {
console.log('没有@任何人,结束!');
return;
}
let query = new AV.Query('Comment');
query.get(rid).then(function (parentComment) {
mail.send(currentComment, parentComment);
}, function (error) {
console.warn('获取@对象失败!');
});
});

@ -9,13 +9,15 @@
"dev": "nodemon server.js -- "
},
"dependencies": {
"akismet-api": "^3.0.0",
"body-parser": "1.12.3",
"connect-timeout": "^1.7.0",
"cookie-parser": "^1.3.5",
"ejs": "2.3.1",
"express": "4.12.3",
"leancloud-storage": "^3.0.0",
"leanengine": "^3.0.0"
"leanengine": "^3.0.0",
"nodemailer": "^4.0.1"
},
"devDependencies": {
"nodemon": "^1.11.0"

@ -1,7 +1,152 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;
font-size: 1rem;
}
a {
color: #00b7ff;
text-decoration: none;
color: #3eb0ef;
}
.footer {
font-size: 0.8rem;
}
.title {
font-weight: 200;
font-size: 1.5rem;
}
ul {
list-style: none;
}
li {
list-style: none;
}
.content {
padding: 0;
}
.header{
display: flex;
-ms-flex-direction: row;
flex-direction: row;
align-items: flex-start;
}
.header .title {
flex: 90%;
}
.logout-wrapper {
}
.comment-main{
padding: 1rem 5rem;
}
.vhead {
padding-bottom: 0.5rem;
}
.vhead a {
font-weight: bolder;
color: #333;
}
.spacer {
color: #ccc;
margin-left: 0.3rem;
margin-right: 0.3rem;
}
.vtime {
color: #a9a4a4;
display: inline-block;
padding: 0 5px;
}
.vcomment {
line-height: 1.8;
}
.vcard {
padding: 2rem 0 2rem 0;
border-top: 1px solid #dedede;
}
.check {
padding-top: 1rem;
color: #33b1ff;
}
.sign-in-wrap {
display: flex;
-ms-flex-direction: column;
flex-direction: column;
width: 100%;
color: #738a94;
font-weight: 100;
text-align: center;
align-items: center;
margin-top: 4rem;
}
.gh-signin {
display: flex;
-ms-flex-direction: column;
flex-direction: column;
padding: 1rem 2rem;
width: 350px;
border: 1px solid #dae1e3;
background: #f8fbfd;
border-radius: 5px;
text-align: left;
}
.gh-signin .form-group {
margin-bottom: 1.5rem;
position: relative;
width: 100%;
max-width: 700px;
-webkit-user-select: text;
user-select: text;
}
.sign-in-wrap input {
padding: 10px;
margin: 1rem 1rem;
border: 1px solid #dae1e3;
font-size: 1rem;
line-height: 1.5em;
font-weight: 100;
display: block;
user-select: text;
border-radius: 4px;
transition: border-color .15s linear;
-webkit-appearance: none;
color: #4b5b62;
-webkit-user-select: text;
}
.sign-in-wrap input:focus {
border-color: #c4c8cb;
outline: 0
}
.sign-in-wrap .login-button {
padding: 8px 10px;
cursor: pointer;
background: #33b1ff;
border: .1rem solid #33b1ff;
border-radius: .2rem;
color: #fff;
}
.red {
color: #ee1000;
}

@ -0,0 +1,79 @@
'use strict';
const router = require('express').Router();
const AV = require('leanengine');
const mail = require('../utilities/send-mail');
const spam = require('../utilities/check-spam');
const Comment = AV.Object.extend('Comment');
// Comment 列表
router.get('/', function (req, res, next) {
if (req.currentUser) {
let query = new AV.Query(Comment);
query.descending('createdAt');
query.limit(50);
query.find().then(function (results) {
res.render('comments', {
title: process.env.SITE_NAME + '上的评论',
comment_list: results
});
}, function (err) {
if (err.code === 101) {
res.render('comments', {
title: process.env.SITE_NAME + '上的评论',
comment_list: []
});
} else {
next(err);
}
}).catch(next);
} else {
res.redirect('/login');
}
});
router.get('/not-spam', function (req, res, next) {
if (req.currentUser) {
let query = new AV.Query(Comment);
query.get(req.query.id).then(function (object) {
object.set('isSpam', false);
object.save();
spam.submitHam(object);
res.redirect('/comments')
}, function (err) {
}).catch(next);
} else {
res.redirect('/login');
}
});
router.get('/mark-spam', function (req, res, next) {
if (req.currentUser) {
let query = new AV.Query(Comment);
query.get(req.query.id).then(function (object) {
object.set('isSpam', true);
object.save();
spam.submitSpam(object);
res.redirect('/comments')
}, function (err) {
}).catch(next);
} else {
res.redirect('/');
}
});
router.get('/resend-email', function (req, res, next) {
let query = new AV.Query(Comment);
query.get(req.query.id).then(function (object) {
query.get(object.get('rid')).then(function (parent) {
mail.send(object, parent);
res.redirect('/comments')
}, function (err) {
}
).catch(next);
}, function (err) {
}).catch(next);
});
module.exports = router;

@ -1,40 +0,0 @@
'use strict';
var router = require('express').Router();
var AV = require('leanengine');
var Todo = AV.Object.extend('Todo');
// 查询 Todo 列表
router.get('/', function(req, res, next) {
var query = new AV.Query(Todo);
query.descending('createdAt');
query.find().then(function(results) {
res.render('todos', {
title: 'TODO 列表',
todos: results
});
}, function(err) {
if (err.code === 101) {
// 该错误的信息为:{ code: 101, message: 'Class or object doesn\'t exists.' },说明 Todo 数据表还未创建,所以返回空的 Todo 列表。
// 具体的错误代码详见:https://leancloud.cn/docs/error_code.html
res.render('todos', {
title: 'TODO 列表',
todos: []
});
} else {
next(err);
}
}).catch(next);
});
// 新增 Todo 项目
router.post('/', function(req, res, next) {
var content = req.body.content;
var todo = new Todo();
todo.set('content', content);
todo.save().then(function(todo) {
res.redirect('/todos');
}).catch(next);
});
module.exports = router;

@ -0,0 +1,91 @@
'use strict';
const akismet = require('akismet-api');
const akismetClient = akismet.client({
key : process.env.AKISMET_KEY,
blog : process.env.SITE_URL
});
exports.checkSpam = (comment)=> {
akismetClient.verifyKey(function(err, valid) {
if (err) console.log('Akismet key 异常:', err.message);
if (valid) {
let ipAddr = comment.get('ip');
console.log('评论者的IP是: ' + ipAddr);
akismetClient.checkSpam({
user_ip : ipAddr,
user_agent : comment.get('ua'),
referrer : process.env.SITE_URL, // TODO(1) 这里有缺陷
permalink : process.env.SITE_URL + comment.get('url'), // TODO(2) 这里有缺陷
comment_type : 'comment',
comment_author : comment.get('nick'),
comment_author_email : comment.get('mail'),
comment_author_url : comment.get('link'),
comment_content : comment.get('comment'),
// is_test : true // Default value is false
}, function(err, spam) {
if (err) console.log ('垃圾检测出错!');
if (spam) {
console.log('逮到一只垃圾,烧死它!用文火~');
comment.set('isSpam', true);
comment.save();
// comment.destroy();
} else {
console.log('放行~完事!\n');
}
});
}
else console.log('Akismet key 异常!');
});
};
exports.submitSpam = (comment)=> {
akismetClient.verifyKey(function(err, valid) {
if (err) console.log('Akismet key 异常:', err.message);
if (valid) {
let ipAddr = comment.get('ip');
akismetClient.submitSpam({
user_ip : ipAddr,
user_agent : comment.get('ua'),
referrer : process.env.SITE_URL,
permalink : process.env.SITE_URL + comment.get('url'),
comment_type : 'comment',
comment_author : comment.get('nick'),
comment_author_email : comment.get('mail'),
comment_author_url : comment.get('link'),
comment_content : comment.get('comment'),
// is_test : true // Default value is false
}, function(err) {
if (!err) {
console.log('垃圾评论已经提交!');
}
});
}
else console.log('Akismet key 异常!');
});
};
exports.submitHam = (comment)=> {
akismetClient.verifyKey(function(err, valid) {
if (err) console.log('Akismet key 异常:', err.message);
if (valid) {
let ipAddr = comment.get('ip');
akismetClient.submitHam({
user_ip : ipAddr,
user_agent : comment.get('ua'),
referrer : process.env.SITE_URL,
permalink : process.env.SITE_URL + comment.get('url'),
comment_type : 'comment',
comment_author : comment.get('nick'),
comment_author_email : comment.get('mail'),
comment_author_url : comment.get('link'),
comment_content : comment.get('comment'),
// is_test : true // Default value is false
}, function(err) {
if (!err) {
console.log('评论已经标记为非垃圾!');
}
});
}
else console.log('Akismet key 异常!');
});
};

@ -0,0 +1,71 @@
'use strict';
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
exports.notice = (comment) => {
let emailSubject = '👉 咚!「' + process.env.SITE_NAME + '」上有新评论了';
let emailContent = '<p>「' + process.env.SITE_NAME + '」上 '
+ comment.get('nick')
+' 留下了新评论,内容如下:</p>'
+ comment.get('comment')
+ '<br><p> <a href="'
+ process.env.SITE_URL
+ comment.get('url')
+ '">点击前往查看</a>';
let mailOptions = {
from: '"' + process.env.SENDER_NAME + '" <' + process.env.SENDER_EMAIL + '>',
to: process.env.SENDER_EMAIL,
subject: emailSubject,
html: emailContent
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.log(error);
}
});
}
exports.send = (currentComment, parentComment)=> {
let emailSubject = '👉 叮咚!「' + process.env.SITE_NAME + '」上有人@了你';
let emailContent = '<span style="font-size:16px;color:#212121">Hi,'
+ parentComment.get('nick')
+ '</span>'
+ '<p>「' + process.env.SITE_NAME + '」上 '
+ currentComment.get('nick')
+' @了你,回复内容如下:</p>'
+ currentComment.get('comment')
+ '<br><p>原评论内容为:'
+ parentComment.get('comment')
+ '</p><p> <a href="'
+ process.env.SITE_URL
+ currentComment.get('url')
+ '">点击前往查看</a> <br><p><a href="'
+ process.env.SITE_URL + '">'
+ process.env.SITE_NAME + ' </a>欢迎你的再度光临</p>';
let mailOptions = {
from: '"' + process.env.SENDER_NAME + '" <' + process.env.SENDER_EMAIL + '>', // sender address
to: parentComment.get('mail'),
subject: emailSubject,
html: emailContent
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.log(error);
}
console.log('邮件 %s 成功发送: %s', info.messageId, info.response);
currentComment.set('isNotified', true);
currentComment.save();
});
};

@ -0,0 +1,50 @@
<!DOCTYPE HTML>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
<div class="comment-main">
<div class="header">
<div class="title"><span><%= title %></span></div>
<div class="logout-wrapper"><a href="/logout">退出登录</a></div>
</div>
<ul class="content">
<% for(var i = 0; i < comment_list.length; i++) { %>
<li class="vcard">
<div class="vhead">
<a href="<%= comment_list[i].get('link') %>" target="_blank"
rel="nofollow"> <%= comment_list[i].get('nick') %></a>
<% var date = comment_list[i].get('createdAt') %>
<span class="spacer">•</span><span class="vtime"><%= dateFormat(date) %></span>
</div>
<div class="vcomment">
<%- comment_list[i].get('comment') %>
</div>
<div class="check">
<a class="red" href="/comments/delete?id=<%= comment_list[i].get('objectId') %>" rel="nofollow">删除</a><span class="spacer">•</span>
<% if(comment_list[i].get('isSpam')) { %>
<a href="/comments/not-spam?id=<%= comment_list[i].get('objectId') %>" rel="nofollow">这不是垃圾评论</a>
<% } else { %>
<a href="/comments/mark-spam?id=<%= comment_list[i].get('objectId') %>" rel="nofollow">标记为垃圾评论</a>
<% } %>
<% if (comment_list[i].get('rid')) { %>
<% if(comment_list[i].get('isNotified')) { %>
<span class="spacer">•</span><span class="vtime">通知已送达</span>
<% } else { %>
<span class="spacer">•</span>
<a href="/comments/resend-email?id=<%= comment_list[i].get('objectId') %>" rel="nofollow">重发通知邮件</a>
<% } %>
<% } %>
</div>
</li>
<% } %>
</ul>
</div>
</body>
</html>

@ -1,13 +1,21 @@
<!DOCTYPE HTML>
<html>
<head>
<title>LeanEngine</title>
<head>
<title>LeanCloud评论管理</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
<h1>LeanEngine</h1>
<p>这是 LeanEngine 的示例应用</p>
<p>当前时间:<%= currentTime %></p>
<p><a href="/todos">一个简单的「TODO 列表」示例</a></p>
</body>
</head>
<body>
<div class="sign-in-wrap">
<p class="title">LeanCloud评论管理</p>
<form method="post" action="/login" class="gh-signin">
<input tabindex="1" name="username"
placeholder="用户名" autofocus="" type="text" class="gh-input">
<input tabindex="2" name="password" placeholder="密码" type="password" class="password gh-input ember-view">
<input tabindex="3" type="submit" class="login-button" value="登录">
</form>
<div class="footer"><p><a href="https://panjunwen.com">By Deserts</a><p></div>
</div>
</body>
</html>

@ -1,19 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Todo</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
<h1><%= title %></h1>
<form action="/todos" method="POST">
<input type="text" name="content" />
<input type="submit" value="新增" />
</form>
<ul>
<% for(var i=0; i<todos.length; i++) {%>
<li><%= todos[i].get('content') %></li>
<% } %>
</ul>
</body>
</html>
Loading…
Cancel
Save