feat: implement privacy-aware profile visibility and activity status filtering using optional authentication guard

This commit is contained in:
pradeepkumar
2026-03-28 18:46:49 +05:30
parent ba931c350b
commit a71145f6a6
8 changed files with 235 additions and 38 deletions

View File

@@ -12,6 +12,8 @@ import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { MessagesService } from './messages.service';
import { SupportChatService } from '../support-chat/support-chat.service';
import { ConnectionRequestsService } from '../connection-requests/connection-requests.service';
import { PrismaService } from '../prisma/prisma.service';
import { CreateMessageDto } from './dto';
import { Logger } from '@nestjs/common';
@@ -37,6 +39,8 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
constructor(
private readonly messagesService: MessagesService,
private readonly supportChatService: SupportChatService,
private readonly connectionRequestsService: ConnectionRequestsService,
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
@@ -415,14 +419,46 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
}
/**
* Broadcast user online/offline status
* Broadcast user online/offline status respecting activity_status privacy setting
*/
private broadcastUserStatus(userId: string, isOnline: boolean) {
this.server.emit('user_status_change', {
private async broadcastUserStatus(userId: string, isOnline: boolean) {
const statusEvent = {
userId,
isOnline,
lastSeenAt: isOnline ? null : new Date(),
});
};
try {
// Get user's activity_status privacy setting
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { privacyPreferences: true },
});
const prefs = user?.privacyPreferences as any;
const activityStatus = prefs?.privacySettings?.activity_status || 'public';
if (activityStatus === 'private') {
// Don't broadcast status to anyone
return;
}
if (activityStatus === 'connections') {
// Only send status to connected users
const connectedIds = await this.connectionRequestsService.getConnectedUserIds(userId);
for (const connectedId of connectedIds) {
this.sendToUser(connectedId, 'user_status_change', statusEvent);
}
return;
}
// 'public' — broadcast to everyone
this.server.emit('user_status_change', statusEvent);
} catch (err) {
// Fallback to public broadcast if privacy check fails
this.logger.warn(`Privacy check failed for ${userId}, broadcasting publicly: ${err.message}`);
this.server.emit('user_status_change', statusEvent);
}
}
/**

View File

@@ -6,11 +6,13 @@ import { MessagesService } from './messages.service';
import { MessagesGateway } from './messages.gateway';
import { PrismaModule } from '../prisma/prisma.module';
import { SupportChatModule } from '../support-chat/support-chat.module';
import { ConnectionRequestsModule } from '../connection-requests/connection-requests.module';
@Module({
imports: [
PrismaModule,
SupportChatModule,
ConnectionRequestsModule,
ConfigModule,
JwtModule.registerAsync({
imports: [ConfigModule],

View File

@@ -16,6 +16,17 @@ export class MessagesService {
private readonly eventEmitter: EventEmitter2,
) {}
/**
* Check if a user's activity status allows showing online status.
* Connected users always see online status for 'connections' setting.
*/
private shouldShowOnlineStatus(privacyPreferences: any, isConnected = true): boolean {
const activityStatus = (privacyPreferences as any)?.privacySettings?.activity_status || 'public';
if (activityStatus === 'public') return true;
if (activityStatus === 'connections' && isConnected) return true;
return false;
}
/**
* Validate if a user can message an agent (must have ACCEPTED connection)
*/
@@ -87,6 +98,7 @@ export class MessagesService {
select: {
isOnline: true,
lastSeenAt: true,
privacyPreferences: true,
},
},
},
@@ -96,6 +108,7 @@ export class MessagesService {
id: true,
isOnline: true,
lastSeenAt: true,
privacyPreferences: true,
userProfile: {
select: {
firstName: true,
@@ -130,8 +143,10 @@ export class MessagesService {
}
// Transform to include otherParty based on caller role
// Conversations only exist between connected users, so isConnected=true
if (callerRole === UserRole.AGENT) {
// Agent is viewing — other party is the user
const showStatus = this.shouldShowOnlineStatus(conversation.user.privacyPreferences);
return {
...conversation,
unreadCount: conversation.agentUnreadCount,
@@ -139,12 +154,13 @@ export class MessagesService {
id: conversation.user.id,
name: `${conversation.user.userProfile?.firstName || ''} ${conversation.user.userProfile?.lastName || ''}`.trim() || 'User',
avatar: conversation.user.userProfile?.avatar,
isOnline: conversation.user.isOnline,
lastSeenAt: conversation.user.lastSeenAt,
isOnline: showStatus ? conversation.user.isOnline : false,
lastSeenAt: showStatus ? conversation.user.lastSeenAt : null,
},
};
} else {
// User is viewing — other party is the agent
const showStatus = this.shouldShowOnlineStatus(conversation.agentProfile.user?.privacyPreferences);
return {
...conversation,
unreadCount: conversation.userUnreadCount,
@@ -154,8 +170,8 @@ export class MessagesService {
name: `${conversation.agentProfile.firstName || ''} ${conversation.agentProfile.lastName || ''}`.trim() || 'Agent',
avatar: conversation.agentProfile.avatar,
headline: conversation.agentProfile.headline,
isOnline: conversation.agentProfile.user?.isOnline || false,
lastSeenAt: conversation.agentProfile.user?.lastSeenAt || null,
isOnline: showStatus ? (conversation.agentProfile.user?.isOnline || false) : false,
lastSeenAt: showStatus ? (conversation.agentProfile.user?.lastSeenAt || null) : null,
},
};
}
@@ -306,6 +322,7 @@ export class MessagesService {
id: true,
isOnline: true,
lastSeenAt: true,
privacyPreferences: true,
userProfile: {
select: {
firstName: true,
@@ -324,17 +341,20 @@ export class MessagesService {
});
// Map to include unread count from agent perspective
return conversations.map((conv) => ({
...conv,
unreadCount: conv.agentUnreadCount,
otherParty: {
id: conv.user.id,
name: `${conv.user.userProfile?.firstName || ''} ${conv.user.userProfile?.lastName || ''}`.trim() || 'User',
avatar: conv.user.userProfile?.avatar,
isOnline: conv.user.isOnline,
lastSeenAt: conv.user.lastSeenAt,
},
}));
return conversations.map((conv) => {
const showStatus = this.shouldShowOnlineStatus(conv.user.privacyPreferences);
return {
...conv,
unreadCount: conv.agentUnreadCount,
otherParty: {
id: conv.user.id,
name: `${conv.user.userProfile?.firstName || ''} ${conv.user.userProfile?.lastName || ''}`.trim() || 'User',
avatar: conv.user.userProfile?.avatar,
isOnline: showStatus ? conv.user.isOnline : false,
lastSeenAt: showStatus ? conv.user.lastSeenAt : null,
},
};
});
} else {
// Regular user
conversations = await this.prisma.conversation.findMany({
@@ -352,6 +372,7 @@ export class MessagesService {
select: {
isOnline: true,
lastSeenAt: true,
privacyPreferences: true,
},
},
},
@@ -365,19 +386,22 @@ export class MessagesService {
});
// Map to include unread count from user perspective
return conversations.map((conv) => ({
...conv,
unreadCount: conv.userUnreadCount,
otherParty: {
id: conv.agentProfile.id,
userId: conv.agentProfile.userId,
name: `${conv.agentProfile.firstName || ''} ${conv.agentProfile.lastName || ''}`.trim() || 'Agent',
avatar: conv.agentProfile.avatar,
headline: conv.agentProfile.headline,
isOnline: conv.agentProfile.user.isOnline,
lastSeenAt: conv.agentProfile.user.lastSeenAt,
},
}));
return conversations.map((conv) => {
const showStatus = this.shouldShowOnlineStatus(conv.agentProfile.user.privacyPreferences);
return {
...conv,
unreadCount: conv.userUnreadCount,
otherParty: {
id: conv.agentProfile.id,
userId: conv.agentProfile.userId,
name: `${conv.agentProfile.firstName || ''} ${conv.agentProfile.lastName || ''}`.trim() || 'Agent',
avatar: conv.agentProfile.avatar,
headline: conv.agentProfile.headline,
isOnline: showStatus ? conv.agentProfile.user.isOnline : false,
lastSeenAt: showStatus ? conv.agentProfile.user.lastSeenAt : null,
},
};
});
}
}