feat: user org and fix bugs
This commit is contained in:
		| @@ -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" \ | ||||
|   | ||||
							
								
								
									
										42
									
								
								src/models/org.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/models/org.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| }); | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
							
								
								
									
										6
									
								
								src/routes/app-manager/revoke.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/routes/app-manager/revoke.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| }; | ||||
| @@ -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'); | ||||
|       } | ||||
|   | ||||
| @@ -1 +1,4 @@ | ||||
| import './list.ts' | ||||
| import './list.ts'; | ||||
| import './org.ts'; | ||||
|  | ||||
| import './me.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,72 +18,21 @@ app | ||||
|   }) | ||||
|   .addTo(app); | ||||
|  | ||||
| app | ||||
|   .route('user', 'login') | ||||
|   .define(async (ctx) => { | ||||
|     const { username, password } = ctx.query; | ||||
|     const user = await User.findOne({ where: { username } }); | ||||
|     if (!user) { | ||||
|       new CustomError(401, 'User not found'); | ||||
|     } | ||||
|     if (user.password !== password) { | ||||
|       new CustomError(401, '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) { | ||||
|       new CustomError(401, 'Token InValid '); | ||||
|     } | ||||
|   }) | ||||
|   .addTo(app); | ||||
|  | ||||
| app | ||||
|   .route('user', 'updateSelf', { | ||||
|   .route({ | ||||
|     path: 'user', | ||||
|     key: 'update', | ||||
|     middleware: ['auth'], | ||||
|   }) | ||||
|   .define(async (ctx) => { | ||||
|     const { username, password, description } = ctx.query; | ||||
|     const state = ctx.state?.tokenUser || {}; | ||||
|     const { id } = state; | ||||
|     const tokenUser = ctx.state.tokenUser; | ||||
|     const { id, username, password, description } = ctx.query.data || {}; | ||||
|     const user = await User.findByPk(id); | ||||
|     if (!user) { | ||||
|       throw new CustomError(500, 'user not found'); | ||||
|     if (user.id !== tokenUser.id) { | ||||
|       throw new CustomError(401, 'Permission denied'); | ||||
|     } | ||||
|     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', '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'); | ||||
|     } | ||||
| @@ -102,8 +55,15 @@ app | ||||
|   }) | ||||
|   .addTo(app); | ||||
|  | ||||
| app.route('user', 'add').define(async (ctx) => { | ||||
|   const { username, password, description } = ctx.query; | ||||
| 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'); | ||||
|     } | ||||
| @@ -116,4 +76,4 @@ app.route('user', 'add').define(async (ctx) => { | ||||
|       needChangePassword: user.needChangePassword, | ||||
|       token, | ||||
|     }; | ||||
| }); | ||||
|   }); | ||||
|   | ||||
							
								
								
									
										124
									
								
								src/routes/user/me.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/routes/user/me.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
							
								
								
									
										98
									
								
								src/routes/user/org.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/routes/user/org.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
		Reference in New Issue
	
	Block a user