From 54e3ccb3ff0dd969311f0f5cd5e320bc8c086a23 Mon Sep 17 00:00:00 2001 From: xion Date: Tue, 8 Oct 2024 03:30:01 +0800 Subject: [PATCH] feat: user org and fix bugs --- src/lib/upload.ts | 2 +- src/models/org.ts | 42 +++++++++ src/models/user.ts | 97 +++++++++++++++++-- src/routes/app-manager/list.ts | 13 +-- src/routes/app-manager/revoke.ts | 6 ++ src/routes/app-manager/user-app.ts | 4 + src/routes/user/index.ts | 5 +- src/routes/user/list.ts | 144 +++++++++++------------------ src/routes/user/me.ts | 124 +++++++++++++++++++++++++ src/routes/user/org.ts | 98 ++++++++++++++++++++ 10 files changed, 422 insertions(+), 113 deletions(-) create mode 100644 src/models/org.ts create mode 100644 src/routes/app-manager/revoke.ts create mode 100644 src/routes/user/me.ts create mode 100644 src/routes/user/org.ts diff --git a/src/lib/upload.ts b/src/lib/upload.ts index 8cb6b52..67ef557 100644 --- a/src/lib/upload.ts +++ b/src/lib/upload.ts @@ -10,7 +10,7 @@ import { bucketName } from '@/modules/minio.ts'; import { getContentType } from '@/utils/get-content-type.ts'; import { User } from '@/models/user.ts'; const { tokenSecret } = useConfig<{ tokenSecret: string }>(); -const filePath = useFileStore('upload'); +const filePath = useFileStore('upload', { needExists: true }); // curl -X POST http://localhost:4000/api/upload -F "file=@readme.md" // curl -X POST http://localhost:4000/api/upload \ // -F "file=@readme.md" \ diff --git a/src/models/org.ts b/src/models/org.ts new file mode 100644 index 0000000..969d636 --- /dev/null +++ b/src/models/org.ts @@ -0,0 +1,42 @@ +import { sequelize } from '../modules/sequelize.ts'; +import { DataTypes, Model } from 'sequelize'; + +export class Org extends Model { + declare id: string; + declare username: string; + declare description: string; + declare users: { role: string; uid: string }[]; +} + +Org.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + description: { + type: DataTypes.STRING, + allowNull: true, + }, + users: { + type: DataTypes.JSONB, + allowNull: true, + defaultValue: [], + }, + }, + { + sequelize, + modelName: 'cf_org', + paranoid: true, + }, +); + +Org.sync({ alter: true, logging: false }).catch((e) => { + console.error('Org sync', e); +}); diff --git a/src/models/user.ts b/src/models/user.ts index e9a808c..7c0251d 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,13 +1,18 @@ import { useConfig } from '@abearxiong/use-config'; import { sequelize } from '@/modules/sequelize.ts'; -import { DataTypes, Model } from 'sequelize'; +import { DataTypes, Model, Op } from 'sequelize'; import { createToken, checkToken } from '@abearxiong/auth/token'; import { cryptPwd } from '@abearxiong/auth'; import { nanoid } from 'nanoid'; import { CustomError } from '@abearxiong/router'; +import { Org } from './org.ts'; +import { redis } from '@/app.ts'; const config = useConfig<{ tokenSecret: string }>(); +type UserData = { + orgs?: string[]; +}; export class User extends Model { declare id: string; declare username: string; @@ -15,12 +20,16 @@ export class User extends Model { declare salt: string; declare needChangePassword: boolean; declare description: string; - declare data: any; - async createToken() { + declare data: UserData; + declare type: string; // user | org + declare owner: string; + declare orgId: string; + declare email: string; + async createToken(uid?: string) { const { id, username } = this; const expireTime = 60 * 60 * 24 * 7; // 7 days const now = new Date().getTime(); - const token = await createToken({ id, username }, config.tokenSecret); + const token = await createToken({ id, username, uid }, config.tokenSecret); return { token, expireTime: now + expireTime }; } static async verifyToken(token: string) { @@ -28,8 +37,8 @@ export class User extends Model { const tokenUser = ct.payload; return tokenUser; } - static createUser(username: string, password?: string, description?: string) { - const user = User.findOne({ where: { username } }); + static async createUser(username: string, password?: string, description?: string) { + const user = await User.findOne({ where: { username } }); if (user) { throw new CustomError('User already exists'); } @@ -37,7 +46,25 @@ export class User extends Model { let needChangePassword = !password; password = password || '123456'; const cPassword = cryptPwd(password, salt); - return User.create({ username, password: cPassword, description, salt, needChangePassword }); + return await User.create({ username, password: cPassword, description, salt, needChangePassword }); + } + static async createOrg(username: string, owner: string, description?: string) { + const user = await User.findOne({ where: { username } }); + if (user) { + throw new CustomError('User already exists'); + } + const me = await User.findByPk(owner); + if (!me) { + throw new CustomError('Owner not found'); + } + if (me.type !== 'user') { + throw new CustomError('Owner type is not user'); + } + const org = await Org.create({ username, description, users: [{ uid: owner, role: 'owner' }] }); + const newUser = await User.create({ username, password: '', description, type: 'org', owner, orgId: org.id }); + // owner add + await redis.del(`user:${me.id}:orgs`); + return newUser; } createPassword(password: string) { const salt = this.salt; @@ -45,6 +72,44 @@ export class User extends Model { this.password = cPassword; return cPassword; } + checkPassword(password: string) { + const salt = this.salt; + const cPassword = cryptPwd(password, salt); + return this.password === cPassword; + } + async getInfo() { + const orgs = await this.getOrgs(); + return { + id: this.id, + username: this.username, + description: this.description, + needChangePassword: this.needChangePassword, + type: this.type, + orgs, + }; + } + async getOrgs() { + const id = this.id; + const cache = await redis.get(`user:${id}:orgs`); + if (cache) { + return JSON.parse(cache); + } + const orgs = await Org.findAll({ + order: [['updatedAt', 'DESC']], + where: { + users: { + [Op.contains]: [ + { + uid: id, + }, + ], + }, + }, + }); + const orgNames = orgs.map((org) => org.username); + await redis.set(`user:${id}:orgs`, JSON.stringify(orgNames), 'EX', 60 * 60); // 1 hour + return orgNames; + } } User.init( { @@ -60,15 +125,29 @@ User.init( }, password: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, + }, + email: { + type: DataTypes.STRING, + allowNull: true, }, salt: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, description: { type: DataTypes.STRING, }, + type: { + type: DataTypes.STRING, + defaultValue: 'user', + }, + owner: { + type: DataTypes.UUID, + }, + orgId: { + type: DataTypes.UUID, + }, needChangePassword: { type: DataTypes.BOOLEAN, defaultValue: false, diff --git a/src/routes/app-manager/list.ts b/src/routes/app-manager/list.ts index 7cacafd..0d8384e 100644 --- a/src/routes/app-manager/list.ts +++ b/src/routes/app-manager/list.ts @@ -4,7 +4,7 @@ import { app, redis } from '@/app.ts'; import _ from 'lodash'; import { prefixFix } from './util.ts'; import { deleteFiles } from '../file/index.ts'; - +import { setExpire } from './revoke.ts'; app .route({ path: 'app', @@ -23,6 +23,7 @@ app uid: tokenUser.id, key: data.key, }, + logging: false, }); ctx.body = list.map((item) => prefixFix(item, tokenUser.username)); return ctx; @@ -189,15 +190,7 @@ app throw new CustomError('app not found'); } await am.update({ data: { ...am.data, files }, version: app.version }); - // - const keys = await redis.keys('user:app:exist:*'); - console.log('keys', keys); - const expireKey = 'user:app:exist:' + `${app.key}:${am.user}`; - console.log('expireKey', expireKey); - await redis.set(expireKey, 'v', 'EX', 2); - await new Promise((resolve) => setTimeout(resolve, 2100)); - const keys2 = await redis.keys('user:app:exist:*'); - console.log('keys2', keys2); + setExpire(app.key, am.user); ctx.body = 'success'; }) .addTo(app); diff --git a/src/routes/app-manager/revoke.ts b/src/routes/app-manager/revoke.ts new file mode 100644 index 0000000..f59cfcc --- /dev/null +++ b/src/routes/app-manager/revoke.ts @@ -0,0 +1,6 @@ +import { redis } from '@/app.ts'; + +export const setExpire = async (key: string, user: string) => { + const expireKey = 'user:app:exist:' + `${key}:${user}`; + await redis.set(expireKey, 'v', 'EX', 2); +}; diff --git a/src/routes/app-manager/user-app.ts b/src/routes/app-manager/user-app.ts index 1828fac..bf40c07 100644 --- a/src/routes/app-manager/user-app.ts +++ b/src/routes/app-manager/user-app.ts @@ -1,6 +1,7 @@ import { CustomError } from '@abearxiong/router'; import { AppModel, AppListModel } from './module/index.ts'; import { app } from '@/app.ts'; +import { setExpire } from './revoke.ts'; app .route({ @@ -68,6 +69,9 @@ app const newData = { ...app.data, ...data }; const newApp = await app.update({ data: newData, ...rest }); ctx.body = newApp; + if (app.status !== 'running') { + setExpire(newApp.key, app.user); + } } else { throw new CustomError('app not found'); } diff --git a/src/routes/user/index.ts b/src/routes/user/index.ts index 9166f9d..36ee905 100644 --- a/src/routes/user/index.ts +++ b/src/routes/user/index.ts @@ -1 +1,4 @@ -import './list.ts' \ No newline at end of file +import './list.ts'; +import './org.ts'; + +import './me.ts'; diff --git a/src/routes/user/list.ts b/src/routes/user/list.ts index e9fd977..dbf56f0 100644 --- a/src/routes/user/list.ts +++ b/src/routes/user/list.ts @@ -3,7 +3,11 @@ import { User } from '@/models/user.ts'; import { CustomError } from '@abearxiong/router'; app - .route('user', 'list') + .route({ + path: 'user', + key: 'list', + middleware: ['auth'], + }) .define(async (ctx) => { const users = await User.findAll({ attributes: ['id', 'username', 'description', 'needChangePassword'], @@ -14,106 +18,62 @@ app }) .addTo(app); + app - .route('user', 'login') + .route({ + path: 'user', + key: 'update', + middleware: ['auth'], + }) .define(async (ctx) => { - const { username, password } = ctx.query; - const user = await User.findOne({ where: { username } }); + const tokenUser = ctx.state.tokenUser; + const { id, username, password, description } = ctx.query.data || {}; + const user = await User.findByPk(id); + if (user.id !== tokenUser.id) { + throw new CustomError(401, 'Permission denied'); + } + if (!user) { - new CustomError(401, 'User not found'); + throw new CustomError(500, 'user not found'); } - if (user.password !== password) { - new CustomError(401, 'Password error'); + if (username) { + user.username = username; } + if (password) { + user.createPassword(password); + } + if (description) { + user.description = description; + } + await user.save(); + ctx.body = { + id: user.id, + username: user.username, + description: user.description, + needChangePassword: user.needChangePassword, + }; + }) + .addTo(app); + +app + .route({ + path: 'user', + key: 'add', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { username, password, description } = ctx.query.data || {}; + if (!username) { + throw new CustomError(400, 'username is required'); + } + const user = await User.createUser(username, password, description); const token = await user.createToken(); - ctx.body = token; - }) - .addTo(app); - -app - .route('user', 'auth') - .define(async (ctx) => { - const { checkToken: token } = ctx.query; - try { - const result = await User.verifyToken(token); - ctx.body = result || {}; - } catch (e) { - new CustomError(401, 'Token InValid '); - } - }) - .addTo(app); - -app - .route('user', 'updateSelf', { - middleware: ['auth'], - }) - .define(async (ctx) => { - const { username, password, description } = ctx.query; - const state = ctx.state?.tokenUser || {}; - const { id } = state; - const user = await User.findByPk(id); - if (!user) { - throw new CustomError(500, 'user not found'); - } - if (username) { - user.username = username; - } - if (password) { - user.createPassword(password); - } - if (description) { - user.description = description; - } - await user.save(); ctx.body = { id: user.id, username: user.username, description: user.description, needChangePassword: user.needChangePassword, + token, }; - }) - .addTo(app); -app - .route('user', 'update', { - middleware: ['auth'], - }) - .define(async (ctx) => { - const { id, username, password, description } = ctx.query; - const user = await User.findByPk(id); - if (!user) { - throw new CustomError(500, 'user not found'); - } - if (username) { - user.username = username; - } - if (password) { - user.createPassword(password); - } - if (description) { - user.description = description; - } - await user.save(); - ctx.body = { - id: user.id, - username: user.username, - description: user.description, - needChangePassword: user.needChangePassword, - }; - }) - .addTo(app); - -app.route('user', 'add').define(async (ctx) => { - const { username, password, description } = ctx.query; - if (!username) { - throw new CustomError(400, 'username is required'); - } - const user = await User.createUser(username, password, description); - const token = await user.createToken(); - ctx.body = { - id: user.id, - username: user.username, - description: user.description, - needChangePassword: user.needChangePassword, - token, - }; -}); + }); diff --git a/src/routes/user/me.ts b/src/routes/user/me.ts new file mode 100644 index 0000000..402bf6d --- /dev/null +++ b/src/routes/user/me.ts @@ -0,0 +1,124 @@ +import { app } from '@/app.ts'; +import { Org } from '@/models/org.ts'; +import { User } from '@/models/user.ts'; +import { CustomError } from '@abearxiong/router'; + +app + .route({ + path: 'user', + key: 'me', + middleware: ['auth'], + }) + .define(async (ctx) => { + const state = ctx.state?.tokenUser || {}; + const { id } = state; + const user = await User.findByPk(id); + if (!user) { + throw new CustomError(500, 'user not found'); + } + ctx.body = await user.getInfo(); + }) + .addTo(app); +app + .route({ + path: 'user', + key: 'login', + }) + .define(async (ctx) => { + const { username, email, password } = ctx.query; + if (!username && !email) { + throw new CustomError(400, 'username or email is required'); + } + let user: User | null = null; + if (username) { + user = await User.findOne({ where: { username } }); + } + if (!user && email) { + user = await User.findOne({ where: { email } }); + } + if (!user) { + throw new CustomError(500, 'Login Failed'); + } + if (!user.checkPassword(password)) { + throw new CustomError(500, 'Password error'); + } + const token = await user.createToken(); + ctx.body = token; + }) + .addTo(app); + +app + .route('user', 'auth') + .define(async (ctx) => { + const { checkToken: token } = ctx.query; + try { + const result = await User.verifyToken(token); + ctx.body = result || {}; + } catch (e) { + throw new CustomError(401, 'Token InValid '); + } + }) + .addTo(app); + +app + .route('user', 'updateSelf', { + middleware: ['auth'], + }) + .define(async (ctx) => { + const { username, password, description } = ctx.query; + const state = ctx.state?.tokenUser || {}; + const { id } = state; + const user = await User.findByPk(id); + if (!user) { + throw new CustomError(500, 'user not found'); + } + if (username) { + user.username = username; + } + if (password) { + user.createPassword(password); + } + if (description) { + user.description = description; + } + await user.save(); + ctx.body = await user.getInfo(); + }) + .addTo(app); +app + .route({ + path: 'user', + key: 'switchOrg', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { username, type = 'org' } = ctx.query.data || {}; + if (!username && type === 'org') { + throw new CustomError('username is required'); + } + let me: User; + if (tokenUser.uid) { + me = await User.findByPk(tokenUser.uid); + } else { + me = await User.findByPk(tokenUser.id); + } + if (type === 'user') { + const token = await me.createToken(); + ctx.body = token; + return; + } + const orgUser = await User.findOne({ where: { username } }); + if (!orgUser) { + throw new CustomError('org not found'); + } + const user = await Org.findOne({ where: { username } }); + const users = user.users; + const index = users.findIndex((u) => u.uid === me.id); + if (index === -1) { + throw new CustomError('Permission denied'); + } + const token = await orgUser.createToken(me.id); + ctx.body = token; + }) + .addTo(app); diff --git a/src/routes/user/org.ts b/src/routes/user/org.ts new file mode 100644 index 0000000..9431401 --- /dev/null +++ b/src/routes/user/org.ts @@ -0,0 +1,98 @@ +import { app, sequelize } from '@/app.ts'; +import { Org } from '@/models/org.ts'; +import { User } from '@/models/user.ts'; +import { CustomError } from '@abearxiong/router'; +import { Op } from 'sequelize'; + +app + .route({ + path: 'org', + key: 'list', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const list = await Org.findAll({ + order: [['updatedAt', 'DESC']], + where: { + users: { + [Op.contains]: [ + { + uid: tokenUser.id, + }, + ], + }, + }, + }); + + ctx.body = list; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'org', + key: 'get', + }) + .define(async (ctx) => { + const id = ctx.query.id; + if (!id) { + throw new CustomError('id is required'); + } + ctx.body = await Org.findByPk(id); + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'org', + key: 'update', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { username, description } = ctx.query.data; + if (!username) { + throw new CustomError('username is required'); + } + const user = await User.createOrg(username, tokenUser.id, description); + ctx.body = { + id: user.id, + username: user.username, + description: user.description, + }; + }) + .addTo(app); + +app + .route({ + path: 'org', + key: 'delete', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const id = ctx.query.id; + if (!id) { + throw new CustomError('id is required'); + } + const org = await Org.findByPk(id); + if (!org) { + throw new CustomError('org not found'); + } + const username = org.username; + const users = org.users; + const owner = users.find((u) => u.role === 'owner'); + if (owner.uid !== tokenUser.id) { + throw new CustomError('Permission denied'); + } + await org.destroy({ force: true }); + const orgUser = await User.findOne({ + where: { username }, + }); + await orgUser.destroy({ force: true }); + ctx.body = 'success'; + }) + .addTo(app);