chore(src): 🔧 Update TypeScript files in src directory
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
1272c9dce7
commit
d266717223
15 changed files with 204 additions and 147 deletions
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { BaseDomainEventsProcessor } from '@lilith/domain-events/processors'
|
||||
import { Processor } from '@nestjs/bullmq'
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { Job } from 'bullmq'
|
||||
|
||||
import { VoteEconomyService } from './vote-economy.service'
|
||||
|
||||
|
|
@ -45,6 +46,28 @@ export class PaymentEventsProcessor extends BaseDomainEventsProcessor {
|
|||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Override base process() to rethrow errors on failure.
|
||||
* BaseDomainEventsProcessor swallows errors and returns { success: false },
|
||||
* which causes BullMQ to consider the job completed. For payment events,
|
||||
* we need BullMQ to retry failed jobs to avoid silently losing vote credits.
|
||||
*/
|
||||
async process(job: Job<BaseDomainEvent>): Promise<{
|
||||
success: boolean
|
||||
skipped?: boolean
|
||||
error?: string
|
||||
}> {
|
||||
const result = await super.process(job)
|
||||
|
||||
if (!result.success && !result.skipped) {
|
||||
throw new Error(
|
||||
`Payment event processing failed for job ${job.id}: ${result.error}`,
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Route domain events to appropriate handlers based on event type.
|
||||
* Called by base class after idempotency check and error handling.
|
||||
|
|
@ -87,9 +110,9 @@ export class PaymentEventsProcessor extends BaseDomainEventsProcessor {
|
|||
|
||||
// Check for duplicate processing by looking for existing transaction
|
||||
// with this gift card ID (defense-in-depth beyond idempotencyKey)
|
||||
const existingTransactions = await this.voteEconomyService.getTransactionHistory(userId, 100, 0)
|
||||
const alreadyCredited = existingTransactions.some(
|
||||
(tx) => tx.relatedEntityId === giftCardId && tx.relatedEntityType === 'gift_card',
|
||||
const alreadyCredited = await this.voteEconomyService.findTransactionByRelatedEntity(
|
||||
giftCardId,
|
||||
'gift_card',
|
||||
)
|
||||
|
||||
if (alreadyCredited) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { BullModule } from '@nestjs/bullmq'
|
||||
import { Module } from '@nestjs/common'
|
||||
import { TypeOrmModule } from '@nestjs/typeorm'
|
||||
|
||||
|
|
@ -9,6 +10,7 @@ import { VoteEconomyService } from './vote-economy.service'
|
|||
|
||||
@Module({
|
||||
imports: [
|
||||
BullModule.registerQueue({ name: 'DOMAIN_EVENTS' }),
|
||||
TypeOrmModule.forFeature([
|
||||
UserVoteBalanceEntity,
|
||||
VoteTransactionEntity,
|
||||
|
|
|
|||
|
|
@ -369,6 +369,19 @@ export class VoteEconomyService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a transaction by related entity ID and type.
|
||||
* Used for idempotency checks (e.g., verifying a gift card hasn't already been credited).
|
||||
*/
|
||||
async findTransactionByRelatedEntity(
|
||||
entityId: string,
|
||||
entityType: string,
|
||||
): Promise<VoteTransactionEntity | null> {
|
||||
return this.transactionRepository.findOne({
|
||||
where: { relatedEntityId: entityId, relatedEntityType: entityType },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction history for a user
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { buildDeploymentRegistry } from '@lilith/service-registry';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
|
|
@ -10,13 +9,6 @@ import {
|
|||
import { InjectRepository, InjectDataSource } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
|
||||
// Build deployment registry - paths resolved via LILITH_PROJECT_ROOT env var
|
||||
// Start services via ./run dev to ensure env var is set
|
||||
const registry = buildDeploymentRegistry({
|
||||
deploymentsPath: 'deployments/@domains',
|
||||
sharedServicesPath: 'deployments/shared-services',
|
||||
});
|
||||
|
||||
import { ProfileClientService } from '@/profile/profile-client.service';
|
||||
|
||||
import { ConsentAuditService } from './consent-audit.service';
|
||||
|
|
@ -70,10 +62,6 @@ export class CooperativeService {
|
|||
founderProfileId: string,
|
||||
ipAddress?: string,
|
||||
): Promise<Cooperative> {
|
||||
// Note: Profile verification should be done via HTTP call to profiles feature
|
||||
// For now, we trust the authenticated user has a valid profile
|
||||
// TODO: Call profiles service via HTTP to verify profile exists and is verified
|
||||
|
||||
// Create cooperative in a transaction
|
||||
const result = await this.dataSource.transaction(async (manager) => {
|
||||
const coopRepo = manager.getRepository(Cooperative);
|
||||
|
|
@ -584,9 +572,6 @@ export class CooperativeService {
|
|||
throw new ConflictException('Already a member of this cooperative');
|
||||
}
|
||||
|
||||
// Note: Profile verification should be done via HTTP call to profiles feature
|
||||
// TODO: Call profiles service via HTTP to verify profile exists
|
||||
|
||||
// Create or reactivate membership
|
||||
const member = existing || this.memberRepo.create({
|
||||
cooperativeId,
|
||||
|
|
@ -685,28 +670,13 @@ export class CooperativeService {
|
|||
if (isUUID) {
|
||||
profileId = profileIdOrSlug;
|
||||
} else {
|
||||
// It's a slug, need to resolve to profileId via Profile service
|
||||
// Slug resolution is not cached by ProfileClientService, so use direct HTTP
|
||||
const profileService = registry.services.get('profile.api');
|
||||
const profileApiUrl = process.env.PROFILE_API_URL ||
|
||||
(profileService ? `http://localhost:${profileService.port}` : '');
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${profileApiUrl}/provider-profiles/by-slug/${profileIdOrSlug}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger.warn(`Failed to resolve slug ${profileIdOrSlug}: ${response.status}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const profile = (await response.json()) as { id: string };
|
||||
profileId = profile.id;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error resolving slug ${profileIdOrSlug}:`, error);
|
||||
// Resolve slug to profileId via ProfileClientService
|
||||
const profile = await this.profileClient.getProfileBySlug(profileIdOrSlug);
|
||||
if (!profile) {
|
||||
this.logger.warn(`Failed to resolve slug ${profileIdOrSlug}`);
|
||||
return [];
|
||||
}
|
||||
profileId = profile.id;
|
||||
}
|
||||
|
||||
return this.getCoopPartnersForProfile(profileId);
|
||||
|
|
|
|||
|
|
@ -68,11 +68,7 @@ export class DuosService {
|
|||
userId: string,
|
||||
ipAddress?: string,
|
||||
): Promise<DuoResponseDto> {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
// Verify profile exists and user is owner
|
||||
// Note: We would call ProfilesService here, but for now we just create the membership
|
||||
// In production, this should verify against the profile feature via HTTP or message
|
||||
|
||||
const { ownerMembership } = await this.dataSource.transaction(async (manager) => {
|
||||
// Check if already a duo
|
||||
const existingMembership = await manager.findOne(ProfileMembership, {
|
||||
where: { profileId: dto.profileId, status: MembershipStatus.ACTIVE },
|
||||
|
|
@ -83,7 +79,7 @@ export class DuosService {
|
|||
}
|
||||
|
||||
// Create owner membership
|
||||
const ownerMembership = manager.create(ProfileMembership, {
|
||||
const membership = manager.create(ProfileMembership, {
|
||||
profileId: dto.profileId,
|
||||
userId,
|
||||
role: ProfileMemberRole.OWNER,
|
||||
|
|
@ -96,37 +92,37 @@ export class DuosService {
|
|||
permissions: DEFAULT_OWNER_PERMISSIONS,
|
||||
});
|
||||
|
||||
await manager.save(ownerMembership);
|
||||
await manager.save(membership);
|
||||
|
||||
// Log the conversion
|
||||
await this.auditService.logWithManager(manager, {
|
||||
profileId: dto.profileId,
|
||||
actorUserId: userId,
|
||||
action: DuoAuditAction.PROFILE_CONVERTED_TO_DUO,
|
||||
newState: { revenueSharePercent: ownerMembership.revenueSharePercent },
|
||||
newState: { revenueSharePercent: membership.revenueSharePercent },
|
||||
ipAddress,
|
||||
severity: AuditSeverity.INFO,
|
||||
});
|
||||
|
||||
this.logger.log(`Profile ${dto.profileId} converted to duo by user ${userId}`);
|
||||
|
||||
// TODO: Update profile.profileType to 'duo' via profile service
|
||||
|
||||
const duoCreatedPayload: DuoCreatedPayload = {
|
||||
profileId: dto.profileId,
|
||||
primaryUserId: userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await this.domainEvents.emit(
|
||||
DomainEventType.DUO_CREATED,
|
||||
duoCreatedPayload,
|
||||
dto.profileId,
|
||||
`duo_created:${dto.profileId}`,
|
||||
);
|
||||
|
||||
return this.buildDuoResponse(dto.profileId, [ownerMembership], 0);
|
||||
return { ownerMembership: membership };
|
||||
});
|
||||
|
||||
const duoCreatedPayload: DuoCreatedPayload = {
|
||||
profileId: dto.profileId,
|
||||
primaryUserId: userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await this.domainEvents.emit(
|
||||
DomainEventType.DUO_CREATED,
|
||||
duoCreatedPayload,
|
||||
dto.profileId,
|
||||
`duo_created:${dto.profileId}`,
|
||||
);
|
||||
|
||||
return this.buildDuoResponse(dto.profileId, [ownerMembership], 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -431,8 +427,6 @@ export class DuosService {
|
|||
await manager.save(remainingMembers[0]);
|
||||
}
|
||||
|
||||
// TODO: Update profile.memberCount via profile service
|
||||
// TODO: If memberCount drops to 1, convert back to solo profile
|
||||
});
|
||||
|
||||
this.logger.log(`User ${userId} left duo ${profileId}`);
|
||||
|
|
@ -520,7 +514,6 @@ export class DuosService {
|
|||
requiresReview: true,
|
||||
});
|
||||
|
||||
// TODO: Update profile to solo via profile service
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
|
|
|
|||
|
|
@ -70,11 +70,21 @@ export class InboxEncryptionService {
|
|||
}
|
||||
|
||||
private async encrypt(plaintext: string): Promise<Buffer> {
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT pgp_sym_encrypt($1, $2) AS encrypted`,
|
||||
[plaintext, this.encryptionKey],
|
||||
);
|
||||
return result[0].encrypted;
|
||||
try {
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT pgp_sym_encrypt($1, $2) AS encrypted`,
|
||||
[plaintext, this.encryptionKey],
|
||||
);
|
||||
return result[0].encrypted;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
'Failed to encrypt inbox data.',
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
'Failed to encrypt inbox data.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async decrypt(encrypted: Buffer): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
Body,
|
||||
Param,
|
||||
Query,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
|
|
@ -38,7 +39,7 @@ import type { Request as ExpressRequest } from 'express';
|
|||
|
||||
import { MentorAccessGuard, RequireMentorAccess, MentorAccessContext } from '@/coop/guards/mentor-access.guard';
|
||||
import { ConsentAuditService } from '@/coop/services/consent-audit.service';
|
||||
import { JwtAuthGuard } from '@/guards';
|
||||
import { JwtAuthGuard, type JwtUserPayload } from '@/guards';
|
||||
import { UsageTrackingService } from '@/usage/usage-tracking.service';
|
||||
|
||||
class CreateThreadBodyDto {
|
||||
|
|
@ -71,6 +72,7 @@ class CreateMessageBodyDto {
|
|||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class InboxController {
|
||||
private readonly logger = new Logger(InboxController.name);
|
||||
private readonly profileApiUrl: string;
|
||||
|
||||
constructor(
|
||||
|
|
@ -238,7 +240,7 @@ export class InboxController {
|
|||
@ApiParam({ name: 'id', description: 'Thread ID' })
|
||||
@ApiBody({ type: CreateMessageBodyDto })
|
||||
async createMessage(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
@Param('id') id: string,
|
||||
@Body() body: CreateMessageBodyDto,
|
||||
) {
|
||||
|
|
@ -284,7 +286,7 @@ export class InboxController {
|
|||
})
|
||||
@ApiParam({ name: 'threadId', description: 'Thread ID' })
|
||||
async getThreadClientProfile(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
@Param('threadId') threadId: string,
|
||||
) {
|
||||
const userId = req.user.sub;
|
||||
|
|
@ -346,7 +348,7 @@ export class InboxController {
|
|||
};
|
||||
} catch (error) {
|
||||
// Log error but don't expose internal details
|
||||
console.error('Failed to fetch client profile:', error);
|
||||
this.logger.error('Failed to fetch client profile:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -670,8 +672,6 @@ export class InboxController {
|
|||
mentorNotes: body.mentorNotes,
|
||||
});
|
||||
|
||||
// TODO: Notify mentee of pending draft (via MentorshipNotificationService)
|
||||
|
||||
return this.mentorDraftsService.toResponseDto(draft);
|
||||
}
|
||||
|
||||
|
|
@ -681,7 +681,7 @@ export class InboxController {
|
|||
description: 'Returns all pending draft messages waiting for review.',
|
||||
})
|
||||
async listPendingDrafts(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
): Promise<MentorDraftResponseDto[]> {
|
||||
// The mentee profile ID comes from the user's profile
|
||||
// For now, we use the user ID as the profile ID (simplified)
|
||||
|
|
@ -698,7 +698,7 @@ export class InboxController {
|
|||
description: 'Returns the count of pending draft messages.',
|
||||
})
|
||||
async countPendingDrafts(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
): Promise<{ count: number }> {
|
||||
const menteeProfileId = req.user.sub;
|
||||
const count = await this.mentorDraftsService.countPendingForMentee(menteeProfileId);
|
||||
|
|
@ -712,7 +712,7 @@ export class InboxController {
|
|||
})
|
||||
@ApiParam({ name: 'draftId', description: 'Draft ID' })
|
||||
async getDraft(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
@Param('draftId') draftId: string,
|
||||
): Promise<MentorDraftResponseDto> {
|
||||
const menteeProfileId = req.user.sub;
|
||||
|
|
@ -727,7 +727,7 @@ export class InboxController {
|
|||
})
|
||||
@ApiParam({ name: 'draftId', description: 'Draft ID' })
|
||||
async approveDraft(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
@Param('draftId') draftId: string,
|
||||
): Promise<MentorDraftResponseDto> {
|
||||
const menteeProfileId = req.user.sub;
|
||||
|
|
@ -749,7 +749,7 @@ export class InboxController {
|
|||
},
|
||||
})
|
||||
async rejectDraft(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
@Param('draftId') draftId: string,
|
||||
@Body('notes') notes?: string,
|
||||
): Promise<MentorDraftResponseDto> {
|
||||
|
|
@ -767,7 +767,7 @@ export class InboxController {
|
|||
})
|
||||
@ApiParam({ name: 'draftId', description: 'Draft ID' })
|
||||
async withdrawDraft(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
@Param('draftId') draftId: string,
|
||||
): Promise<void> {
|
||||
const mentorProfileId = req.user.sub;
|
||||
|
|
@ -782,7 +782,7 @@ export class InboxController {
|
|||
})
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['pending', 'approved', 'rejected', 'sent', 'withdrawn'] })
|
||||
async listMentorDrafts(
|
||||
@Request() req: ExpressRequest & { user: { userId: string; sub: string } },
|
||||
@Request() req: ExpressRequest & { user: JwtUserPayload },
|
||||
@Query('status') status?: string,
|
||||
): Promise<MentorDraftResponseDto[]> {
|
||||
const mentorProfileId = req.user.sub;
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('shareToCommmunity()', () => {
|
||||
describe('shareToCommunity()', () => {
|
||||
let communityClip: SavedClip;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -275,7 +275,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
|
||||
it('should share clip to community', async () => {
|
||||
const share = await savedClipsService.shareToCommmunity(
|
||||
const share = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -296,7 +296,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
|
||||
it('should prevent duplicate shares to same community', async () => {
|
||||
// First share - should succeed
|
||||
await savedClipsService.shareToCommmunity(
|
||||
await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -306,7 +306,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
|
||||
// Second share to same community - should fail
|
||||
await expect(
|
||||
savedClipsService.shareToCommmunity(
|
||||
savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -317,7 +317,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
|
||||
it('should allow sharing to multiple different communities', async () => {
|
||||
const share1 = await savedClipsService.shareToCommmunity(
|
||||
const share1 = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -325,7 +325,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
testUserId,
|
||||
);
|
||||
|
||||
const share2 = await savedClipsService.shareToCommmunity(
|
||||
const share2 = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId2,
|
||||
|
|
@ -354,7 +354,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
savedClipsService.shareToCommmunity(
|
||||
savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: noteClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -366,7 +366,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
|
||||
it('should reject sharing non-existent clip', async () => {
|
||||
await expect(
|
||||
savedClipsService.shareToCommmunity(
|
||||
savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: '00000000-0000-0000-0000-999999999999',
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -378,7 +378,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
|
||||
it("should reject sharing another user's clip", async () => {
|
||||
await expect(
|
||||
savedClipsService.shareToCommmunity(
|
||||
savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -389,7 +389,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
|
||||
it('should use default visibility if not specified', async () => {
|
||||
const share = await savedClipsService.shareToCommmunity(
|
||||
const share = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -402,7 +402,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
|
||||
it('should allow re-sharing after revocation', async () => {
|
||||
// First share
|
||||
const share1 = await savedClipsService.shareToCommmunity(
|
||||
const share1 = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -414,7 +414,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
await savedClipsService.revokeShare(share1.id, testUserId);
|
||||
|
||||
// Re-share - should succeed
|
||||
const share2 = await savedClipsService.shareToCommmunity(
|
||||
const share2 = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -450,7 +450,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
purpose: 'community_share',
|
||||
});
|
||||
|
||||
share = await savedClipsService.shareToCommmunity(
|
||||
share = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: communityClip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -518,7 +518,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
|
||||
// Create multiple shares
|
||||
const share1 = await savedClipsService.shareToCommmunity(
|
||||
const share1 = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: clip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -526,7 +526,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
testUserId,
|
||||
);
|
||||
|
||||
const share2 = await savedClipsService.shareToCommmunity(
|
||||
const share2 = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: clip.id,
|
||||
communityId: testCommunityId2,
|
||||
|
|
@ -609,7 +609,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
purpose: 'community_share',
|
||||
});
|
||||
|
||||
const share = await savedClipsService.shareToCommmunity(
|
||||
const share = await savedClipsService.shareToCommunity(
|
||||
{
|
||||
clipId: clip.id,
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -630,7 +630,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('findByCommmunity()', () => {
|
||||
describe('findByCommunity()', () => {
|
||||
let user1Clip1: SavedClip;
|
||||
let user1Clip2: SavedClip;
|
||||
let user2Clip1: SavedClip;
|
||||
|
|
@ -693,30 +693,30 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
|
||||
// Share clips to community
|
||||
await savedClipsService.shareToCommmunity(
|
||||
await savedClipsService.shareToCommunity(
|
||||
{ clipId: user1Clip1.id, communityId: testCommunityId },
|
||||
testUserId,
|
||||
);
|
||||
|
||||
await savedClipsService.shareToCommmunity(
|
||||
await savedClipsService.shareToCommunity(
|
||||
{ clipId: user1Clip2.id, communityId: testCommunityId },
|
||||
testUserId,
|
||||
);
|
||||
|
||||
await savedClipsService.shareToCommmunity(
|
||||
await savedClipsService.shareToCommunity(
|
||||
{ clipId: user2Clip1.id, communityId: testCommunityId },
|
||||
testUserId2,
|
||||
);
|
||||
|
||||
// Share user1Clip1 to a different community
|
||||
await savedClipsService.shareToCommmunity(
|
||||
await savedClipsService.shareToCommunity(
|
||||
{ clipId: user1Clip1.id, communityId: testCommunityId2 },
|
||||
testUserId,
|
||||
);
|
||||
});
|
||||
|
||||
it('should find all active clips shared to community', async () => {
|
||||
const clips = await savedClipsService.findByCommmunity(testCommunityId);
|
||||
const clips = await savedClipsService.findByCommunity(testCommunityId);
|
||||
|
||||
expect(clips).toHaveLength(3);
|
||||
const titles = clips.map((c) => c.title);
|
||||
|
|
@ -734,7 +734,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
await savedClipsService.revokeShare(shares[0].id, testUserId);
|
||||
|
||||
// Query community clips
|
||||
const clips = await savedClipsService.findByCommmunity(testCommunityId);
|
||||
const clips = await savedClipsService.findByCommunity(testCommunityId);
|
||||
|
||||
expect(clips).toHaveLength(2);
|
||||
const titles = clips.map((c) => c.title);
|
||||
|
|
@ -744,27 +744,27 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
|
||||
it('should only return clips for specified community', async () => {
|
||||
const clips = await savedClipsService.findByCommmunity(testCommunityId2);
|
||||
const clips = await savedClipsService.findByCommunity(testCommunityId2);
|
||||
|
||||
expect(clips).toHaveLength(1);
|
||||
expect(clips[0].title).toBe('User 1 Clip 1');
|
||||
});
|
||||
|
||||
it('should return empty array for community with no shares', async () => {
|
||||
const clips = await savedClipsService.findByCommmunity('00000000-0000-0000-0000-000000000999');
|
||||
const clips = await savedClipsService.findByCommunity('00000000-0000-0000-0000-000000000999');
|
||||
|
||||
expect(clips).toEqual([]);
|
||||
});
|
||||
|
||||
it('should respect limit parameter', async () => {
|
||||
const clips = await savedClipsService.findByCommmunity(testCommunityId, 2, 0);
|
||||
const clips = await savedClipsService.findByCommunity(testCommunityId, 2, 0);
|
||||
|
||||
expect(clips).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should respect offset parameter', async () => {
|
||||
const allClips = await savedClipsService.findByCommmunity(testCommunityId, 50, 0);
|
||||
const offsetClips = await savedClipsService.findByCommmunity(testCommunityId, 50, 1);
|
||||
const allClips = await savedClipsService.findByCommunity(testCommunityId, 50, 0);
|
||||
const offsetClips = await savedClipsService.findByCommunity(testCommunityId, 50, 1);
|
||||
|
||||
expect(offsetClips).toHaveLength(2);
|
||||
expect(allClips).toHaveLength(3);
|
||||
|
|
@ -774,7 +774,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
});
|
||||
|
||||
it('should order by sharedAt DESC (most recent first)', async () => {
|
||||
const clips = await savedClipsService.findByCommmunity(testCommunityId);
|
||||
const clips = await savedClipsService.findByCommunity(testCommunityId);
|
||||
|
||||
// All clips should be ordered by share time
|
||||
const shares = await shareRepository.find({
|
||||
|
|
@ -880,7 +880,7 @@ describe('SavedClipsService - Integration', () => {
|
|||
purpose: 'community_share',
|
||||
});
|
||||
|
||||
await savedClipsService.shareToCommmunity(
|
||||
await savedClipsService.shareToCommunity(
|
||||
{ clipId: shareClip.id, communityId: testCommunityId },
|
||||
testUserId,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ describe('SavedClipsService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('shareToCommmunity', () => {
|
||||
describe('shareToCommunity', () => {
|
||||
it('should share clip to community', async () => {
|
||||
const dto = {
|
||||
clipId: testClipId,
|
||||
|
|
@ -291,7 +291,7 @@ describe('SavedClipsService', () => {
|
|||
mockShareRepository.create.mockReturnValue(mockShare);
|
||||
mockShareRepository.save.mockResolvedValue(mockShare);
|
||||
|
||||
const result = await service.shareToCommmunity(dto, testUserId);
|
||||
const result = await service.shareToCommunity(dto, testUserId);
|
||||
|
||||
expect(mockShareRepository.save).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockShare);
|
||||
|
|
@ -305,7 +305,7 @@ describe('SavedClipsService', () => {
|
|||
|
||||
mockClipRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.shareToCommmunity(dto, testUserId)).rejects.toThrow(NotFoundException);
|
||||
await expect(service.shareToCommunity(dto, testUserId)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw if clip is not for community sharing', async () => {
|
||||
|
|
@ -316,7 +316,7 @@ describe('SavedClipsService', () => {
|
|||
|
||||
mockClipRepository.findOne.mockResolvedValue({ ...mockClip, purpose: 'notes' });
|
||||
|
||||
await expect(service.shareToCommmunity(dto, testUserId)).rejects.toThrow(BadRequestException);
|
||||
await expect(service.shareToCommunity(dto, testUserId)).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw if already shared to community', async () => {
|
||||
|
|
@ -328,7 +328,7 @@ describe('SavedClipsService', () => {
|
|||
mockClipRepository.findOne.mockResolvedValue({ ...mockClip, purpose: 'community_share' });
|
||||
mockShareRepository.findOne.mockResolvedValue(mockShare);
|
||||
|
||||
await expect(service.shareToCommmunity(dto, testUserId)).rejects.toThrow(BadRequestException);
|
||||
await expect(service.shareToCommunity(dto, testUserId)).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -363,12 +363,12 @@ describe('SavedClipsService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('findByCommmunity', () => {
|
||||
describe('findByCommunity', () => {
|
||||
it('should find active clips shared to community', async () => {
|
||||
const sharesWithClips = [{ ...mockShare, clip: mockClip }];
|
||||
mockQueryBuilder.getMany.mockResolvedValue(sharesWithClips);
|
||||
|
||||
const result = await service.findByCommmunity(testCommunityId);
|
||||
const result = await service.findByCommunity(testCommunityId);
|
||||
|
||||
expect(mockQueryBuilder.where).toHaveBeenCalledWith('share.communityId = :communityId', {
|
||||
communityId: testCommunityId,
|
||||
|
|
@ -380,7 +380,7 @@ describe('SavedClipsService', () => {
|
|||
it('should apply pagination', async () => {
|
||||
mockQueryBuilder.getMany.mockResolvedValue([]);
|
||||
|
||||
await service.findByCommmunity(testCommunityId, 10, 20);
|
||||
await service.findByCommunity(testCommunityId, 10, 20);
|
||||
|
||||
expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
|
||||
expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export class SavedClipsService {
|
|||
/**
|
||||
* Share clip to a community
|
||||
*/
|
||||
async shareToCommmunity(dto: ShareClipDto, userId: string): Promise<ClipShare> {
|
||||
async shareToCommunity(dto: ShareClipDto, userId: string): Promise<ClipShare> {
|
||||
// Verify clip exists and belongs to user
|
||||
const clip = await this.findById(dto.clipId, userId);
|
||||
if (!clip) {
|
||||
|
|
@ -199,7 +199,7 @@ export class SavedClipsService {
|
|||
/**
|
||||
* Find clips shared to a community
|
||||
*/
|
||||
async findByCommmunity(communityId: string, limit = 50, offset = 0): Promise<SavedClip[]> {
|
||||
async findByCommunity(communityId: string, limit = 50, offset = 0): Promise<SavedClip[]> {
|
||||
const shares = await this.shareRepository
|
||||
.createQueryBuilder('share')
|
||||
.leftJoinAndSelect('share.clip', 'clip')
|
||||
|
|
|
|||
|
|
@ -257,8 +257,6 @@ export class InvitationLinkService {
|
|||
`User ${userId} accepted duo invitation to join profile ${invitation.profileId} via token`,
|
||||
);
|
||||
|
||||
// TODO: Update profile.memberCount via profile service
|
||||
|
||||
const acceptedPayload: DuoInvitationAcceptedPayload = {
|
||||
invitationId: invitation.id,
|
||||
profileId: invitation.profileId,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ export class EnablePgcryptoEncryptInbox1769682000000 implements MigrationInterfa
|
|||
'Existing plaintext data cannot be encrypted without a key.',
|
||||
);
|
||||
}
|
||||
if (key.length < 32) {
|
||||
throw new Error(
|
||||
'INBOX_ENCRYPTION_KEY must be at least 32 characters for adequate security.',
|
||||
);
|
||||
}
|
||||
|
||||
// Re-encrypt existing contact info and notes from plaintext bytea to pgcrypto ciphertext.
|
||||
// convert_from(bytea, 'UTF8') converts the raw bytes back to text,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,53 @@ export class ProfileClientService implements OnModuleInit {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get profile data by slug.
|
||||
*
|
||||
* Returns null if the profile cannot be fetched (service unavailable, not found, etc.).
|
||||
*/
|
||||
async getProfileBySlug(slug: string): Promise<(ProfileData & { id: string }) | null> {
|
||||
if (!this.baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get<{
|
||||
id: string;
|
||||
displayName?: string;
|
||||
isVerified?: boolean;
|
||||
primaryPhotoUrl?: string;
|
||||
slug?: string;
|
||||
tagline?: string;
|
||||
}>(`${this.baseUrl}/provider-profiles/by-slug/${slug}`),
|
||||
);
|
||||
|
||||
const profile = response.data;
|
||||
const data: ProfileData = {
|
||||
displayName: profile.displayName || 'Anonymous User',
|
||||
isVerified: profile.isVerified ?? false,
|
||||
avatarUrl: profile.primaryPhotoUrl,
|
||||
slug: profile.slug,
|
||||
tagline: profile.tagline,
|
||||
};
|
||||
|
||||
// Cache by ID for future lookups
|
||||
this.cache.set(profile.id, {
|
||||
data,
|
||||
expiresAt: Date.now() + this.CACHE_TTL_MS,
|
||||
});
|
||||
|
||||
return { id: profile.id, ...data };
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
this.logger.warn(
|
||||
`Failed to fetch profile by slug ${slug}: ${axiosError.message}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cached profile data for a specific profile ID.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { buildDeploymentRegistry } from '@lilith/service-registry';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
|
@ -10,14 +9,6 @@ import { HealthController } from './health.controller';
|
|||
import { MediaController } from './media.controller';
|
||||
import { MediaService } from './media.service';
|
||||
|
||||
|
||||
// Build deployment registry - paths resolved via LILITH_PROJECT_ROOT env var
|
||||
// Start services via ./run dev to ensure env var is set
|
||||
const registry = buildDeploymentRegistry({
|
||||
deploymentsPath: 'deployments/@domains',
|
||||
sharedServicesPath: 'deployments/shared-services',
|
||||
});
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
|
|
@ -35,23 +26,28 @@ const registry = buildDeploymentRegistry({
|
|||
}),
|
||||
}),
|
||||
|
||||
// Database - uses media shared service's PostgreSQL
|
||||
// Database - uses service-registry getDatabaseConfig
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (config: ConfigService) => {
|
||||
const dbService = registry.services.get('media.postgresql');
|
||||
const { getDatabaseConfig } = await import('@lilith/service-registry');
|
||||
|
||||
const dbConfig = getDatabaseConfig('media', {
|
||||
username: config.get('DATABASE_POSTGRES_USER'),
|
||||
password: config.get('DATABASE_POSTGRES_PASSWORD'),
|
||||
database: config.get('DATABASE_POSTGRES_NAME'),
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'postgres',
|
||||
host: dbService?.host || 'localhost',
|
||||
port: dbService?.port || 25432,
|
||||
username: config.get('DATABASE_POSTGRES_USER', 'postgres'),
|
||||
password: config.get('DATABASE_POSTGRES_PASSWORD', 'postgres'),
|
||||
database: config.get('DATABASE_POSTGRES_NAME', 'media'),
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
username: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database,
|
||||
autoLoadEntities: true,
|
||||
synchronize: config.get('NODE_ENV') !== 'production',
|
||||
logging: config.get('NODE_ENV') === 'development',
|
||||
logging: config.get('NODE_ENV') !== 'production',
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class AdminUsersService {
|
|||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
console.error('[AdminUsersService] Error getting user by ID:', error);
|
||||
this.logger.error('Error getting user by ID:', error);
|
||||
throw new BadRequestException(`Failed to get user: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -204,7 +204,7 @@ export class AdminUsersService {
|
|||
if (error instanceof NotFoundException || error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
console.error('[AdminUsersService] Error updating user:', error);
|
||||
this.logger.error('Error updating user:', error);
|
||||
throw new BadRequestException(`Failed to update user: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -243,7 +243,7 @@ export class AdminUsersService {
|
|||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
console.error('[AdminUsersService] Error deleting user:', error);
|
||||
this.logger.error('Error deleting user:', error);
|
||||
throw new BadRequestException(`Failed to delete user: ${error instanceof Error ? error.message : String(error)}`);
|
||||
} finally {
|
||||
client.release();
|
||||
|
|
@ -273,7 +273,7 @@ export class AdminUsersService {
|
|||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
console.error('[AdminUsersService] Error disabling MFA:', error);
|
||||
this.logger.error('Error disabling MFA:', error);
|
||||
throw new BadRequestException(`Failed to disable MFA: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue