Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ea9d417
refactor(shares): add validate-share decorator
ycdzj Sep 12, 2025
a6d1b7f
refactor(resources): add getAllSubResources
ycdzj Sep 12, 2025
cbee57d
refactor(stream): add computeVisibleResources
ycdzj Sep 14, 2025
f24b4cc
refactor(messages): nullable user_id
ycdzj Sep 14, 2025
b8554be
refactor(wizard): update agent stream
ycdzj Sep 15, 2025
72c1c10
refactor(messages): specify type for userId
ycdzj Sep 15, 2025
ec7cccb
refactor(stream): update getUserVisibleResources
ycdzj Sep 15, 2025
158c279
refactor(resources): update shared-resource-meta
ycdzj Sep 15, 2025
9ff6e2b
refactor(shares): add create conversations api
ycdzj Sep 15, 2025
cbd5525
refactor(shares): add hasChildren
ycdzj Sep 16, 2025
155a2f8
refactor(shares): update public-share-info
ycdzj Sep 16, 2025
9d59beb
refactor(shares): add conversations api
ycdzj Sep 17, 2025
369530a
refactor(wizard): update api endpoint
ycdzj Sep 17, 2025
d4fc654
refactor(wizard): update dto
ycdzj Sep 17, 2025
46487ff
refactor(stream): fix dto
ycdzj Sep 17, 2025
78f7eb2
test(wizard): update api endpoints
ycdzj Sep 17, 2025
7d86903
test(wizard): fix text
ycdzj Sep 17, 2025
97f228a
refactor(wizard): update collect endpoint
ycdzj Sep 18, 2025
8f0f14b
refactor(share): add namespace name
ycdzj Sep 18, 2025
f8ffa09
fix(shares): fix visible resources
ycdzj Sep 19, 2025
3fd190f
refactor(wizard): fix CollectZRequestDto
ycdzj Sep 24, 2025
b580d6b
refactor(wizard): fix namespace id
ycdzj Sep 29, 2025
07c636c
refactor(wizard): fix namespace id
ycdzj Sep 24, 2025
c3ff370
refactor(wizard): fix controller
ycdzj Sep 24, 2025
84cb69e
refactor(stream): add span
ycdzj Sep 24, 2025
ab7d18c
refactor(stream): update shared visible resources
ycdzj Sep 24, 2025
b07ce17
refactor(wizard): fix merge conflicts
ycdzj Oct 9, 2025
7ab02a7
fix(wizard): fix test
ycdzj Oct 9, 2025
4735040
refactor(websocket): add share stream
ycdzj Oct 10, 2025
8780aa8
fix(websocket): fix module
ycdzj Oct 10, 2025
18fbc4a
refactor(shares): add share user
ycdzj Oct 11, 2025
da5f711
refactor(app): add migration module
ycdzj Oct 11, 2025
828bd4a
refactor(share): remove namespace_name
ycdzj Oct 11, 2025
c57a82e
fix(wizard): fix equality operator
ycdzj Oct 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ import { FeedbackModule } from 'omniboxd/feedback/feedback.module';
import { Feedback1757100000000 } from 'omniboxd/migrations/1757100000000-feedback';
import { UserInterceptor } from 'omniboxd/interceptor/user.interceptor';
import { WebSocketModule } from 'omniboxd/websocket/websocket.module';
import { NullableUserId1757844448000 } from 'omniboxd/migrations/1757844448000-nullable-user-id';
import { AddShareIdToConversations1757844449000 } from 'omniboxd/migrations/1757844449000-add-share-id-to-conversations';
import { ShareUser1760171824000 } from 'omniboxd/migrations/1760171824000-share-user';

@Module({})
export class AppModule implements NestModule {
Expand Down Expand Up @@ -148,6 +151,9 @@ export class AppModule implements NestModule {
SharesAllResources1754471311959,
Applications1756914379375,
Feedback1757100000000,
NullableUserId1757844448000,
AddShareIdToConversations1757844449000,
ShareUser1760171824000,
...extraMigrations,
],
migrationsRun: true,
Expand Down
10 changes: 2 additions & 8 deletions src/attachments/attachments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from './dto/upload-attachments-response.dto';
import { SharesService } from 'omniboxd/shares/shares.service';
import { SharedResourcesService } from 'omniboxd/shared-resources/shared-resources.service';
import { Share } from 'omniboxd/shares/entities/share.entity';

@Injectable()
export class AttachmentsService {
Expand Down Expand Up @@ -181,18 +182,11 @@ export class AttachmentsService {
}

async downloadAttachmentViaShare(
shareId: string,
share: Share,
resourceId: string,
attachmentId: string,
password: string,
userId: string | undefined,
httpResponse: Response,
) {
const share = await this.sharesService.getAndValidateShare(
shareId,
password,
userId,
);
await this.sharedResourcesService.getAndValidateResource(share, resourceId);
await this.resourceAttachmentsService.getResourceAttachmentOrFail(
share.namespaceId,
Expand Down
20 changes: 11 additions & 9 deletions src/attachments/share-attachments.controller.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import { Controller, Get, Param, Res } from '@nestjs/common';
import { Controller, Get, Param, Res, UseInterceptors } from '@nestjs/common';
import { Response } from 'express';
import { AttachmentsService } from './attachments.service';
import { UserId } from 'omniboxd/decorators/user-id.decorator';
import { CookieAuth } from 'omniboxd/auth/decorators';
import { Cookies } from 'omniboxd/decorators/cookie.decorators';
import {
ValidateShare,
ValidatedShare,
} from 'omniboxd/decorators/validate-share.decorator';
import { ValidateShareInterceptor } from 'omniboxd/interceptor/validate-share.interceptor';
import { Share } from 'omniboxd/shares/entities/share.entity';

@Controller('api/v1/shares/:shareId/resources/:resourceId/attachments')
@UseInterceptors(ValidateShareInterceptor)
export class ShareAttachmentsController {
constructor(private readonly attachmentsService: AttachmentsService) {}

@CookieAuth({ onAuthFail: 'continue' })
@ValidateShare()
@Get(':attachmentId')
async downloadAttachment(
@Param('shareId') shareId: string,
@Param('resourceId') resourceId: string,
@Param('attachmentId') attachmentId: string,
@Cookies('share-password') password: string,
@ValidatedShare() share: Share,
@Res() res: Response,
@UserId({ optional: true }) userId?: string,
) {
return await this.attachmentsService.downloadAttachmentViaShare(
shareId,
share,
resourceId,
attachmentId,
password,
userId,
res,
);
}
Expand Down
1 change: 1 addition & 0 deletions src/auth/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './public.auth.decorator';
export * from './ws-auth-options.decorator';
export * from '../api-key/api-key.auth.decorator';
export * from '../api-key/api-key.decorator';
export * from '../cookie/cookie.auth.decorator';
10 changes: 10 additions & 0 deletions src/auth/decorators/ws-auth-options.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SetMetadata } from '@nestjs/common';

export interface WsAuthConfig {
optional?: boolean;
}

export const WS_AUTH_CONFIG_KEY = 'wsAuthConfig';

export const WsAuthOptions = (config: WsAuthConfig = {}) =>
SetMetadata(WS_AUTH_CONFIG_KEY, config);
2 changes: 1 addition & 1 deletion src/conversations/conversations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class ConversationsController {
@Param('id') id: string,
@Req() req,
): Promise<ConversationDetailDto> {
return await this.conversationsService.getDetail(id, req.user);
return await this.conversationsService.getConversationForUser(id, req.user);
}

@Post(':id/title')
Expand Down
5 changes: 4 additions & 1 deletion src/conversations/conversations.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Conversation } from 'omniboxd/conversations/entities/conversation.entity';
import { ConversationsService } from 'omniboxd/conversations/conversations.service';
import { ConversationsController } from 'omniboxd/conversations/conversations.controller';
import { SharedConversationsController } from 'omniboxd/conversations/shared-conversations.controller';
import { MessagesModule } from '../messages/messages.module';
import { UserModule } from '../user/user.module';
import { TasksModule } from 'omniboxd/tasks/tasks.module';
import { SharesModule } from 'omniboxd/shares/shares.module';

@Module({
imports: [
MessagesModule,
UserModule,
TasksModule,
SharesModule,
TypeOrmModule.forFeature([Conversation]),
],
providers: [ConversationsService],
controllers: [ConversationsController],
controllers: [ConversationsController, SharedConversationsController],
exports: [ConversationsService],
})
export class ConversationsModule {}
52 changes: 43 additions & 9 deletions src/conversations/conversations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from 'omniboxd/messages/entities/message.entity';
import { WizardTaskService } from 'omniboxd/tasks/wizard-task.service';
import { Task } from 'omniboxd/tasks/tasks.entity';
import { Share } from 'omniboxd/shares/entities/share.entity';

const TASK_PRIORITY = 5;

Expand Down Expand Up @@ -50,6 +51,16 @@ export class ConversationsService {
return await this.conversationRepository.save(conversation);
}

async createConversationForShare(share: Share) {
const conversation = this.conversationRepository.create({
namespaceId: share.namespaceId,
userId: null,
title: '',
shareId: share.id,
});
return await this.conversationRepository.save(conversation);
}

async update(id: string, title: string) {
const conversation = await this.findOne(id);
conversation.title = title;
Expand Down Expand Up @@ -192,22 +203,17 @@ export class ConversationsService {
};
}

async getDetail(id: string, user: User): Promise<ConversationDetailDto> {
const conversation = await this.conversationRepository.findOneOrFail({
where: { id, userId: user.id },
});

private convertToConversationDetail(
conversation: Conversation,
messages: Message[],
): ConversationDetailDto {
const detail: ConversationDetailDto = {
id: conversation.id,
title: conversation.title,
created_at: conversation.createdAt.toISOString(),
updated_at: conversation.updatedAt?.toISOString(),
mapping: {},
};
const messages = await this.messagesService.findAll(
user.id,
conversation.id,
);
if (messages.length === 0) {
return detail;
}
Expand Down Expand Up @@ -251,6 +257,34 @@ export class ConversationsService {
return detail;
}

async getConversationForUser(
conversationId: string,
user: User,
): Promise<ConversationDetailDto> {
const conversation = await this.conversationRepository.findOneOrFail({
where: { id: conversationId, userId: user.id },
});
const messages = await this.messagesService.findAll(
user.id,
conversation.id,
);
return this.convertToConversationDetail(conversation, messages);
}

async getConversationForShare(
conversationId: string,
share: Share,
): Promise<ConversationDetailDto> {
const conversation = await this.conversationRepository.findOneOrFail({
where: { id: conversationId, shareId: share.id },
});
const messages = await this.messagesService.findAll(
undefined,
conversation.id,
);
return this.convertToConversationDetail(conversation, messages);
}

async findOne(id: string) {
return await this.conversationRepository.findOneOrFail({
where: { id },
Expand Down
7 changes: 5 additions & 2 deletions src/conversations/entities/conversation.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ export class Conversation extends Base {
@Column()
namespaceId: string;

@Column()
userId: string;
@Column('varchar', { nullable: true })
userId: string | null;

@Column()
title: string;

@Column('varchar', { nullable: true })
shareId: string | null;
}
35 changes: 35 additions & 0 deletions src/conversations/shared-conversations.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Controller, Get, Param, Post, UseInterceptors } from '@nestjs/common';
import { ConversationsService } from './conversations.service';
import { CookieAuth } from 'omniboxd/auth/decorators';
import {
ValidateShare,
ValidatedShare,
} from 'omniboxd/decorators/validate-share.decorator';
import { ValidateShareInterceptor } from 'omniboxd/interceptor/validate-share.interceptor';
import { Share } from 'omniboxd/shares/entities/share.entity';

@Controller('api/v1/shares/:shareId/conversations')
@UseInterceptors(ValidateShareInterceptor)
export class SharedConversationsController {
constructor(private readonly conversationsService: ConversationsService) {}

@CookieAuth({ onAuthFail: 'continue' })
@ValidateShare({ requireChat: true })
@Post()
async createConversationForShare(@ValidatedShare() share: Share) {
return await this.conversationsService.createConversationForShare(share);
}

@CookieAuth({ onAuthFail: 'continue' })
@ValidateShare({ requireChat: true })
@Get(':conversationId')
async getSharedConversation(
@Param('conversationId') conversationId: string,
@ValidatedShare() share: Share,
) {
return await this.conversationsService.getConversationForShare(
conversationId,
share,
);
}
}
31 changes: 31 additions & 0 deletions src/decorators/validate-share.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
createParamDecorator,
ExecutionContext,
SetMetadata,
applyDecorators,
} from '@nestjs/common';
import { Request } from 'express';

export const VALIDATE_SHARE_KEY = 'validate-share';

export interface ValidateShareOptions {
requireChat?: boolean;
}

/**
* Parameter decorator to inject the validated share into a method parameter
*/
export const ValidatedShare = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request: Request = ctx.switchToHttp().getRequest();
return (request as any).validatedShare;
},
);

/**
* Method decorator to automatically validate share before method execution
* Extracts shareId from route params, password from cookies, and userId from request
*/
export function ValidateShare(options: ValidateShareOptions = {}) {
return applyDecorators(SetMetadata(VALIDATE_SHARE_KEY, options));
}
73 changes: 73 additions & 0 deletions src/interceptor/validate-share.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { Request } from 'express';
import { SharesService } from 'omniboxd/shares/shares.service';
import {
VALIDATE_SHARE_KEY,
ValidateShareOptions,
} from 'omniboxd/decorators/validate-share.decorator';
import { ShareType } from 'omniboxd/shares/entities/share.entity';

@Injectable()
export class ValidateShareInterceptor implements NestInterceptor {
constructor(
private readonly reflector: Reflector,
private readonly sharesService: SharesService,
) {}

async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const validateOptions = this.reflector.get<ValidateShareOptions>(
VALIDATE_SHARE_KEY,
context.getHandler(),
);

if (!validateOptions) {
return next.handle();
}

const request: Request = context.switchToHttp().getRequest();

// Extract parameters using fixed parameter names
const shareId = request.params['shareId'];
const password = request.cookies?.['share-password'];
const userId = request.user?.id; // Assuming user is attached to request by auth middleware

if (!shareId) {
throw new Error(`Share ID parameter 'shareId' not found in request`);
}

// Validate the share
const validatedShare = await this.sharesService.getAndValidateShare(
shareId,
password,
userId,
);

// Additional chat validation if required
if (validateOptions.requireChat) {
if (
validatedShare.shareType !== ShareType.CHAT_ONLY &&
validatedShare.shareType !== ShareType.ALL
) {
throw new ForbiddenException(
'This share does not allow chat functionality',
);
}
}

// Attach the validated share to the request for the @ValidatedShare decorator
(request as any).validatedShare = validatedShare;

return next.handle();
}
}
4 changes: 2 additions & 2 deletions src/messages/entities/message.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export class Message extends Base {
@PrimaryGeneratedColumn()
id: string;

@Column()
userId: string;
@Column('varchar', { nullable: true })
userId: string | null;

@Column()
conversationId: string;
Expand Down
Loading