feat: implement privacy-aware profile visibility and activity status filtering using optional authentication guard
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user