mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-19 18:35:18 +02:00
refactor authz check
This commit is contained in:
parent
831356a155
commit
cd6ff9a46f
10 changed files with 153 additions and 124 deletions
|
|
@ -10,6 +10,9 @@ import { FetchCachedTurnUseCase } from "@/src/application/use-cases/conversation
|
||||||
import { CreateCachedTurnController } from "@/src/interface-adapters/controllers/conversations/create-cached-turn.controller";
|
import { CreateCachedTurnController } from "@/src/interface-adapters/controllers/conversations/create-cached-turn.controller";
|
||||||
import { RunTurnController } from "@/src/interface-adapters/controllers/conversations/run-turn.controller";
|
import { RunTurnController } from "@/src/interface-adapters/controllers/conversations/run-turn.controller";
|
||||||
import { RedisUsageQuotaPolicy } from "@/src/infrastructure/policies/redis.usage-quota.policy";
|
import { RedisUsageQuotaPolicy } from "@/src/infrastructure/policies/redis.usage-quota.policy";
|
||||||
|
import { ProjectActionAuthorizationPolicy } from "@/src/application/policies/project-action-authorization.policy";
|
||||||
|
import { MongoDBProjectMembersRepository } from "@/src/infrastructure/repositories/mongodb.project-members.repository";
|
||||||
|
import { MongoDBApiKeysRepository } from "@/src/infrastructure/repositories/mongodb.api-keys.repository";
|
||||||
|
|
||||||
export const container = createContainer({
|
export const container = createContainer({
|
||||||
injectionMode: InjectionMode.PROXY,
|
injectionMode: InjectionMode.PROXY,
|
||||||
|
|
@ -20,7 +23,19 @@ container.register({
|
||||||
// services
|
// services
|
||||||
// ---
|
// ---
|
||||||
cacheService: asClass(RedisCacheService).singleton(),
|
cacheService: asClass(RedisCacheService).singleton(),
|
||||||
|
|
||||||
|
// policies
|
||||||
|
// ---
|
||||||
usageQuotaPolicy: asClass(RedisUsageQuotaPolicy).singleton(),
|
usageQuotaPolicy: asClass(RedisUsageQuotaPolicy).singleton(),
|
||||||
|
projectActionAuthorizationPolicy: asClass(ProjectActionAuthorizationPolicy).singleton(),
|
||||||
|
|
||||||
|
// project members
|
||||||
|
// ---
|
||||||
|
projectMembersRepository: asClass(MongoDBProjectMembersRepository).singleton(),
|
||||||
|
|
||||||
|
// api keys
|
||||||
|
// ---
|
||||||
|
apiKeysRepository: asClass(MongoDBApiKeysRepository).singleton(),
|
||||||
|
|
||||||
// conversations
|
// conversations
|
||||||
// ---
|
// ---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { BadRequestError, NotAuthorizedError } from "@/src/entities/errors/common";
|
||||||
|
import { IProjectMembersRepository } from "../repositories/project-members.repository.interface";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { IApiKeysRepository } from "../repositories/api-keys.repository.interface";
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
caller: z.enum(["user", "api"]),
|
||||||
|
userId: z.string().optional(),
|
||||||
|
apiKey: z.string().optional(),
|
||||||
|
projectId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface IProjectActionAuthorizationPolicy {
|
||||||
|
authorize(data: z.infer<typeof inputSchema>): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProjectActionAuthorizationPolicy implements IProjectActionAuthorizationPolicy {
|
||||||
|
private readonly projectMembersRepository: IProjectMembersRepository;
|
||||||
|
private readonly apiKeysRepository: IApiKeysRepository;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
projectMembersRepository,
|
||||||
|
apiKeysRepository,
|
||||||
|
}: {
|
||||||
|
projectMembersRepository: IProjectMembersRepository;
|
||||||
|
apiKeysRepository: IApiKeysRepository;
|
||||||
|
}) {
|
||||||
|
this.projectMembersRepository = projectMembersRepository;
|
||||||
|
this.apiKeysRepository = apiKeysRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async authorize(data: z.infer<typeof inputSchema>): Promise<void> {
|
||||||
|
const { caller, userId, apiKey, projectId } = data;
|
||||||
|
|
||||||
|
if (caller === "user") {
|
||||||
|
if (!userId) {
|
||||||
|
throw new BadRequestError('User ID is required');
|
||||||
|
}
|
||||||
|
const membership = await this.projectMembersRepository.checkMembership(projectId, userId);
|
||||||
|
if (!membership) {
|
||||||
|
throw new NotAuthorizedError('User is not a member of the project');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new BadRequestError('API key is required');
|
||||||
|
}
|
||||||
|
// check and consume api key
|
||||||
|
// while also updating last used timestamp
|
||||||
|
const result = await this.apiKeysRepository.checkAndConsumeKey(projectId, apiKey);
|
||||||
|
if (!result) {
|
||||||
|
throw new NotAuthorizedError('Invalid API key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IApiKeysRepository {
|
||||||
|
checkAndConsumeKey(projectId: string, apiKey: string): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IProjectMembersRepository {
|
||||||
|
checkMembership(projectId: string, userId: string): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { BadRequestError, NotAuthorizedError, NotFoundError } from '@/src/entities/errors/common';
|
import { NotFoundError } from '@/src/entities/errors/common';
|
||||||
import { apiKeysCollection, projectMembersCollection } from "@/app/lib/mongodb";
|
|
||||||
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
|
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { ICacheService } from '@/src/application/services/cache.service.interface';
|
import { ICacheService } from '@/src/application/services/cache.service.interface';
|
||||||
import { CachedTurnRequest, Turn } from '@/src/entities/models/turn';
|
import { CachedTurnRequest, Turn } from '@/src/entities/models/turn';
|
||||||
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
||||||
|
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
caller: z.enum(["user", "api"]),
|
caller: z.enum(["user", "api"]),
|
||||||
|
|
@ -23,19 +23,23 @@ export class CreateCachedTurnUseCase implements ICreateCachedTurnUseCase {
|
||||||
private readonly cacheService: ICacheService;
|
private readonly cacheService: ICacheService;
|
||||||
private readonly conversationsRepository: IConversationsRepository;
|
private readonly conversationsRepository: IConversationsRepository;
|
||||||
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
|
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
|
||||||
|
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
cacheService,
|
cacheService,
|
||||||
conversationsRepository,
|
conversationsRepository,
|
||||||
usageQuotaPolicy,
|
usageQuotaPolicy,
|
||||||
|
projectActionAuthorizationPolicy,
|
||||||
}: {
|
}: {
|
||||||
cacheService: ICacheService,
|
cacheService: ICacheService,
|
||||||
conversationsRepository: IConversationsRepository,
|
conversationsRepository: IConversationsRepository,
|
||||||
usageQuotaPolicy: IUsageQuotaPolicy,
|
usageQuotaPolicy: IUsageQuotaPolicy,
|
||||||
|
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
|
||||||
}) {
|
}) {
|
||||||
this.cacheService = cacheService;
|
this.cacheService = cacheService;
|
||||||
this.conversationsRepository = conversationsRepository;
|
this.conversationsRepository = conversationsRepository;
|
||||||
this.usageQuotaPolicy = usageQuotaPolicy;
|
this.usageQuotaPolicy = usageQuotaPolicy;
|
||||||
|
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(data: z.infer<typeof inputSchema>): Promise<{ key: string }> {
|
async execute(data: z.infer<typeof inputSchema>): Promise<{ key: string }> {
|
||||||
|
|
@ -51,35 +55,13 @@ export class CreateCachedTurnUseCase implements ICreateCachedTurnUseCase {
|
||||||
// assert and consume quota
|
// assert and consume quota
|
||||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||||
|
|
||||||
// if caller is a user, ensure they are a member of project
|
// authz check
|
||||||
if (data.caller === "user") {
|
await this.projectActionAuthorizationPolicy.authorize({
|
||||||
if (!data.userId) {
|
caller: data.caller,
|
||||||
throw new BadRequestError('User ID is required');
|
userId: data.userId,
|
||||||
}
|
apiKey: data.apiKey,
|
||||||
const membership = await projectMembersCollection.findOne({
|
projectId,
|
||||||
projectId,
|
});
|
||||||
userId: data.userId,
|
|
||||||
});
|
|
||||||
if (!membership) {
|
|
||||||
throw new NotAuthorizedError('User not a member of project');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!data.apiKey) {
|
|
||||||
throw new BadRequestError('API key is required');
|
|
||||||
}
|
|
||||||
// check if api key is valid
|
|
||||||
// while also updating last used timestamp
|
|
||||||
const result = await apiKeysCollection.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
projectId,
|
|
||||||
key: data.apiKey,
|
|
||||||
},
|
|
||||||
{ $set: { lastUsedAt: new Date().toISOString() } }
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
throw new NotAuthorizedError('Invalid API key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create cache entry
|
// create cache entry
|
||||||
const key = nanoid();
|
const key = nanoid();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { BadRequestError, NotAuthorizedError, NotFoundError } from '@/src/entities/errors/common';
|
import { BadRequestError, NotFoundError } from '@/src/entities/errors/common';
|
||||||
import { apiKeysCollection, projectMembersCollection, projectsCollection } from "@/app/lib/mongodb";
|
import { projectsCollection } from "@/app/lib/mongodb";
|
||||||
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
|
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Conversation } from "@/src/entities/models/conversation";
|
import { Conversation } from "@/src/entities/models/conversation";
|
||||||
import { Workflow } from "@/app/lib/types/workflow_types";
|
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||||
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
||||||
|
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
caller: z.enum(["user", "api"]),
|
caller: z.enum(["user", "api"]),
|
||||||
|
|
@ -22,16 +23,20 @@ export interface ICreateConversationUseCase {
|
||||||
export class CreateConversationUseCase implements ICreateConversationUseCase {
|
export class CreateConversationUseCase implements ICreateConversationUseCase {
|
||||||
private readonly conversationsRepository: IConversationsRepository;
|
private readonly conversationsRepository: IConversationsRepository;
|
||||||
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
|
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
|
||||||
|
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
conversationsRepository,
|
conversationsRepository,
|
||||||
usageQuotaPolicy,
|
usageQuotaPolicy,
|
||||||
|
projectActionAuthorizationPolicy,
|
||||||
}: {
|
}: {
|
||||||
conversationsRepository: IConversationsRepository,
|
conversationsRepository: IConversationsRepository,
|
||||||
usageQuotaPolicy: IUsageQuotaPolicy,
|
usageQuotaPolicy: IUsageQuotaPolicy,
|
||||||
|
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
|
||||||
}) {
|
}) {
|
||||||
this.conversationsRepository = conversationsRepository;
|
this.conversationsRepository = conversationsRepository;
|
||||||
this.usageQuotaPolicy = usageQuotaPolicy;
|
this.usageQuotaPolicy = usageQuotaPolicy;
|
||||||
|
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(data: z.infer<typeof inputSchema>): Promise<z.infer<typeof Conversation>> {
|
async execute(data: z.infer<typeof inputSchema>): Promise<z.infer<typeof Conversation>> {
|
||||||
|
|
@ -42,35 +47,13 @@ export class CreateConversationUseCase implements ICreateConversationUseCase {
|
||||||
// assert and consume quota
|
// assert and consume quota
|
||||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||||
|
|
||||||
// if caller is a user, ensure they are a member of project
|
// authz check
|
||||||
if (caller === "user") {
|
await this.projectActionAuthorizationPolicy.authorize({
|
||||||
if (!userId) {
|
caller,
|
||||||
throw new BadRequestError('User ID is required');
|
userId,
|
||||||
}
|
apiKey,
|
||||||
const membership = await projectMembersCollection.findOne({
|
projectId,
|
||||||
projectId,
|
});
|
||||||
userId,
|
|
||||||
});
|
|
||||||
if (!membership) {
|
|
||||||
throw new NotAuthorizedError('User not a member of project');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new BadRequestError('API key is required');
|
|
||||||
}
|
|
||||||
// check if api key is valid
|
|
||||||
// while also updating last used timestamp
|
|
||||||
const result = await apiKeysCollection.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
projectId,
|
|
||||||
key: apiKey,
|
|
||||||
},
|
|
||||||
{ $set: { lastUsedAt: new Date().toISOString() } }
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
throw new NotAuthorizedError('Invalid API key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if workflow is not provided, fetch workflow
|
// if workflow is not provided, fetch workflow
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { BadRequestError, NotAuthorizedError, NotFoundError } from '@/src/entities/errors/common';
|
import { NotFoundError } from '@/src/entities/errors/common';
|
||||||
import { apiKeysCollection, projectMembersCollection } from "@/app/lib/mongodb";
|
|
||||||
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
|
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ICacheService } from '@/src/application/services/cache.service.interface';
|
import { ICacheService } from '@/src/application/services/cache.service.interface';
|
||||||
import { CachedTurnRequest, Turn } from '@/src/entities/models/turn';
|
import { CachedTurnRequest, Turn } from '@/src/entities/models/turn';
|
||||||
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
||||||
|
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
caller: z.enum(["user", "api"]),
|
caller: z.enum(["user", "api"]),
|
||||||
|
|
@ -21,19 +21,23 @@ export class FetchCachedTurnUseCase implements IFetchCachedTurnUseCase {
|
||||||
private readonly cacheService: ICacheService;
|
private readonly cacheService: ICacheService;
|
||||||
private readonly conversationsRepository: IConversationsRepository;
|
private readonly conversationsRepository: IConversationsRepository;
|
||||||
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
|
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
|
||||||
|
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
cacheService,
|
cacheService,
|
||||||
conversationsRepository,
|
conversationsRepository,
|
||||||
usageQuotaPolicy,
|
usageQuotaPolicy,
|
||||||
|
projectActionAuthorizationPolicy,
|
||||||
}: {
|
}: {
|
||||||
cacheService: ICacheService,
|
cacheService: ICacheService,
|
||||||
conversationsRepository: IConversationsRepository,
|
conversationsRepository: IConversationsRepository,
|
||||||
usageQuotaPolicy: IUsageQuotaPolicy,
|
usageQuotaPolicy: IUsageQuotaPolicy,
|
||||||
|
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
|
||||||
}) {
|
}) {
|
||||||
this.cacheService = cacheService;
|
this.cacheService = cacheService;
|
||||||
this.conversationsRepository = conversationsRepository;
|
this.conversationsRepository = conversationsRepository;
|
||||||
this.usageQuotaPolicy = usageQuotaPolicy;
|
this.usageQuotaPolicy = usageQuotaPolicy;
|
||||||
|
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(data: z.infer<typeof inputSchema>): Promise<z.infer<typeof CachedTurnRequest>> {
|
async execute(data: z.infer<typeof inputSchema>): Promise<z.infer<typeof CachedTurnRequest>> {
|
||||||
|
|
@ -58,35 +62,13 @@ export class FetchCachedTurnUseCase implements IFetchCachedTurnUseCase {
|
||||||
// assert and consume quota
|
// assert and consume quota
|
||||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||||
|
|
||||||
// if caller is a user, ensure they are a member of project
|
// authz check
|
||||||
if (data.caller === "user") {
|
await this.projectActionAuthorizationPolicy.authorize({
|
||||||
if (!data.userId) {
|
caller: data.caller,
|
||||||
throw new BadRequestError('User ID is required');
|
userId: data.userId,
|
||||||
}
|
apiKey: data.apiKey,
|
||||||
const membership = await projectMembersCollection.findOne({
|
projectId,
|
||||||
projectId,
|
});
|
||||||
userId: data.userId,
|
|
||||||
});
|
|
||||||
if (!membership) {
|
|
||||||
throw new NotAuthorizedError('User not a member of project');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!data.apiKey) {
|
|
||||||
throw new BadRequestError('API key is required');
|
|
||||||
}
|
|
||||||
// check if api key is valid
|
|
||||||
// while also updating last used timestamp
|
|
||||||
const result = await apiKeysCollection.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
projectId,
|
|
||||||
key: data.apiKey,
|
|
||||||
},
|
|
||||||
{ $set: { lastUsedAt: new Date().toISOString() } }
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
throw new NotAuthorizedError('Invalid API key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete from cache
|
// delete from cache
|
||||||
await this.cacheService.delete(`turn-${data.key}`);
|
await this.cacheService.delete(`turn-${data.key}`);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { Turn, TurnEvent } from "@/src/entities/models/turn";
|
import { Turn, TurnEvent } from "@/src/entities/models/turn";
|
||||||
import { USE_BILLING } from "@/app/lib/feature_flags";
|
import { USE_BILLING } from "@/app/lib/feature_flags";
|
||||||
import { authorize, getCustomerIdForProject } from "@/app/lib/billing";
|
import { authorize, getCustomerIdForProject } from "@/app/lib/billing";
|
||||||
import { BadRequestError, BillingError, NotAuthorizedError, NotFoundError } from '@/src/entities/errors/common';
|
import { NotFoundError } from '@/src/entities/errors/common';
|
||||||
import { apiKeysCollection, projectMembersCollection } from "@/app/lib/mongodb";
|
|
||||||
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
|
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
|
||||||
import { streamResponse } from "@/app/lib/agents";
|
import { streamResponse } from "@/app/lib/agents";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Message } from "@/app/lib/types/types";
|
import { Message } from "@/app/lib/types/types";
|
||||||
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
||||||
|
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
caller: z.enum(["user", "api"]),
|
caller: z.enum(["user", "api"]),
|
||||||
|
|
@ -25,16 +25,20 @@ export interface IRunConversationTurnUseCase {
|
||||||
export class RunConversationTurnUseCase implements IRunConversationTurnUseCase {
|
export class RunConversationTurnUseCase implements IRunConversationTurnUseCase {
|
||||||
private readonly conversationsRepository: IConversationsRepository;
|
private readonly conversationsRepository: IConversationsRepository;
|
||||||
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
|
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
|
||||||
|
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
conversationsRepository,
|
conversationsRepository,
|
||||||
usageQuotaPolicy,
|
usageQuotaPolicy,
|
||||||
|
projectActionAuthorizationPolicy,
|
||||||
}: {
|
}: {
|
||||||
conversationsRepository: IConversationsRepository,
|
conversationsRepository: IConversationsRepository,
|
||||||
usageQuotaPolicy: IUsageQuotaPolicy,
|
usageQuotaPolicy: IUsageQuotaPolicy,
|
||||||
|
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
|
||||||
}) {
|
}) {
|
||||||
this.conversationsRepository = conversationsRepository;
|
this.conversationsRepository = conversationsRepository;
|
||||||
this.usageQuotaPolicy = usageQuotaPolicy;
|
this.usageQuotaPolicy = usageQuotaPolicy;
|
||||||
|
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
async *execute(data: z.infer<typeof inputSchema>): AsyncGenerator<z.infer<typeof TurnEvent>, void, unknown> {
|
async *execute(data: z.infer<typeof inputSchema>): AsyncGenerator<z.infer<typeof TurnEvent>, void, unknown> {
|
||||||
|
|
@ -50,35 +54,13 @@ export class RunConversationTurnUseCase implements IRunConversationTurnUseCase {
|
||||||
// assert and consume quota
|
// assert and consume quota
|
||||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||||
|
|
||||||
// if caller is a user, ensure they are a member of project
|
// authz check
|
||||||
if (data.caller === "user") {
|
await this.projectActionAuthorizationPolicy.authorize({
|
||||||
if (!data.userId) {
|
caller: data.caller,
|
||||||
throw new BadRequestError('User ID is required');
|
userId: data.userId,
|
||||||
}
|
apiKey: data.apiKey,
|
||||||
const membership = await projectMembersCollection.findOne({
|
projectId,
|
||||||
projectId,
|
});
|
||||||
userId: data.userId,
|
|
||||||
});
|
|
||||||
if (!membership) {
|
|
||||||
throw new NotAuthorizedError('User not a member of project');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!data.apiKey) {
|
|
||||||
throw new BadRequestError('API key is required');
|
|
||||||
}
|
|
||||||
// check if api key is valid
|
|
||||||
// while also updating last used timestamp
|
|
||||||
const result = await apiKeysCollection.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
projectId,
|
|
||||||
key: data.apiKey,
|
|
||||||
},
|
|
||||||
{ $set: { lastUsedAt: new Date().toISOString() } }
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
throw new NotAuthorizedError('Invalid API key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check billing auth
|
// Check billing auth
|
||||||
if (USE_BILLING) {
|
if (USE_BILLING) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { IApiKeysRepository } from "@/src/application/repositories/api-keys.repository.interface";
|
||||||
|
import { apiKeysCollection } from "@/app/lib/mongodb";
|
||||||
|
|
||||||
|
export class MongoDBApiKeysRepository implements IApiKeysRepository {
|
||||||
|
async checkAndConsumeKey(projectId: string, apiKey: string): Promise<boolean> {
|
||||||
|
const result = await apiKeysCollection.findOneAndUpdate(
|
||||||
|
{ projectId, key: apiKey },
|
||||||
|
{ $set: { lastUsedAt: new Date().toISOString() } }
|
||||||
|
);
|
||||||
|
return !!result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { IProjectMembersRepository } from "@/src/application/repositories/project-members.repository.interface";
|
||||||
|
import { db } from "@/app/lib/mongodb";
|
||||||
|
|
||||||
|
export class MongoDBProjectMembersRepository implements IProjectMembersRepository {
|
||||||
|
async checkMembership(projectId: string, userId: string): Promise<boolean> {
|
||||||
|
const membership = await db.collection('project_members').findOne({
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
return !!membership;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue