upload avatar

This commit is contained in:
satori 2022-01-30 17:28:09 +08:00
parent b703a321ab
commit 5fc803a9b2
7 changed files with 188 additions and 96 deletions

View File

@ -39,12 +39,12 @@ pm2 start node --name kana -- index
## 特征 ## 特征
`/:name/:_id` `/:name/:_id`
RESTful 风格 API, URL形式为两段 name是对象类型, _id是对象id RESTful 风格 API, url 形式为两段 name是对象类型, _id是对象id. (与 vue 的 router 类同)
如发表一篇文章, 文章类型是 article, 文章 id 是 2333, 则 url为 `/article/2333`
如发表一篇文章,
@ -78,7 +78,7 @@ fetch('/user', {
} }
``` ```
* 创建的第一个账户默认为管理员 * 创建的第一个账户默认为管理员账户
* 可以使用管理员权限设置其他账户为管理员 * 可以使用管理员权限设置其他账户为管理员
* 默认并没有验证邮箱等检查步骤, 允许直接设置, 也允许用户重名 * 默认并没有验证邮箱等检查步骤, 允许直接设置, 也允许用户重名
@ -86,6 +86,8 @@ fetch('/user', {
#### 登录会话 #### 登录会话
登录行为被认为是创建一个终端到服务器的会话. 因此不是使用 login 或 signin, 而是 session.
```javascript ```javascript
fetch('/session', { fetch('/session', {
method: 'POST', method: 'POST',
@ -103,6 +105,8 @@ fetch('/session', {
#### 会话列表 #### 会话列表
因此, 可以查看和管理自己所有的终端会话
```javascript ```javascript
fetch('/session', { fetch('/session', {
method: 'GET', method: 'GET',
@ -120,12 +124,11 @@ fetch('/session', {
#### 注销会话 #### 注销会话
注销当前会话 相应地, 退出行为被认为是删除一个终端到服务端的会话, 因此不使用 loguot 或 signout, 而是 session.
注销当前会话
```javascript ```javascript
fetch('/session', { fetch('/session', {
method: 'DELETE', method: 'DELETE',
@ -176,6 +179,9 @@ fetch('/user/ApSXNLoUy', {
#### 上传头像 #### 上传头像
实际分为两步,
第一步先上传附件到自己的账户
```html ```html
<!DOCTYPE html> <!DOCTYPE html>
<input type="file" name="photos", multiple, onchange="upload()"/> <input type="file" name="photos", multiple, onchange="upload()"/>
@ -196,6 +202,7 @@ function upload() {
</script> </script>
``` ```
第二步修改自己的头像路径为返回的图像路径, (!注意此处未作安全检查)
```javascript ```javascript
fetch('/user/ApSXNLoUy', { fetch('/user/ApSXNLoUy', {
method: 'PATCH', method: 'PATCH',
@ -212,6 +219,7 @@ fetch('/user/ApSXNLoUy', {
#### 删除用户 #### 删除用户
管理员可以直接删除指定用户, 普通用户可以删除自己
```javascript ```javascript
fetch('/user/ApSXNLoUy', { fetch('/user/ApSXNLoUy', {
method: 'DELETE', method: 'DELETE',
@ -224,6 +232,8 @@ fetch('/user/ApSXNLoUy', {
#### 创建文章 #### 创建文章
此处 book 路径是未作限制的, 也可以是其他未被限制的路径.
```javascript ```javascript
fetch('/book', { fetch('/book', {
method: 'POST', method: 'POST',
@ -248,6 +258,11 @@ fetch('/book', {
#### 评论文章 #### 评论文章
这里假设文章类型是 book, 文章 id 是 ppNXLoUK
attach 意为附属于指定对象类型
aid 意为附属于指定对象 id
```javascript ```javascript
fetch('/post?attach=book&aid=ppNXLoUK', { fetch('/post?attach=book&aid=ppNXLoUK', {
method: 'POST', method: 'POST',
@ -265,10 +280,34 @@ fetch('/post?attach=book&aid=ppNXLoUK', {
} }
``` ```
#### 评论评论
(二级评论)
这里假设评论类型是 post, 评论 id 是 spNkjLA
受益于评论的实现结构, 也可以对二级评论继续增加三级评论, 也可以无限深度
```javascript
fetch('/post?attach=post&aid=spNkjLA', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: 'yaaaaaaaaaa~'
}),
}).then(Response => Response.json()).then(data => {
console.log(data)
})
{
_id: 'adjkasj',
data: 'ahahahha~'
}
```
#### 点赞评论 #### 点赞评论
受益于评论的实现结构, 也可以对二级评论作点赞操作, 也可以对任意对象点赞操作
```javascript ```javascript
fetch('/like?attach=post&aid=spNkjLA', { fetch('/like?attach=post&aid=spNkjLA', {
method: 'POST', method: 'POST',
@ -299,6 +338,9 @@ fetch('/like/SOAPSAdaw', {
#### 调频广播 #### 调频广播
通过频道订阅模式实现广播, 其中心思想是保障一个终端只维持一个 websocket 连接,
所有消息都通过一个连接通道发送到终端, 如聊天室频道, 系统消息, 全局广播, 消息盒子通知等
```javascript ```javascript
let socket = new WebSocket("ws://localhost:2333"); let socket = new WebSocket("ws://localhost:2333");
@ -355,7 +397,6 @@ socket.onerror = function(error) {
#### 游客广播 #### 游客广播
允许未登录会话加入订阅, 将未登录会话加入到游客账户 允许未登录会话加入订阅, 将未登录会话加入到游客账户
因此不能再通过账户登录状态进行拦截 因此不能再通过账户登录状态进行拦截

0
admin/home.js Normal file
View File

View File

@ -1,69 +1,42 @@
import interrelated from 'interrelated'
export default class { export default class {
constructor() { constructor() {
this.channels = new Map() this.用户订阅 = new interrelated()
this.users = new Map() this.用户会话 = new interrelated()
} }
订阅频道(fid, uid) { 订阅频道(fid, uid) {
console.log(`用户 ${uid} 订阅了 ${fid}`) this.用户订阅.set(fid, uid)
let channel = this.channels.get(fid) || new Map()
if (!channel.size) this.channels.set(fid, channel)
channel.set(uid, true)
} }
取消订阅(fid, uid) { 取消订阅(fid, uid) {
console.log(`用户 ${uid} 取消订阅 ${fid} 频道`) this.用户订阅.delete(fid, uid)
let channel = this.channels.get(fid)
channel.delete(uid)
// TODO: 如果此频道没有用户订阅了, 则将此频道也移除
// TODO: 如果此用户没有任何订阅了, 则将此用户也移除
} }
增加会话(uid, ws) { 增加会话(uid, ws) {
console.log(`用户 ${uid} 建立了新的会话连接`) this.用户会话.set(uid, ws)
let user = this.users.get(uid) || new Map()
if (!user.size) this.users.set(uid, user)
user.set(ws, true)
} }
移除会话(uid, ws) { 移除会话(uid, ws) {
console.log(`用户 ${uid} 结束了当前会话连接`) this.用户会话.delete(uid, ws)
let user = this.users.get(uid) || new Map()
user.delete(ws)
console.log(`此用户还剩 ${user.size} 个 ws 连接`)
if (user.size < 1) {
console.log(`由于用户 ${uid} 已经没有会话, 直接移除此用户记录`)
this.users.delete(uid)
// 理论上在会话结束后移除 (但为了避免反复遍历, 可以放在发送消息时)
this.channels.forEach((channel, fm) => {
channel.delete(uid)
})
}
} }
发送消息(fm, uid, data) { 发送消息(fm, uid, data) {
console.log("发送消息", fm, uid, data)
let msg = JSON.stringify({ fm, uid, data }) let msg = JSON.stringify({ fm, uid, data })
let channel = this.channels.get(fm) || new Map() this.用户订阅.atob(fm, (uid) => {
if (!channel.size) this.channels.set(fm, channel) //console.log(`用户 ${uid} 订阅的所有频道`)
channel.forEach((value, userid) => { this.用户会话.atob(uid, (ws) => {
//console.log(userid, value) //console.log(`用户 ${uid} 的会话`)
let user = this.users.get(userid) || new Map()
if (!user.size) {
return console.log(`订阅频道的用户 ${userid} 没有会话连接, 应移除此订阅记录`);
//console.log(user, "在这里移除可能更安全点, 因为用户可能只是断线, 立即就会重连")
//console.log("但是, 就不能清除闲置频道占用")
//return channel.delete(userid)
}
user.forEach((value, ws) => {
ws.send(msg) ws.send(msg)
}) })
}) })
//console.log(`用户 ${uid} 订阅的所有频道`)
//this.用户订阅.aall(uid, (fid) => {
// console.log(fid)
//})
//console.log(`频道 ${fm} 下的所有用户`)
//this.用户订阅.ball(fm, (uid) => {
// console.log(uid)
//})
} }
移除用户(uid) { 移除用户(uid) {
console.log(`移除 ${uid} 的所有会话`) this.用户订阅.adelete(uid)
let user = this.users.get(uid) this.用户会话.adelete(uid)
if (user) {
user.forEach((value, ws) => ws.close()) // 断开用户所有会话
this.users.delete(uid) // 删除用户(所有会话)
}
// TODO: 移除所有频道中的此用户
// TODO: 移除此用户的记录
// TODO: 断开所有此用户的连接
} }
} }

130
index.js
View File

@ -7,9 +7,7 @@ import random from 'string-random'
import formidable from 'formidable' import formidable from 'formidable'
import md5 from 'md5-node' import md5 from 'md5-node'
import HUB from './fmhub.js' import HUB from './fmhub.js'
//import site from './collection.js'
const app = expressWs(express()).app
const databases = new Map() // 所有数据库 const databases = new Map() // 所有数据库
const FM = new HUB() // 频道消息分发器 const FM = new HUB() // 频道消息分发器
@ -228,6 +226,27 @@ const object_create = async function (req, res) {
req.body.views = 0 // 再生计数 req.body.views = 0 // 再生计数
} }
// 如果包含标签
if (req.body.tags && Array.isArray(req.body.tags)) {
req.body.tags.forEach(item => {
// 先查询是否存在, 存在则使用返回的_id进行挂载, 不存在则创建新的
db('tag').findOne({ name: item }, function (err, doc) {
if (err && !doc) {
return // 创建新的
} else {
return // 使用这个 _id, 向它写入
}
})
})
// 是否可以创建一个复杂关系型数据库?
// 以应对映射的共同对象
// 例如在使用 tag 时, 向 idea 表的 tag 段读写, 即是 tag表的 idea 索引范围
// (自动构建和维护双向索引)
// 当删除此 idea 时, 也自动清理掉 tag 对 idea 的连接
}
// 如果是挂载对象到指定目标 // 如果是挂载对象到指定目标
if (req.body.attach && req.body.aid) { if (req.body.attach && req.body.aid) {
let count = await count_load(req.body.attach, { _id: req.body.aid }) let count = await count_load(req.body.attach, { _id: req.body.aid })
@ -247,6 +266,40 @@ const object_create = async function (req, res) {
}) })
} }
// 修改对象
const object_patch = function (req, res) {
return db(req.params.name).findOne({ _id: req.params._id }, function (err, doc) {
if (!doc) return res.status(404).send('目标对象不存在')
// 如果是 user 做一些特殊处理
if (req.params.name === 'user') {
if (req.session.account.uid !== doc._id && req.session.account.gid !== 1) {
return res.status(403).send('没有权限修改账户')
}
if (req.body.gid && req.session.account.gid !== 1) {
return res.status(403).send('没有权限修改权限')
}
if (req.body.password) {
req.body.salt = random(32) // 密码加盐
req.body.password = md5(req.body.password + req.body.salt) // 设置密码
}
if (req.body.name) {
// 检查用户名是否可用
}
} else {
if (req.session.account.uid !== doc.uid && req.session.account.gid !== 1) {
return res.status(403).send('没有权限修改对象')
}
if (req.body.uid && req.session.account.gid !== 1) {
return res.status(403).send('没有权限修改归属')
}
}
return db(req.params.name).update({ _id: req.params._id }, { $set: req.body }, function (err, count) {
if (!count) return res.status(500).send('修改失败')
return res.send('修改成功')
})
})
}
// 删除对象 // 删除对象
const object_remove = function (req, res) { const object_remove = function (req, res) {
return db(req.params.name).findOne({ _id: req.params._id }, async function (err, doc) { return db(req.params.name).findOne({ _id: req.params._id }, async function (err, doc) {
@ -298,40 +351,6 @@ const object_load = function (req, res) {
}) })
} }
// 修改对象
const object_patch = function (req, res) {
return db(req.params.name).findOne({ _id: req.params._id }, function (err, doc) {
if (!doc) return res.status(404).send('目标对象不存在')
// 如果是 user 做一些特殊处理
if (req.params.name === 'user') {
if (req.session.account.uid !== doc._id && req.session.account.gid !== 1) {
return res.status(403).send('没有权限修改账户')
}
if (req.body.gid && req.session.account.gid !== 1) {
return res.status(403).send('没有权限修改权限')
}
if (req.body.password) {
req.body.salt = random(32) // 密码加盐
req.body.password = md5(req.body.password + req.body.salt) // 设置密码
}
if (req.body.name) {
// 检查用户名是否可用
}
} else {
if (req.session.account.uid !== doc.uid && req.session.account.gid !== 1) {
return res.status(403).send('没有权限修改对象')
}
if (req.body.uid && req.session.account.gid !== 1) {
return res.status(403).send('没有权限修改归属')
}
}
return db(req.params.name).update({ _id: req.params._id }, { $set: req.body }, function (err, count) {
if (!count) return res.status(500).send('修改失败')
return res.send('修改成功')
})
})
}
// 附件上传 // 附件上传
const file_upload = function (req, res) { const file_upload = function (req, res) {
return db(req.params.name).findOne({ _id: req.params._id }, function (err, doc) { return db(req.params.name).findOne({ _id: req.params._id }, function (err, doc) {
@ -355,18 +374,55 @@ const file_upload = function (req, res) {
}) })
} }
// 头像上传
const uploadavatar = function (req, res) {
let idable = formidable({
multiples: true,
uploadDir: 'data/file',
keepExtensions: true,
maxFieldsSize: 200 * 1024 * 1024,
})
idable.parse(req, (err, fields, files) => {
let list = []
for (let key in files) {
(Array.isArray(files[key]) ? files[key] : [files[key]]).map((data) => {
let { filepath, mimetype, newFilename, originalFilename, size } = data
list.push({ filepath, mimetype, newFilename, originalFilename, size })
})
}
if (!list[0]) return res.status(400).send('未获得图像')
let query = { _id: req.session.account.uid }
let data = {
$addToSet: { file: { $each: list } }, // 保存记录
$set: { avatar: '/data/file/' + list[0].newFilename }, // 替换头像
}
db('user').update(query, data, (err, count) => {
if (!count) return res.status(500).send('附件挂载对象失败')
res.json(list[0]) // 返回唯一图像
})
})
}
const db_compact = function (req, res) { const db_compact = function (req, res) {
db(req.params.name).persistence.compactDatafile() db(req.params.name).persistence.compactDatafile()
return res.send("ok") return res.send("ok")
} }
const app = expressWs(express()).app
app.use(express.json()) app.use(express.json())
app.use(express.urlencoded({ extended: false })) app.use(express.urlencoded({ extended: false }))
app.use(session({ secret: 'kana', name: 'sid', resave: false, saveUninitialized: false, cookie: { maxAge: 180 * 24 * 3600000 }, store: session_store })) app.use(session({ secret: 'kana', name: 'sid', resave: false, saveUninitialized: false, cookie: { maxAge: 180 * 24 * 3600000 }, store: session_store }))
app.use('/data/file/', express.static('data/file')) app.use('/data/file/', express.static('data/file'))
app.ws('/', websocketer) app.ws('/', websocketer)
app.route('/').get((req, res) => res.send(`<DOCTYPE html><p> Hello World</p>`)) app.route('/').get((req, res) => res.send(`<DOCTYPE html><p> Hello World</p>`))
app.route('/account').get(profile) app.route('/account').get(profile).post(online, uploadavatar)
app.route('/session').get(online, session_list).post(session_create).delete(online, sessionDeleteSelf) app.route('/session').get(online, session_list).post(session_create).delete(online, sessionDeleteSelf)
app.route('/session/:sid').delete(online, session_delete) app.route('/session/:sid').delete(online, session_delete)
app.route('/:name').get(object_list).post(object_create).put(db_compact) app.route('/:name').get(object_list).post(object_create).put(db_compact)

View File

@ -12,6 +12,7 @@
"express-session-nedb": "^1.0.1", "express-session-nedb": "^1.0.1",
"express-ws": "^5.0.2", "express-ws": "^5.0.2",
"formidable": "^2.0.1", "formidable": "^2.0.1",
"interrelated": "^2.0.0-0",
"md5-node": "^1.0.1", "md5-node": "^1.0.1",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"string-random": "^0.1.3" "string-random": "^0.1.3"

View File

@ -4,3 +4,19 @@ chmod 777 start.sh
pm2 delete kana pm2 delete kana
pm2 start node --name kana -- index pm2 start node --name kana -- index
if [ ! -d "./data/" ];then
mkdir ./data
chmod 777 ./data
fi
if [ ! -d "./data/db/" ];then
mkdir ./data/db
chmod 777 ./data/db
fi
if [ ! -d "./data/file/" ];then
mkdir ./data/file
chmod 777 ./data/file
fi

View File

@ -274,6 +274,11 @@ inherits@2.0.4:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
interrelated@^2.0.0-0:
version "2.0.0-0"
resolved "https://registry.yarnpkg.com/interrelated/-/interrelated-2.0.0-0.tgz#5e7798c4035051fa5f53efbf94ea27bf491d0162"
integrity sha512-FfLlAsmLBiJRLVdIPhuAiwy6q0QpZIl7fAQTBs4WPAE1/8PdvEpA0OzThvN+3x1pt3bVPYzghTWMhzwrjlfBIw==
ipaddr.js@1.9.1: ipaddr.js@1.9.1:
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"