Add user-scoped data filtering for subscription and dependence operations

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-09 16:37:37 +00:00
parent 4cf2858ab0
commit 777fd3fb23
4 changed files with 106 additions and 42 deletions

View File

@ -22,7 +22,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.dependencies(req.query as any); const data = await dependenceService.dependencies(req.query as any, [], {}, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -45,7 +45,7 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.create(req.body); const data = await dependenceService.create(req.body, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); return next(e);
@ -82,10 +82,10 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.remove(req.body); const data = await dependenceService.remove(req.body, false, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );
@ -98,10 +98,10 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.remove(req.body, true); const data = await dependenceService.remove(req.body, true, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );
@ -132,10 +132,10 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.reInstall(req.body); const data = await dependenceService.reInstall(req.body, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );
@ -148,10 +148,10 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
await dependenceService.cancel(req.body); await dependenceService.cancel(req.body, req.user?.userId);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );

View File

@ -16,6 +16,7 @@ export default (app: Router) => {
const data = await subscriptionService.list( const data = await subscriptionService.list(
req.query.searchValue as string, req.query.searchValue as string,
req.query.ids as string, req.query.ids as string,
req.user?.userId,
); );
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
@ -63,7 +64,10 @@ export default (app: Router) => {
cron_parser.parseExpression(req.body.schedule).hasNext() cron_parser.parseExpression(req.body.schedule).hasNext()
) { ) {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.create(req.body); const data = await subscriptionService.create({
...req.body,
userId: req.user?.userId,
});
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} else { } else {
return res.send({ code: 400, message: 'param schedule error' }); return res.send({ code: 400, message: 'param schedule error' });
@ -83,10 +87,10 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.run(req.body); const data = await subscriptionService.run(req.body, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );
@ -100,10 +104,10 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.stop(req.body); const data = await subscriptionService.stop(req.body, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );
@ -117,10 +121,10 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.disabled(req.body); const data = await subscriptionService.disabled(req.body, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );
@ -134,10 +138,10 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.enabled(req.body); const data = await subscriptionService.enabled(req.body, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );
@ -220,10 +224,10 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.remove(req.body, req.query); const data = await subscriptionService.remove(req.body, req.query, req.user?.userId);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e: any) {
return next(e); return res.send({ code: 400, message: e.message });
} }
}, },
); );

View File

@ -30,9 +30,33 @@ export default class DependenceService {
private sockService: SockService, private sockService: SockService,
) { } ) { }
public async create(payloads: Dependence[]): Promise<Dependence[]> { private addUserIdFilter(query: any, userId?: number) {
if (userId !== undefined) {
query.userId = userId;
}
return query;
}
private async checkOwnership(ids: number[], userId?: number): Promise<void> {
if (userId === undefined) {
// Admin can access all dependencies
return;
}
const dependencies = await DependenceModel.findAll({
where: { id: ids },
attributes: ['id', 'userId'],
});
const unauthorized = dependencies.filter(
(dep) => dep.userId !== undefined && dep.userId !== userId
);
if (unauthorized.length > 0) {
throw new Error('无权限操作该依赖');
}
}
public async create(payloads: Dependence[], userId?: number): Promise<Dependence[]> {
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {
const tab = new Dependence({ ...x, status: DependenceStatus.queued }); const tab = new Dependence({ ...x, status: DependenceStatus.queued, userId });
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
@ -65,7 +89,8 @@ export default class DependenceService {
return await this.getDb({ id: payload.id }); return await this.getDb({ id: payload.id });
} }
public async remove(ids: number[], force = false): Promise<Dependence[]> { public async remove(ids: number[], force = false, userId?: number): Promise<Dependence[]> {
await this.checkOwnership(ids, userId);
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
taskLimit.removeQueuedDependency(doc); taskLimit.removeQueuedDependency(doc);
@ -105,8 +130,10 @@ export default class DependenceService {
}, },
sort: any = [], sort: any = [],
query: any = {}, query: any = {},
userId?: number,
): Promise<Dependence[]> { ): Promise<Dependence[]> {
let condition = query; let condition = query;
this.addUserIdFilter(condition, userId);
if (DependenceTypes[type]) { if (DependenceTypes[type]) {
condition.type = DependenceTypes[type]; condition.type = DependenceTypes[type];
} }
@ -141,7 +168,8 @@ export default class DependenceService {
return taskLimit.waitDependencyQueueDone(); return taskLimit.waitDependencyQueueDone();
} }
public async reInstall(ids: number[]): Promise<Dependence[]> { public async reInstall(ids: number[], userId?: number): Promise<Dependence[]> {
await this.checkOwnership(ids, userId);
await DependenceModel.update( await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] }, { status: DependenceStatus.queued, log: [] },
{ where: { id: ids } }, { where: { id: ids } },
@ -155,7 +183,8 @@ export default class DependenceService {
return docs; return docs;
} }
public async cancel(ids: number[]) { public async cancel(ids: number[], userId?: number) {
await this.checkOwnership(ids, userId);
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
taskLimit.removeQueuedDependency(doc); taskLimit.removeQueuedDependency(doc);

View File

@ -42,11 +42,37 @@ export default class SubscriptionService {
private crontabService: CrontabService, private crontabService: CrontabService,
) {} ) {}
private addUserIdFilter(query: any, userId?: number) {
if (userId !== undefined) {
query.userId = userId;
}
return query;
}
private async checkOwnership(ids: number[], userId?: number): Promise<void> {
if (userId === undefined) {
// Admin can access all subscriptions
return;
}
const subscriptions = await SubscriptionModel.findAll({
where: { id: ids },
attributes: ['id', 'userId'],
});
const unauthorized = subscriptions.filter(
(sub) => sub.userId !== undefined && sub.userId !== userId
);
if (unauthorized.length > 0) {
throw new Error('无权限操作该订阅');
}
}
public async list( public async list(
searchText?: string, searchText?: string,
ids?: string, ids?: string,
userId?: number,
): Promise<SubscriptionInstance[]> { ): Promise<SubscriptionInstance[]> {
let query = {}; let query: any = {};
this.addUserIdFilter(query, userId);
const subIds = JSON.parse(ids || '[]'); const subIds = JSON.parse(ids || '[]');
if (searchText) { if (searchText) {
const reg = { const reg = {
@ -262,7 +288,8 @@ export default class SubscriptionService {
); );
} }
public async remove(ids: number[], query: { force?: boolean }) { public async remove(ids: number[], query: { force?: boolean }, userId?: number) {
await this.checkOwnership(ids, userId);
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc.get({ plain: true }), false); await this.handleTask(doc.get({ plain: true }), false);
@ -273,7 +300,7 @@ export default class SubscriptionService {
if (query?.force === true) { if (query?.force === true) {
const crons = await CrontabModel.findAll({ where: { sub_id: ids } }); const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
if (crons?.length) { if (crons?.length) {
await this.crontabService.remove(crons.map((x) => x.id!)); await this.crontabService.remove(crons.map((x) => x.id!), userId);
} }
for (const doc of docs) { for (const doc of docs) {
const filePath = join(config.scriptPath, doc.alias); const filePath = join(config.scriptPath, doc.alias);
@ -294,7 +321,8 @@ export default class SubscriptionService {
return doc.get({ plain: true }); return doc.get({ plain: true });
} }
public async run(ids: number[]) { public async run(ids: number[], userId?: number) {
await this.checkOwnership(ids, userId);
await SubscriptionModel.update( await SubscriptionModel.update(
{ status: SubscriptionStatus.queued }, { status: SubscriptionStatus.queued },
{ where: { id: ids } }, { where: { id: ids } },
@ -304,7 +332,8 @@ export default class SubscriptionService {
}); });
} }
public async stop(ids: number[]) { public async stop(ids: number[], userId?: number) {
await this.checkOwnership(ids, userId);
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
@ -339,7 +368,8 @@ export default class SubscriptionService {
}); });
} }
public async disabled(ids: number[]) { public async disabled(ids: number[], userId?: number) {
await this.checkOwnership(ids, userId);
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } }); await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig(); await this.setSshConfig();
@ -348,7 +378,8 @@ export default class SubscriptionService {
} }
} }
public async enabled(ids: number[]) { public async enabled(ids: number[], userId?: number) {
await this.checkOwnership(ids, userId);
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } }); await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig(); await this.setSshConfig();