upload avatar
This commit is contained in:
parent
b703a321ab
commit
5fc803a9b2
61
README.md
61
README.md
@ -39,12 +39,12 @@ pm2 start node --name kana -- index
|
||||
## 特征
|
||||
|
||||
|
||||
|
||||
`/: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
|
||||
fetch('/session', {
|
||||
method: 'POST',
|
||||
@ -103,6 +105,8 @@ fetch('/session', {
|
||||
|
||||
#### 会话列表
|
||||
|
||||
因此, 可以查看和管理自己所有的终端会话
|
||||
|
||||
```javascript
|
||||
fetch('/session', {
|
||||
method: 'GET',
|
||||
@ -120,12 +124,11 @@ fetch('/session', {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### 注销会话
|
||||
|
||||
注销当前会话
|
||||
相应地, 退出行为被认为是删除一个终端到服务端的会话, 因此不使用 loguot 或 signout, 而是 session.
|
||||
|
||||
注销当前会话
|
||||
```javascript
|
||||
fetch('/session', {
|
||||
method: 'DELETE',
|
||||
@ -176,6 +179,9 @@ fetch('/user/ApSXNLoUy', {
|
||||
|
||||
#### 上传头像
|
||||
|
||||
实际分为两步,
|
||||
第一步先上传附件到自己的账户
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<input type="file" name="photos", multiple, onchange="upload()"/>
|
||||
@ -196,6 +202,7 @@ function upload() {
|
||||
</script>
|
||||
```
|
||||
|
||||
第二步修改自己的头像路径为返回的图像路径, (!注意此处未作安全检查)
|
||||
```javascript
|
||||
fetch('/user/ApSXNLoUy', {
|
||||
method: 'PATCH',
|
||||
@ -212,6 +219,7 @@ fetch('/user/ApSXNLoUy', {
|
||||
|
||||
#### 删除用户
|
||||
|
||||
管理员可以直接删除指定用户, 普通用户可以删除自己
|
||||
```javascript
|
||||
fetch('/user/ApSXNLoUy', {
|
||||
method: 'DELETE',
|
||||
@ -224,6 +232,8 @@ fetch('/user/ApSXNLoUy', {
|
||||
|
||||
#### 创建文章
|
||||
|
||||
此处 book 路径是未作限制的, 也可以是其他未被限制的路径.
|
||||
|
||||
```javascript
|
||||
fetch('/book', {
|
||||
method: 'POST',
|
||||
@ -248,6 +258,11 @@ fetch('/book', {
|
||||
|
||||
#### 评论文章
|
||||
|
||||
这里假设文章类型是 book, 文章 id 是 ppNXLoUK
|
||||
|
||||
attach 意为附属于指定对象类型
|
||||
aid 意为附属于指定对象 id
|
||||
|
||||
```javascript
|
||||
fetch('/post?attach=book&aid=ppNXLoUK', {
|
||||
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
|
||||
fetch('/like?attach=post&aid=spNkjLA', {
|
||||
method: 'POST',
|
||||
@ -299,6 +338,9 @@ fetch('/like/SOAPSAdaw', {
|
||||
|
||||
#### 调频广播
|
||||
|
||||
通过频道订阅模式实现广播, 其中心思想是保障一个终端只维持一个 websocket 连接,
|
||||
所有消息都通过一个连接通道发送到终端, 如聊天室频道, 系统消息, 全局广播, 消息盒子通知等
|
||||
|
||||
```javascript
|
||||
let socket = new WebSocket("ws://localhost:2333");
|
||||
|
||||
@ -355,7 +397,6 @@ socket.onerror = function(error) {
|
||||
#### 游客广播
|
||||
|
||||
允许未登录会话加入订阅, 将未登录会话加入到游客账户
|
||||
|
||||
因此不能再通过账户登录状态进行拦截
|
||||
|
||||
|
||||
@ -368,8 +409,8 @@ socket.onerror = function(error) {
|
||||
|
||||
```javascript
|
||||
{
|
||||
page: Number, // 当前页码(默认为1)
|
||||
pagesize: Number, // 分页大小(默认20)
|
||||
page: Number, // 当前页码(默认为1)
|
||||
pagesize: Number, // 分页大小(默认20)
|
||||
sort: string, // 排序方式(只能是对象的通用属性名)
|
||||
desc: Number, // 0或1, 正序和倒序
|
||||
uid: string, // 指定发布者uid查询
|
||||
|
0
admin/home.js
Normal file
0
admin/home.js
Normal file
71
fmhub.js
71
fmhub.js
@ -1,69 +1,42 @@
|
||||
import interrelated from 'interrelated'
|
||||
|
||||
export default class {
|
||||
constructor() {
|
||||
this.channels = new Map()
|
||||
this.users = new Map()
|
||||
this.用户订阅 = new interrelated()
|
||||
this.用户会话 = new interrelated()
|
||||
}
|
||||
订阅频道(fid, uid) {
|
||||
console.log(`用户 ${uid} 订阅了 ${fid}`)
|
||||
let channel = this.channels.get(fid) || new Map()
|
||||
if (!channel.size) this.channels.set(fid, channel)
|
||||
channel.set(uid, true)
|
||||
this.用户订阅.set(fid, uid)
|
||||
}
|
||||
取消订阅(fid, uid) {
|
||||
console.log(`用户 ${uid} 取消订阅 ${fid} 频道`)
|
||||
let channel = this.channels.get(fid)
|
||||
channel.delete(uid)
|
||||
// TODO: 如果此频道没有用户订阅了, 则将此频道也移除
|
||||
// TODO: 如果此用户没有任何订阅了, 则将此用户也移除
|
||||
this.用户订阅.delete(fid, uid)
|
||||
}
|
||||
增加会话(uid, ws) {
|
||||
console.log(`用户 ${uid} 建立了新的会话连接`)
|
||||
let user = this.users.get(uid) || new Map()
|
||||
if (!user.size) this.users.set(uid, user)
|
||||
user.set(ws, true)
|
||||
this.用户会话.set(uid, ws)
|
||||
}
|
||||
移除会话(uid, ws) {
|
||||
console.log(`用户 ${uid} 结束了当前会话连接`)
|
||||
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)
|
||||
})
|
||||
}
|
||||
this.用户会话.delete(uid, ws)
|
||||
}
|
||||
发送消息(fm, uid, data) {
|
||||
console.log("发送消息", fm, uid, data)
|
||||
let msg = JSON.stringify({ fm, uid, data })
|
||||
let channel = this.channels.get(fm) || new Map()
|
||||
if (!channel.size) this.channels.set(fm, channel)
|
||||
channel.forEach((value, userid) => {
|
||||
//console.log(userid, value)
|
||||
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) => {
|
||||
this.用户订阅.atob(fm, (uid) => {
|
||||
//console.log(`用户 ${uid} 订阅的所有频道`)
|
||||
this.用户会话.atob(uid, (ws) => {
|
||||
//console.log(`用户 ${uid} 的会话`)
|
||||
ws.send(msg)
|
||||
})
|
||||
})
|
||||
//console.log(`用户 ${uid} 订阅的所有频道`)
|
||||
//this.用户订阅.aall(uid, (fid) => {
|
||||
// console.log(fid)
|
||||
//})
|
||||
//console.log(`频道 ${fm} 下的所有用户`)
|
||||
//this.用户订阅.ball(fm, (uid) => {
|
||||
// console.log(uid)
|
||||
//})
|
||||
}
|
||||
移除用户(uid) {
|
||||
console.log(`移除 ${uid} 的所有会话`)
|
||||
let user = this.users.get(uid)
|
||||
if (user) {
|
||||
user.forEach((value, ws) => ws.close()) // 断开用户所有会话
|
||||
this.users.delete(uid) // 删除用户(所有会话)
|
||||
}
|
||||
// TODO: 移除所有频道中的此用户
|
||||
// TODO: 移除此用户的记录
|
||||
// TODO: 断开所有此用户的连接
|
||||
this.用户订阅.adelete(uid)
|
||||
this.用户会话.adelete(uid)
|
||||
}
|
||||
}
|
||||
|
130
index.js
130
index.js
@ -7,9 +7,7 @@ import random from 'string-random'
|
||||
import formidable from 'formidable'
|
||||
import md5 from 'md5-node'
|
||||
import HUB from './fmhub.js'
|
||||
//import site from './collection.js'
|
||||
|
||||
const app = expressWs(express()).app
|
||||
const databases = new Map() // 所有数据库
|
||||
const FM = new HUB() // 频道消息分发器
|
||||
|
||||
@ -228,6 +226,27 @@ const object_create = async function (req, res) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
db(req.params.name).persistence.compactDatafile()
|
||||
return res.send("ok")
|
||||
}
|
||||
|
||||
const app = expressWs(express()).app
|
||||
app.use(express.json())
|
||||
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('/data/file/', express.static('data/file'))
|
||||
app.ws('/', websocketer)
|
||||
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/:sid').delete(online, session_delete)
|
||||
app.route('/:name').get(object_list).post(object_create).put(db_compact)
|
||||
|
@ -12,6 +12,7 @@
|
||||
"express-session-nedb": "^1.0.1",
|
||||
"express-ws": "^5.0.2",
|
||||
"formidable": "^2.0.1",
|
||||
"interrelated": "^2.0.0-0",
|
||||
"md5-node": "^1.0.1",
|
||||
"nedb": "^1.8.0",
|
||||
"string-random": "^0.1.3"
|
||||
|
16
start.sh
16
start.sh
@ -4,3 +4,19 @@ chmod 777 start.sh
|
||||
|
||||
pm2 delete kana
|
||||
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
|
||||
|
@ -274,6 +274,11 @@ inherits@2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
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:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
|
Loading…
Reference in New Issue
Block a user