chore(src): 🔧 Update TypeScript files in src directory

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-18 10:42:29 -08:00
parent 1272c9dce7
commit d266717223
15 changed files with 204 additions and 147 deletions

View file

@ -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) {

View file

@ -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,

View file

@ -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
*/

View file

@ -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);

View file

@ -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(

View file

@ -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> {

View file

@ -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;

View file

@ -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,
);

View file

@ -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);

View file

@ -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')

View file

@ -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,

View file

@ -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,

View file

@ -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.
*/

View file

@ -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',
};
},
}),

View file

@ -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)}`);
}
}