feat: implement privacy-aware profile visibility and activity status filtering using optional authentication guard
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
} from './dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { OptionalAuthGuard } from '../auth/guards/optional-auth.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
@@ -243,11 +245,13 @@ export class AgentsController {
|
||||
// Public parameterized routes - MUST come after specific routes like profile/*
|
||||
@Get(':id')
|
||||
@Public()
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@ApiOperation({ summary: 'Get agent profile by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Agent profile retrieved successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Agent profile not found' })
|
||||
async getAgentById(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.agentsService.getProfileById(id);
|
||||
async getAgentById(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
|
||||
const requestingUserId = req.user?.id || null;
|
||||
return this.agentsService.getProfileById(id, requestingUserId);
|
||||
}
|
||||
|
||||
@Get(':id/field-values')
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { AgentsController } from './agents.controller';
|
||||
import { AgentsService } from './agents.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { ConnectionRequestsModule } from '../connection-requests/connection-requests.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, ConnectionRequestsModule],
|
||||
controllers: [AgentsController],
|
||||
providers: [AgentsService],
|
||||
exports: [AgentsService],
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
SaveFieldValuesDto,
|
||||
} from './dto';
|
||||
import { Prisma, Prisma as PrismaTypes, VerificationStatus } from '@prisma/client';
|
||||
import { ConnectionRequestsService } from '../connection-requests/connection-requests.service';
|
||||
|
||||
function generateSlug(firstName: string, lastName: string): string {
|
||||
const base = `${firstName}-${lastName}`
|
||||
@@ -24,7 +26,24 @@ function generateSlug(firstName: string, lastName: string): string {
|
||||
|
||||
@Injectable()
|
||||
export class AgentsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private connectionRequestsService: ConnectionRequestsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get profile_visibility setting from user's privacy preferences
|
||||
*/
|
||||
private getProfileVisibility(privacyPreferences: any): string {
|
||||
return privacyPreferences?.privacySettings?.profile_visibility || 'public';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activity_status setting from user's privacy preferences
|
||||
*/
|
||||
private getActivityStatus(privacyPreferences: any): string {
|
||||
return privacyPreferences?.privacySettings?.activity_status || 'public';
|
||||
}
|
||||
|
||||
async createProfile(userId: string, dto: CreateAgentProfileDto) {
|
||||
// Check if user already has an agent profile
|
||||
@@ -81,7 +100,7 @@ export class AgentsService {
|
||||
return profile;
|
||||
}
|
||||
|
||||
async getProfileById(profileId: string) {
|
||||
async getProfileById(profileId: string, requestingUserId?: string) {
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { id: profileId },
|
||||
include: {
|
||||
@@ -90,6 +109,7 @@ export class AgentsService {
|
||||
id: true,
|
||||
email: true,
|
||||
avatar: true,
|
||||
privacyPreferences: true,
|
||||
},
|
||||
},
|
||||
agentType: true,
|
||||
@@ -100,7 +120,30 @@ export class AgentsService {
|
||||
throw new NotFoundException('Agent profile not found');
|
||||
}
|
||||
|
||||
return profile;
|
||||
const visibility = this.getProfileVisibility(profile.user.privacyPreferences);
|
||||
|
||||
// Own profile — always visible
|
||||
if (requestingUserId && requestingUserId === profile.user.id) {
|
||||
const { privacyPreferences, ...userWithoutPrefs } = profile.user;
|
||||
return { ...profile, user: userWithoutPrefs };
|
||||
}
|
||||
|
||||
if (visibility === 'private') {
|
||||
throw new ForbiddenException('This profile is not available');
|
||||
}
|
||||
|
||||
if (visibility === 'connections' && requestingUserId) {
|
||||
const connected = await this.connectionRequestsService.isConnected(requestingUserId, profile.user.id);
|
||||
if (!connected) {
|
||||
throw new ForbiddenException('This profile is only visible to connections');
|
||||
}
|
||||
} else if (visibility === 'connections' && !requestingUserId) {
|
||||
throw new ForbiddenException('This profile is only visible to connections');
|
||||
}
|
||||
|
||||
// Strip privacyPreferences from response
|
||||
const { privacyPreferences, ...userWithoutPrefs } = profile.user;
|
||||
return { ...profile, user: userWithoutPrefs };
|
||||
}
|
||||
|
||||
async updateProfile(userId: string, dto: UpdateAgentProfileDto) {
|
||||
@@ -197,8 +240,17 @@ export class AgentsService {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build where clause — only show admin-approved profiles in search
|
||||
// Exclude agents who set profile_visibility to 'private'
|
||||
const where: Prisma.AgentProfileWhereInput = {
|
||||
verificationStatus: 'APPROVED',
|
||||
NOT: {
|
||||
user: {
|
||||
privacyPreferences: {
|
||||
path: ['privacySettings', 'profile_visibility'],
|
||||
equals: 'private',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (search) {
|
||||
@@ -484,6 +536,14 @@ export class AgentsService {
|
||||
isVerified: true,
|
||||
isFeatured: true,
|
||||
verificationStatus: 'APPROVED',
|
||||
NOT: {
|
||||
user: {
|
||||
privacyPreferences: {
|
||||
path: ['privacySettings', 'profile_visibility'],
|
||||
equals: 'private',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: limit,
|
||||
orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }],
|
||||
@@ -508,6 +568,14 @@ export class AgentsService {
|
||||
isVerified: true,
|
||||
totalReviews: { gte: 5 },
|
||||
verificationStatus: 'APPROVED',
|
||||
NOT: {
|
||||
user: {
|
||||
privacyPreferences: {
|
||||
path: ['privacySettings', 'profile_visibility'],
|
||||
equals: 'private',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: limit,
|
||||
orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }],
|
||||
|
||||
Reference in New Issue
Block a user