feat: user org and fix bugs
This commit is contained in:
parent
3c7ef0d6e4
commit
54e3ccb3ff
@ -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,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,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
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);
|
Loading…
x
Reference in New Issue
Block a user