使用pg数据库重构
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import MySQLConnection from '../Plugs/MySQLConnection'
|
||||
import Database from '../Plugs/Database'
|
||||
import MountUserAgent from "../Plugs/Middleware/MountUserAgent";
|
||||
import axios from "axios";
|
||||
import MountIP from "../Plugs/Middleware/MountIP";
|
||||
import CheckCaptchaPassed from "../Plugs/Middleware/CheckCaptchaPassed";
|
||||
import { BlogComment as BlogCommentType } from "@/Types/Schema"
|
||||
|
||||
// 提交博客评论
|
||||
class BlogComment extends API {
|
||||
constructor() {
|
||||
super('POST', '/blogComment', CheckCaptchaPassed, MountUserAgent, MountIP);
|
||||
super('POST', '/blogComment', MountUserAgent, MountIP);
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
@@ -26,17 +26,17 @@ class BlogComment extends API {
|
||||
// 获取IPAddress
|
||||
let ip_address = '未知'
|
||||
try {
|
||||
let ipAddressRes = await axios.get(`https://mesh.if.iqiyi.com/aid/ip/info?version=1.1.1&ip=`+_ip);
|
||||
let ipAddressRes = await axios.get(`https://mesh.if.iqiyi.com/aid/ip/info?version=1.1.1&ip=` + _ip);
|
||||
if (ipAddressRes.data && ipAddressRes.data.msg == 'success') {
|
||||
|
||||
|
||||
ip_address = ipAddressRes.data.data.countryCN == '中国' ? ipAddressRes.data.data.provinceCN : ipAddressRes.data.data.countryCN;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('获取IP属地失败', error);
|
||||
}
|
||||
|
||||
let blogLikeRes = await MySQLConnection.execute('INSERT INTO blog_comment (uuid, content, name, ip, ip_address, user_agent, time) VALUES (?,?,?,?,?,?,?)', [bloguuid, content.trim(), name.trim(), _ip, ip_address, _userAgent, Date.now()]);
|
||||
if (!blogLikeRes || blogLikeRes.affectedRows != 1) {
|
||||
let blogLikeRes = await Database.query<BlogCommentType>('INSERT INTO blog_comment (uuid, content, name, ip, ip_address, user_agent, display, created_at) VALUES ($1,$2,$3,$4,$5,$6,true,$7)', [bloguuid, content.trim(), name.trim(), _ip, ip_address, _userAgent, new Date()]);
|
||||
if (!blogLikeRes) {
|
||||
this.logger.error('发布博客评论时,数据库发生错误');
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import MySQLConnection from '../Plugs/MySQLConnection'
|
||||
import Database from '../Plugs/Database'
|
||||
import { Buffer } from 'buffer';
|
||||
import axios from "axios";
|
||||
import { Blog } from "@/Types/Schema";
|
||||
|
||||
|
||||
// 点赞
|
||||
@@ -18,15 +19,11 @@ class BlogLike extends API {
|
||||
return res.json(ServerStdResponse.INVALID_PARAMS);
|
||||
}
|
||||
|
||||
let blogLikeRes = await MySQLConnection.execute('UPDATE blog SET like_count = like_count + 1 WHERE access_level > ? AND uuid = ? ', [this.defaultAccessLevel, bloguuid]);
|
||||
let blogLikeRes = await Database.query<Blog>('UPDATE blog SET like_count = like_count + 1 WHERE access_level > $1 AND uuid = $2 ', [this.defaultAccessLevel, bloguuid]);
|
||||
if (!blogLikeRes) {
|
||||
this.logger.error('点赞博客时,数据库发生错误');
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
if (blogLikeRes.affectedRows != 1) {
|
||||
this.logger.warn('查询的博客不存在或不可见', bloguuid);
|
||||
return res.json(ServerStdResponse.BLOG.NOTFOUND);
|
||||
}
|
||||
return res.json(ServerStdResponse.OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import captchaSession from "../Plugs/Service/captchaSession";
|
||||
|
||||
// 检查人机验证
|
||||
class CheckCaptcha extends API {
|
||||
constructor() {
|
||||
super('POST', '/checkCaptcha');
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
let { session, rotateDeg } = data;
|
||||
if (!session || !rotateDeg) {
|
||||
return res.json(ServerStdResponse.PARAMS_MISSING);
|
||||
}
|
||||
switch (await captchaSession.check(session, rotateDeg)) {
|
||||
case 0:
|
||||
// 验证码已过期或服务器错误
|
||||
res.json(ServerStdResponse.CAPTCHA.NOTFOUND);
|
||||
break;
|
||||
case 1:
|
||||
// 验证通过
|
||||
res.json(ServerStdResponse.OK);
|
||||
break;
|
||||
case -1:
|
||||
// 超过最大尝试次数
|
||||
res.json(ServerStdResponse.CAPTCHA.MAX_TRY_COUNT);
|
||||
break;
|
||||
case -2:
|
||||
// 角度不正确
|
||||
res.json(ServerStdResponse.CAPTCHA.NOTRIGHT);
|
||||
break;
|
||||
default:
|
||||
// 未知错误
|
||||
res.json(ServerStdResponse.SERVER_ERROR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckCaptcha;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { API } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import Auth from "../../Plugs/Middleware/Auth";
|
||||
|
||||
// 删除博客
|
||||
@@ -10,13 +10,13 @@ class DelBlog extends API {
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
let { id } = data;
|
||||
if (!id) {
|
||||
let { uuid } = data;
|
||||
if (!uuid) {
|
||||
return res.json(ServerStdResponse.PARAMS_MISSING);
|
||||
}
|
||||
let execRes = await MySQLConnection.execute('DELETE FROM blog WHERE `id` = ?', [id]);
|
||||
let execRes = await Database.query('DELETE FROM blog WHERE uuid = $1', [uuid]);
|
||||
|
||||
if (!execRes || execRes.affectedRows != 1) {
|
||||
if (!execRes) {
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
return res.json({ ...ServerStdResponse.OK });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { API } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import Auth from "../../Plugs/Middleware/Auth";
|
||||
|
||||
// 删除资源
|
||||
@@ -10,13 +10,13 @@ class DelResource extends API {
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
let { id } = data;
|
||||
if (!id) {
|
||||
let { uuid } = data;
|
||||
if (!uuid) {
|
||||
return res.json(ServerStdResponse.PARAMS_MISSING);
|
||||
}
|
||||
let execRes = await MySQLConnection.execute('DELETE FROM resource WHERE `id` = ?', [id]);
|
||||
let execRes = await Database.query('DELETE FROM resource WHERE uuid = $1', [uuid]);
|
||||
|
||||
if (!execRes || execRes.affectedRows != 1) {
|
||||
if (!execRes) {
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
return res.json({ ...ServerStdResponse.OK });
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { API } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import Auth from "../../Plugs/Middleware/Auth";
|
||||
import { Blog } from "@/Types/Schema";
|
||||
|
||||
// 获取博客列表
|
||||
class GetBlogs extends API {
|
||||
@@ -10,8 +11,7 @@ class GetBlogs extends API {
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
// const { uuid } = data._jwt;
|
||||
let resourcesRes = await MySQLConnection.execute("SELECT * FROM blog ORDER BY id DESC");
|
||||
let resourcesRes = await Database.query<Blog>("SELECT * FROM blog ORDER BY created_at DESC");
|
||||
if (!resourcesRes) {
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { API, RequestData } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import Auth from "../../Plugs/Middleware/Auth";
|
||||
import jwt from "jsonwebtoken";
|
||||
import config from "../../config";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { API } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import Auth from "../../Plugs/Middleware/Auth";
|
||||
import { Resource } from "@/Types/Schema";
|
||||
|
||||
// 获取资源列表
|
||||
class GetResources extends API {
|
||||
@@ -11,7 +12,7 @@ class GetResources extends API {
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
// const { uuid } = data._jwt;
|
||||
let resourcesRes = await MySQLConnection.execute("SELECT * FROM resource");
|
||||
let resourcesRes = await Database.query<Resource>("SELECT * FROM resource ORDER BY type, recommand, created_at DESC");
|
||||
if (!resourcesRes) {
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { API } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import MountUserAgent from "../../Plugs/Middleware/MountUserAgent";
|
||||
import MountIP from "../../Plugs/Middleware/MountIP";
|
||||
import CheckCaptchaPassed from "../../Plugs/Middleware/CheckCaptchaPassed";
|
||||
import config from "../../config";
|
||||
import jwt from 'jsonwebtoken'
|
||||
import crypto from 'crypto'
|
||||
import { User } from "@/Types/Schema";
|
||||
|
||||
// 登录
|
||||
class Login extends API {
|
||||
constructor() {
|
||||
super('POST', '/console/login', CheckCaptchaPassed, MountUserAgent, MountIP);
|
||||
super('POST', '/console/login', MountUserAgent, MountIP);
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
@@ -21,28 +21,28 @@ class Login extends API {
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
let userInfoRes = await MySQLConnection.execute('SELECT * FROM user WHERE username = ?', [username]);
|
||||
if(!userInfoRes){
|
||||
let userInfoRes = await Database.query<User>('SELECT * FROM user WHERE username = $1', [username]);
|
||||
if (!userInfoRes) {
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
if (userInfoRes.length != 1) {
|
||||
return res.json(ServerStdResponse.USER.NOTFOUND);
|
||||
}
|
||||
userInfoRes = userInfoRes[0];
|
||||
const UserInfo = userInfoRes[0];
|
||||
// 检查密码是否正确
|
||||
if(crypto.createHash('sha256').update(`${userInfoRes.salt}${password}`).digest('hex') != userInfoRes.password){
|
||||
if (crypto.createHash('sha256').update(`${UserInfo.salt}${password}`).digest('hex') != UserInfo.password) {
|
||||
return res.json(ServerStdResponse.USER.PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
// 准备jwtToken
|
||||
const jwtPayload = {
|
||||
uuid: userInfoRes.uuid,
|
||||
uuid: UserInfo.uuid,
|
||||
loginTime: Date.now()
|
||||
}
|
||||
let jwtToken = jwt.sign(jwtPayload, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
|
||||
|
||||
// 写入登录日志
|
||||
MySQLConnection.execute('INSERT INTO user_login_log (user_uuid, ip, user_agent, time) VALUES (?,?,?,?)', [userInfoRes.uuid, _ip, _userAgent, Date.now()]);
|
||||
Database.query('INSERT INTO user_login_log (user_uuid, ip, user_agent, time) VALUES ($1,$2,$3,$4)', [UserInfo.uuid, _ip, _userAgent, Date.now()]);
|
||||
return res.json({ ...ServerStdResponse.OK, data: { token: jwtToken } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { API } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import Auth from "../../Plugs/Middleware/Auth";
|
||||
import crypto from 'crypto'
|
||||
import { Blog } from "@/Types/Schema";
|
||||
|
||||
// 保存博客
|
||||
class SaveBlog extends API {
|
||||
@@ -11,21 +12,21 @@ class SaveBlog extends API {
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
let { id, uuid, title, description, publish_time, src, access_level } = data;
|
||||
if (!title || !description || !publish_time || !src || !access_level) {
|
||||
let { uuid, title, description, created_at, src, access_level } = data;
|
||||
if (!title || !description || !created_at || !src || !access_level) {
|
||||
return res.json(ServerStdResponse.PARAMS_MISSING);
|
||||
}
|
||||
let execRes: any;
|
||||
if (id) {
|
||||
if (uuid) {
|
||||
// 保存
|
||||
execRes = await MySQLConnection.execute('UPDATE blog SET title = ?, description = ?, publish_time = ?, src = ?, access_level = ? WHERE `id` = ?', [title, description, publish_time, src, access_level, id]);
|
||||
execRes = await Database.query<Blog>('UPDATE blog SET title = $1, description = $2, created_at = $3, src = $4, access_level = $5 WHERE uuid = $6', [title, description, created_at, src, access_level, uuid]);
|
||||
} else {
|
||||
// 新建
|
||||
const uuid = crypto.createHash('md5').update(`${Math.random()}${Date.now()}`).digest('hex');
|
||||
execRes = await MySQLConnection.execute('INSERT INTO blog (uuid, title, description, src, publish_time, access_level, visit_count, like_count) VALUES (?,?,?,?,?,?,?,?)', [uuid, title, description, src, publish_time, access_level, 0, 0]);
|
||||
execRes = await Database.query<Blog>('INSERT INTO blog (uuid, title, description, src, created_at, access_level, visit_count, like_count) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)', [uuid, title, description, src, created_at, access_level, 0, 0]);
|
||||
}
|
||||
|
||||
if (!execRes || execRes.affectedRows != 1) {
|
||||
if (!execRes) {
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
return res.json({ ...ServerStdResponse.OK });
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { API } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import Auth from "../../Plugs/Middleware/Auth";
|
||||
import { Resource } from "@/Types/Schema";
|
||||
import Crypto from 'crypto'
|
||||
|
||||
// 保存资源
|
||||
class SaveResource extends API {
|
||||
@@ -10,20 +12,21 @@ class SaveResource extends API {
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
let { id, type, recommand, title, describe, icon_src, addition, src } = data;
|
||||
let { uuid, type, recommand, title, describe, icon_src, addition, src } = data;
|
||||
if (!type || !recommand || !title || !describe || !icon_src || !addition || !src) {
|
||||
return res.json(ServerStdResponse.PARAMS_MISSING);
|
||||
}
|
||||
let execRes: any;
|
||||
if (id) {
|
||||
if (uuid) {
|
||||
// 保存
|
||||
execRes = await MySQLConnection.execute('UPDATE resource SET `type` = ?, `recommand` = ?, `title` = ?, `describe` = ?, `addition` = ?, `icon_src` = ?, `src` = ? WHERE `id` = ?', [type, recommand, title, describe, addition, icon_src, src, id]);
|
||||
execRes = await Database.query<Resource>('UPDATE resource SET "type" = $1, "recommand" = $2, "title" = $3, "describe" = $4, "addition" = $5, "icon_src" = $6, "src" = $7 WHERE "uuid" = $8', [type, recommand, title, describe, addition, icon_src, src, uuid]);
|
||||
} else {
|
||||
// 新建
|
||||
execRes = await MySQLConnection.execute('INSERT INTO resource (`type`, `recommand`, `title`, `describe`, `addition`, `icon_src`, `src`) VALUES (?,?,?,?,?,?,?)', [type, recommand, title, describe, addition, icon_src, src]);
|
||||
uuid = Crypto.createHash('md5').update(`${Math.random()}${Date.now()}`).digest('hex');
|
||||
execRes = await Database.query<Resource>('INSERT INTO resource ("uuid","type", "recommand", "title", "describe", "addition", "icon_src", "src", "created_at") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)', [uuid, type, recommand, title, describe, addition, icon_src, src, new Date()]);
|
||||
}
|
||||
|
||||
if (!execRes || execRes.affectedRows != 1) {
|
||||
if (!execRes) {
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
return res.json({ ...ServerStdResponse.OK });
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { API } from "../../Plugs/API/API";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import MySQLConnection from '../../Plugs/MySQLConnection'
|
||||
import Database from '../../Plugs/Database'
|
||||
import Auth from "../../Plugs/Middleware/Auth";
|
||||
import crypto from 'crypto'
|
||||
import { Blog } from "@/Types/Schema";
|
||||
|
||||
// 设置博客密码
|
||||
class SetBlogPasswd extends API {
|
||||
@@ -16,7 +17,7 @@ class SetBlogPasswd extends API {
|
||||
return res.json(ServerStdResponse.PARAMS_MISSING);
|
||||
}
|
||||
const encrypt_p = crypto.createHash('sha256').update(passwd).digest('hex');
|
||||
MySQLConnection.execute('UPDATE blog SET encrypt_p = ? WHERE uuid = ?', [encrypt_p, uuid]);
|
||||
Database.query<Blog>('UPDATE blog SET encrypt_p = $1 WHERE uuid = $2', [encrypt_p, uuid]);
|
||||
return res.json({ ...ServerStdResponse.OK });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import MySQLConnection from '../Plugs/MySQLConnection'
|
||||
import Database from '../Plugs/Database'
|
||||
import { BlogComment } from "@/Types/Schema";
|
||||
|
||||
// 获取博客评论
|
||||
class GetBlogComment extends API {
|
||||
@@ -16,7 +17,7 @@ class GetBlogComment extends API {
|
||||
return res.json(ServerStdResponse.INVALID_PARAMS);
|
||||
}
|
||||
|
||||
let blogCommentRes = await MySQLConnection.execute('SELECT content, name, ip_address, time FROM blog_comment WHERE uuid = ? AND display = 1 ORDER BY time DESC LIMIT ? OFFSET ?;', [bloguuid, this.pageSize, (page - 1) * this.pageSize]);
|
||||
let blogCommentRes = await Database.query<BlogComment>('SELECT content, name, ip_address, created_at FROM blog_comment WHERE uuid = $1 AND display = true ORDER BY created_at DESC LIMIT $2 OFFSET $3;', [bloguuid, this.pageSize, (page - 1) * this.pageSize]);
|
||||
if (!blogCommentRes) {
|
||||
this.logger.error('获取博客评论时,数据库发生错误');
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import MySQLConnection from '../Plugs/MySQLConnection'
|
||||
import Database from '../Plugs/Database'
|
||||
import { Buffer } from 'buffer';
|
||||
import axios from "axios";
|
||||
import crypto from 'crypto'
|
||||
import MountIP from "../Plugs/Middleware/MountIP";
|
||||
import { Blog } from "@/Types/Schema";
|
||||
|
||||
// 获取博客内容
|
||||
class GetBlogContent extends API {
|
||||
@@ -22,14 +23,14 @@ class GetBlogContent extends API {
|
||||
return res.json(ServerStdResponse.INVALID_PARAMS);
|
||||
}
|
||||
|
||||
let blogContentRes = await MySQLConnection.execute(`SELECT * from blog WHERE access_level in (${this.AccessLevelRule.allow.join(',')}) AND uuid = ? `, [bloguuid]);
|
||||
let blogContentRes = await Database.query<Blog>(`SELECT * from blog WHERE access_level in (${this.AccessLevelRule.allow.join(',')}) AND uuid = $1 `, [bloguuid]);
|
||||
if (!blogContentRes) {
|
||||
this.logger.error('查询时数据库发生错误');
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
if (blogContentRes.length == 0) {
|
||||
// 公开范围不可见,查询允许无连接加密查看的数据
|
||||
blogContentRes = await MySQLConnection.execute(`SELECT * from blog WHERE access_level in (${this.AccessLevelRule.encrypt_allow.join(',')}) AND uuid = ? `, [bloguuid]);
|
||||
blogContentRes = await Database.query<Blog>(`SELECT * from blog WHERE access_level in (${this.AccessLevelRule.encrypt_allow.join(',')}) AND uuid = $1 `, [bloguuid]);
|
||||
if (!blogContentRes) {
|
||||
this.logger.error('查询时数据库发生错误');
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
@@ -57,14 +58,14 @@ class GetBlogContent extends API {
|
||||
const base64Content = Buffer.from(response.data, 'utf-8').toString('base64');
|
||||
|
||||
// 访问次数+1
|
||||
MySQLConnection.execute('UPDATE blog SET visit_count = visit_count + 1 WHERE uuid = ?', [bloguuid]);
|
||||
Database.query('UPDATE blog SET visit_count = visit_count + 1 WHERE uuid = $1', [bloguuid]);
|
||||
return res.json({
|
||||
...ServerStdResponse.OK, data: {
|
||||
data: base64Content,
|
||||
info: {
|
||||
title: blogContentRes[0].title,
|
||||
description: blogContentRes[0].description,
|
||||
publish_time: blogContentRes[0].publish_time,
|
||||
publish_time: blogContentRes[0].created_at,
|
||||
visit_count: blogContentRes[0].visit_count,
|
||||
like_count: blogContentRes[0].like_count
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import MySQLConnection from '../Plugs/MySQLConnection'
|
||||
import Database from '../Plugs/Database'
|
||||
import { Blog } from "@/Types/Schema";
|
||||
|
||||
// 获取博客列表
|
||||
class GetBlogList extends API {
|
||||
@@ -10,12 +11,12 @@ class GetBlogList extends API {
|
||||
private defaultAccessLevel = 9;
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
let blogListRes = await MySQLConnection.execute('SELECT uuid, title, description, publish_time, access_level, visit_count, like_count from blog WHERE access_level >= ? ORDER BY publish_time DESC',[this.defaultAccessLevel]);
|
||||
if(!blogListRes){
|
||||
let blogListRes = await Database.query<Blog>('SELECT uuid, title, description, created_at, access_level, visit_count, like_count from blog WHERE access_level >= $1 ORDER BY created_at DESC', [this.defaultAccessLevel]);
|
||||
if (!blogListRes) {
|
||||
this.logger.error('查询时数据库发生错误');
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
return res.json({...ServerStdResponse.OK, data: blogListRes});
|
||||
return res.json({ ...ServerStdResponse.OK, data: blogListRes });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import fs from "fs";
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import captchaSession from "../Plugs/Service/captchaSession";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
import crypto from 'crypto'
|
||||
|
||||
// 获取人机验证图片及标识符
|
||||
class GetCaptcha extends API {
|
||||
constructor() {
|
||||
super('GET', '/captcha');
|
||||
}
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
const imgsPath = path.join(__dirname, '../assets/captchaImgs');
|
||||
const fileList = fs.readdirSync(imgsPath)
|
||||
const imgPath = path.join(imgsPath, fileList[Math.floor(Math.random() * fileList.length)]);
|
||||
const rotateDeg = Math.floor(Math.random() * 240) + 60;
|
||||
const img = Buffer.from(await sharp(imgPath).rotate(-rotateDeg).toBuffer()).toString('base64')
|
||||
const session = crypto.createHash('md5').update(`${Math.random()} ${Date.now()}`).digest('hex');
|
||||
if (await captchaSession.add(session, rotateDeg)) {
|
||||
return res.json({
|
||||
...ServerStdResponse.OK, data: {
|
||||
img: img,
|
||||
session: session,
|
||||
imgPreStr: 'data:image/jpeg;base64,'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return res.json(ServerStdResponse.SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GetCaptcha;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import MySQLConnection from '../Plugs/MySQLConnection'
|
||||
import Database from '../Plugs/Database'
|
||||
import { Resource } from "@/Types/Schema";
|
||||
|
||||
// 获取资源列表
|
||||
class GetResourceList extends API {
|
||||
@@ -11,18 +12,18 @@ class GetResourceList extends API {
|
||||
|
||||
public async onRequset(data: any, res: any) {
|
||||
let { type } = data;
|
||||
if(!type){
|
||||
if (!type) {
|
||||
return res.json(ServerStdResponse.PARAMS_MISSING);
|
||||
}
|
||||
if(!this.typeList.includes(type)){
|
||||
if (!this.typeList.includes(type)) {
|
||||
return res.json(ServerStdResponse.INVALID_PARAMS);
|
||||
}
|
||||
let resourceListRes = await MySQLConnection.execute('SELECT * from resource WHERE type = ? ORDER BY recommand ASC',[type]);
|
||||
if(!resourceListRes){
|
||||
let resourceListRes = await Database.query<Resource>('SELECT * from resource WHERE type = $1 ORDER BY recommand ASC', [type]);
|
||||
if (!resourceListRes) {
|
||||
this.logger.error('查询时数据库发生错误');
|
||||
return res.json(ServerStdResponse.SERVER_ERROR);
|
||||
}
|
||||
return res.json({...ServerStdResponse.OK, data: resourceListRes});
|
||||
return res.json({ ...ServerStdResponse.OK, data: resourceListRes });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { API } from "../Plugs/API/API";
|
||||
import ServerStdResponse from "../ServerStdResponse";
|
||||
import MySQLConnection from '../Plugs/MySQLConnection'
|
||||
|
||||
// 测试接口
|
||||
class GetTest extends API {
|
||||
|
||||
78
Server/src/Plugs/Database.ts
Normal file
78
Server/src/Plugs/Database.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @file Database.ts
|
||||
* @version 1.0.0
|
||||
* @description PostgreSQL数据库连接池
|
||||
*/
|
||||
import pg from 'pg';
|
||||
import Logger from "./Logger";
|
||||
import config from "../config";
|
||||
|
||||
class DatabasePool {
|
||||
private pool: pg.Pool;
|
||||
private logger = new Logger('Database');
|
||||
|
||||
constructor() {
|
||||
this.pool = this.createConnectPool();
|
||||
this.logger.info("数据库连接池已创建")
|
||||
|
||||
this.pool.on('error', (err) => {
|
||||
this.logger.error(`数据库连接池发生错误:${err}`);
|
||||
});
|
||||
|
||||
this.testConnection();
|
||||
}
|
||||
|
||||
private createConnectPool() {
|
||||
return new pg.Pool({
|
||||
host: config.pg.host,
|
||||
database: config.pg.database,
|
||||
user: config.pg.user,
|
||||
password: config.pg.password,
|
||||
connectionTimeoutMillis: 5000, // 连接超时时间
|
||||
max: 20, // 最大连接数
|
||||
idleTimeoutMillis: 30000, // 空闲连接超时时间
|
||||
allowExitOnIdle: true // 允许空闲时退出
|
||||
})
|
||||
}
|
||||
|
||||
private async testConnection() {
|
||||
let res = await this.query<{ result: number }>("SELECT 1 + 1 As result");
|
||||
if (!res) {
|
||||
this.logger.error("数据库连接测试失败");
|
||||
return;
|
||||
}
|
||||
if (res[0].result == 2)
|
||||
return;
|
||||
this.logger.error("数据库连接测试失败?");
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL查询
|
||||
* @param sql SQL语句
|
||||
* @param values 可选的查询参数列表
|
||||
* @param database 可选的数据库
|
||||
* @returns Promise<any | undefined> 查询结果
|
||||
*/
|
||||
public async query<T>(sql: string, values?: any[]): Promise<T[] | undefined> {
|
||||
let connection;
|
||||
try {
|
||||
connection = await this.pool.connect();
|
||||
const res = await connection.query(sql, values);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
if (!connection) {
|
||||
this.logger.error(`数据库连接失败:${error}`);
|
||||
} else {
|
||||
this.logger.error(`数据库查询发生错误:${error}`);
|
||||
}
|
||||
return undefined;
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Database = new DatabasePool();
|
||||
export default Database;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import ServerStdResponse from "../../ServerStdResponse";
|
||||
import Logger from "../Logger";
|
||||
import captchaSession from "../Service/captchaSession";
|
||||
const logger = new Logger("CheckCaptcha");
|
||||
const CheckCaptchaPassed = async (req: Request, res: Response, next: NextFunction) => {
|
||||
let session = req.query.session || req.body.session || '';
|
||||
if (session) {
|
||||
if (await captchaSession.isPassed(session))
|
||||
return next();
|
||||
}
|
||||
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ip;
|
||||
logger.info(`API[${req.method}][${req.url.split('?')[0]}] 请求人机验证未通过[${ip}]`);
|
||||
res.json(ServerStdResponse.AUTH_ERROR);
|
||||
}
|
||||
|
||||
export default CheckCaptchaPassed;
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* @file MySQLConnection.ts
|
||||
* @version 1.0.0
|
||||
* @description MySQL数据库连接池
|
||||
*/
|
||||
import mysql from "mysql2/promise";
|
||||
import Logger from "./Logger";
|
||||
import config from "../config";
|
||||
|
||||
class MySQLConnectPool {
|
||||
private pool: any;
|
||||
private logger = new Logger('MySQLConnection');
|
||||
|
||||
constructor() {
|
||||
this.pool = this.createConnectPool();
|
||||
this.logger.info("数据库连接池已创建")
|
||||
setTimeout(async () => {
|
||||
let res = await this.testConnection();
|
||||
if (res)
|
||||
this.logger.info("数据库测试成功")
|
||||
else
|
||||
this.logger.error("数据库测试失败")
|
||||
}, 10);
|
||||
}
|
||||
|
||||
private createConnectPool() {
|
||||
return mysql.createPool({
|
||||
host: config.mysql.host,
|
||||
database: config.mysql.database,
|
||||
user: config.mysql.user,
|
||||
password: config.mysql.password,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
})
|
||||
}
|
||||
|
||||
private async testConnection() {
|
||||
try {
|
||||
let res = await this.execute("SELECT 1 + 1 As result");
|
||||
if (res[0].result == 2)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
} catch (error) {
|
||||
this.logger.error(`数据库测试发生了错误:` + error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL查询
|
||||
* @param sql SQL语句
|
||||
* @param values 可选的查询参数列表
|
||||
* @param database 可选的数据库
|
||||
* @returns Promise<any | undefined> 查询结果
|
||||
*/
|
||||
public async execute(sql: string, values?: any[], database?: string): Promise<any | undefined> {
|
||||
let connection: any;
|
||||
try {
|
||||
connection = await this.pool.getConnection();
|
||||
|
||||
// 如果指定了数据库,则更改当前连接的数据库
|
||||
if (database) {
|
||||
await connection.changeUser({ database });
|
||||
}
|
||||
|
||||
let [rows, fields] = await connection.execute(sql, values);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
this.logger.error("数据库发生错误:" + error, '\n##', sql, '\n##', JSON.stringify(values));
|
||||
return undefined;
|
||||
} finally {
|
||||
if (database)
|
||||
await connection.changeUser({ database: config.mysql.database });// 恢复默认数据库
|
||||
if (connection)
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MySQLConnection = new MySQLConnectPool();
|
||||
export default MySQLConnection;
|
||||
@@ -1,43 +0,0 @@
|
||||
import Redis from 'ioredis';
|
||||
import config from '../config';
|
||||
import Logger from './Logger';
|
||||
|
||||
class RedisConnection {
|
||||
private pool?: Redis
|
||||
private logger = new Logger('Redis')
|
||||
|
||||
constructor() {
|
||||
try {
|
||||
this.pool = new Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
password: config.redis.password,
|
||||
maxRetriesPerRequest: 10,
|
||||
});
|
||||
this.logger.info('数据库连接池已创建')
|
||||
} catch (error) {
|
||||
this.logger.error('数据库连接池创建失败:' + error)
|
||||
}
|
||||
setTimeout(async () => {
|
||||
if(this.pool == undefined)
|
||||
return;
|
||||
try {
|
||||
let res = await this.pool.set('redis_test', '1');
|
||||
if (res)
|
||||
this.logger.info('数据库测试成功')
|
||||
else
|
||||
throw new Error('返回值错误')
|
||||
} catch (error) {
|
||||
this.logger.error('数据库测试失败:' + error)
|
||||
}
|
||||
|
||||
}, 10);
|
||||
}
|
||||
|
||||
public getPool(): Redis {
|
||||
return <Redis>this.pool;
|
||||
}
|
||||
}
|
||||
|
||||
const redisConnection = new RedisConnection();
|
||||
export default redisConnection.getPool();
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* @file captchaSession.ts
|
||||
* @version 1.0.0
|
||||
* @description 旋转图像验证服务
|
||||
*/
|
||||
import Logger from '../Logger';
|
||||
import RedisConnection from '../RedisConnection';
|
||||
type CaptchaSessionDataJSON = {
|
||||
rotateDeg: number,
|
||||
tryCount: number,
|
||||
isPassed: boolean
|
||||
}
|
||||
|
||||
class _captchaSession {
|
||||
private readonly logger = new Logger('Service][captchaSession');
|
||||
private readonly AllowMaxTryCount: number = 5;
|
||||
private readonly AllowMaxAngleDiff: number = 8;
|
||||
private readonly ExpriedTimeSec: number = 60;
|
||||
private readonly RedisCommonKey: string = 'Service:captchaSession:';
|
||||
|
||||
constructor() {
|
||||
this.logger.info('旋转图像验证服务已启动');
|
||||
}
|
||||
|
||||
private async get(session: string): Promise<CaptchaSessionDataJSON | undefined> {
|
||||
try {
|
||||
const result = await RedisConnection.get(this.RedisCommonKey + session);
|
||||
if (result === null)
|
||||
return;
|
||||
return JSON.parse(result);
|
||||
} catch (error) {
|
||||
this.logger.error(`获取session[${session}]时发生错误:${error}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async remove(session: string): Promise<void> {
|
||||
try {
|
||||
await RedisConnection.del(this.RedisCommonKey + session);
|
||||
} catch (error) {
|
||||
this.logger.error(`删除session[${session}]失败`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param session 验证会话标识符
|
||||
* @param rotateDeg 图片旋转角度
|
||||
* @returns true存储成功 false存储失败
|
||||
*/
|
||||
public async add(session: string, rotateDeg: number): Promise<boolean> {
|
||||
const result: CaptchaSessionDataJSON = {
|
||||
rotateDeg: rotateDeg,
|
||||
tryCount: 0,
|
||||
isPassed: false
|
||||
}
|
||||
try {
|
||||
const res = await RedisConnection.set(this.RedisCommonKey + session, JSON.stringify(result));
|
||||
if (res && res === 'OK') {
|
||||
RedisConnection.expire(this.RedisCommonKey + session, this.ExpriedTimeSec);
|
||||
this.logger.info(`session[${session}]及角度[${rotateDeg}]已存储`);
|
||||
return true;
|
||||
}
|
||||
this.logger.error(`session[${session}]及角度[${rotateDeg}]存储失败`);
|
||||
return false;
|
||||
} catch (error) {
|
||||
this.logger.error(`session[${session}]及角度[${rotateDeg}]存储失败:${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param session 验证会话标识符
|
||||
* @returns true已验证过期 false未通过
|
||||
*/
|
||||
public async isPassed(session: string): Promise<boolean> {
|
||||
const result = await this.get(session);
|
||||
if (!result)
|
||||
return false;
|
||||
if (result.isPassed)
|
||||
this.remove(session);
|
||||
return result.isPassed;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param session 验证会话标识符
|
||||
* @param rotateDeg 图片旋转角度
|
||||
* @returns 0验证已过期或服务器错误 1通过验证 -1超过最大允许尝试次数 -2角度差异过大
|
||||
*/
|
||||
public async check(session: string, rotateDeg: number): Promise<number> {
|
||||
try {
|
||||
let result = await this.get(session);
|
||||
if (!result) {
|
||||
return 0;
|
||||
}
|
||||
if (result.isPassed) {
|
||||
this.logger.info(`session[${session}]已通过验证,无需重复验证`);
|
||||
return 1;
|
||||
}
|
||||
if (Math.abs(result.rotateDeg - rotateDeg) <= this.AllowMaxAngleDiff) {
|
||||
result.isPassed = true;
|
||||
await RedisConnection.del(this.RedisCommonKey + session);
|
||||
await RedisConnection.set(this.RedisCommonKey + session, JSON.stringify(result));
|
||||
RedisConnection.expire(this.RedisCommonKey + session, this.ExpriedTimeSec);
|
||||
return 1;
|
||||
}
|
||||
result.tryCount++;
|
||||
if (result.tryCount >= this.AllowMaxTryCount) {
|
||||
this.remove(session);
|
||||
return -1;
|
||||
}
|
||||
RedisConnection.set(this.RedisCommonKey + session, JSON.stringify(result));
|
||||
return -2;
|
||||
} catch (error) {
|
||||
this.logger.error(`检查session[${session}]时发生错误:${error}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const captchaSession = new _captchaSession();
|
||||
export default captchaSession;
|
||||
@@ -2,9 +2,6 @@ import Logger from "../Plugs/Logger";
|
||||
import { APILoader } from "../Plugs/API/APILoader";
|
||||
import config from "../config";
|
||||
|
||||
// 加载Plugs
|
||||
import '../Plugs/Service/captchaSession'
|
||||
|
||||
// 加载API
|
||||
import GetTest from "../APIs/GetTest";
|
||||
import GetResourceList from "../APIs/GetResourceList";
|
||||
@@ -13,8 +10,6 @@ import GetBlogContent from "../APIs/GetBlogContent";
|
||||
import BlogLike from "../APIs/BlogLike";
|
||||
import BlogComment from "../APIs/BlogComment";
|
||||
import GetBlogComment from "../APIs/GetBlogComment";
|
||||
import GetCaptcha from "../APIs/GetCaptcha";
|
||||
import CheckCaptcha from "../APIs/CheckCaptcha";
|
||||
|
||||
import Login from "../APIs/Console/Login";
|
||||
import GetResources from "../APIs/Console/GetResources";
|
||||
@@ -45,8 +40,6 @@ class Server {
|
||||
this.apiLoader.add(BlogLike);
|
||||
this.apiLoader.add(BlogComment);
|
||||
this.apiLoader.add(GetBlogComment);
|
||||
this.apiLoader.add(GetCaptcha);
|
||||
this.apiLoader.add(CheckCaptcha);
|
||||
|
||||
this.apiLoader.add(Login);
|
||||
this.apiLoader.add(GetResources);
|
||||
|
||||
101
Server/src/Types/Schema.ts
Normal file
101
Server/src/Types/Schema.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/** 博客文章 */
|
||||
export type Blog = {
|
||||
/** 博客uuid */
|
||||
uuid: string;
|
||||
/** 标题 */
|
||||
title: string;
|
||||
/** 描述 */
|
||||
description: string;
|
||||
/** 文章md链接 */
|
||||
src: string;
|
||||
/** 访问级别 */
|
||||
access_level: number;
|
||||
/** 访问数 */
|
||||
visit_count: number;
|
||||
/** 点赞数 */
|
||||
like_count: number;
|
||||
/** 文章加密密码 */
|
||||
encrypt_p?: string;
|
||||
/** 发布时间 */
|
||||
created_at: Date;
|
||||
};
|
||||
|
||||
/** 博客评论 */
|
||||
export type BlogComment = {
|
||||
/** 评论id */
|
||||
id: number;
|
||||
/** 文章uuid */
|
||||
uuid: string;
|
||||
/** 评论内容 */
|
||||
content: string;
|
||||
/** 昵称 */
|
||||
name: string;
|
||||
/** ip地址 */
|
||||
ip: string;
|
||||
/** ip属地 */
|
||||
ip_address: string;
|
||||
/** 用户代理 */
|
||||
user_agent: string;
|
||||
/** 是否显示 */
|
||||
display: boolean;
|
||||
/** 评论时间 */
|
||||
time: Date;
|
||||
};
|
||||
|
||||
/** 资源信息 */
|
||||
export type Resource = {
|
||||
/** 资源ID */
|
||||
uuid: string;
|
||||
/** 资源类型 */
|
||||
type: 'download' | 'resource';
|
||||
/** 推荐程度,越小越推荐 */
|
||||
recommand?: number;
|
||||
/** 标题 */
|
||||
title: string;
|
||||
/** 描述 */
|
||||
describe: string;
|
||||
/** 附加信息 */
|
||||
addition: any;
|
||||
/** 图片url */
|
||||
icon_src: string;
|
||||
/** 资源src */
|
||||
src: string;
|
||||
/** 创建时间 */
|
||||
created_at: Date;
|
||||
};
|
||||
|
||||
/** 用户信息 */
|
||||
export type User = {
|
||||
/** 用户uuid */
|
||||
uuid: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 密码salt */
|
||||
salt: string;
|
||||
/** 密码hashed */
|
||||
password: string;
|
||||
/** 手机号 */
|
||||
phone: string;
|
||||
/** 权限列表 */
|
||||
permission: any;
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 创建时间 */
|
||||
created_at: Date;
|
||||
/** 更新信息时间 */
|
||||
updated_at?: Date;
|
||||
};
|
||||
|
||||
/** 用户登录日志 */
|
||||
export type UserLoginLog = {
|
||||
/** 登陆记录id */
|
||||
id: number;
|
||||
/** 用户uuid */
|
||||
user_uuid: string;
|
||||
/** 登陆ip */
|
||||
ip: string;
|
||||
/** 用户代理 */
|
||||
user_agent: string;
|
||||
/** 登陆时间 */
|
||||
time: Date;
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 110 KiB |
Reference in New Issue
Block a user