diff --git a/backend/src/users/community.controller.ts b/backend/src/users/community.controller.ts new file mode 100644 index 00000000..6db92eb0 --- /dev/null +++ b/backend/src/users/community.controller.ts @@ -0,0 +1,40 @@ +import { + Controller, + Get, + Param, + Query, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; +import { UsersService } from './providers/users.service'; + +@ApiTags('community') +@ApiBearerAuth() +@Controller('community') +export class CommunityController { + constructor(private readonly usersService: UsersService) {} + + @Get('members') + @ApiOperation({ summary: 'List all active community members (paginated)' }) + @ApiQuery({ name: 'page', required: false, type: Number, example: 1 }) + @ApiQuery({ name: 'limit', required: false, type: Number, example: 20 }) + @ApiQuery({ name: 'search', required: false, type: String }) + async getMembers( + @Query('page') page?: number, + @Query('limit') limit?: number, + @Query('search') search?: string, + ) { + const result = await this.usersService.getCommunityMembers({ + page, + limit, + search, + }); + return { message: 'Community members retrieved successfully', ...result }; + } + + @Get('members/:username') + @ApiOperation({ summary: 'Get a member public profile by username' }) + async getPublicProfile(@Param('username') username: string) { + const profile = await this.usersService.getPublicProfile(username); + return { message: 'Profile retrieved successfully', data: profile }; + } +} diff --git a/backend/src/users/providers/get-community-members.provider.ts b/backend/src/users/providers/get-community-members.provider.ts new file mode 100644 index 00000000..f669e5a9 --- /dev/null +++ b/backend/src/users/providers/get-community-members.provider.ts @@ -0,0 +1,74 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from '../entities/user.entity'; +import { ErrorCatch } from '../../utils/error'; +import { MembershipStatus } from '../enums/membership-status.enum'; + +export interface PaginatedCommunityMembers { + data: { + id: string; + username: string; + firstname: string; + lastname: string; + profilePicture: string; + memberSince: Date; + }[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +@Injectable() +export class GetCommunityMembersProvider { + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + ) {} + + async getMembers(query: { + page?: number; + limit?: number; + search?: string; + }): Promise { + try { + const { page = 1, limit = 20, search } = query; + + const qb = this.usersRepository + .createQueryBuilder('user') + .select([ + 'user.id', + 'user.username', + 'user.firstname', + 'user.lastname', + 'user.profilePicture', + 'user.memberSince', + ]) + .where('user.isDeleted = :isDeleted', { isDeleted: false }) + .andWhere('user.isActive = :isActive', { isActive: true }) + .andWhere('user.isVerified = :isVerified', { isVerified: true }) + .andWhere('user.membershipStatus = :membershipStatus', { + membershipStatus: MembershipStatus.ACTIVE, + }); + + if (search) { + qb.andWhere( + '(LOWER(user.username) LIKE :search OR LOWER(user.firstname) LIKE :search OR LOWER(user.lastname) LIKE :search)', + { search: `%${search.toLowerCase()}%` }, + ); + } + + const total = await qb.getCount(); + const data = await qb + .skip((page - 1) * limit) + .take(limit) + .orderBy('user.memberSince', 'DESC') + .getMany(); + + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } catch (error) { + ErrorCatch(error, 'Error fetching community members'); + } + } +} diff --git a/backend/src/users/providers/get-public-profile.provider.ts b/backend/src/users/providers/get-public-profile.provider.ts new file mode 100644 index 00000000..159ebf89 --- /dev/null +++ b/backend/src/users/providers/get-public-profile.provider.ts @@ -0,0 +1,67 @@ +import { + Injectable, + NotFoundException, + ForbiddenException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from '../entities/user.entity'; +import { ErrorCatch } from '../../utils/error'; +import { MembershipStatus } from '../enums/membership-status.enum'; +import { computeProfileCompleteness } from '../utils/profile-completeness.util'; + +@Injectable() +export class GetPublicProfileProvider { + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + ) {} + + async getPublicProfile( + username: string, + ): Promise<{ + username: string; + firstname: string; + lastname: string; + profilePicture: string; + memberSince: Date; + profileCompleteness: number; + }> { + try { + const user = await this.usersRepository.findOne({ + where: { username }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + if ( + user.isSuspended || + !user.isActive || + user.membershipStatus === MembershipStatus.SUSPENDED + ) { + throw new ForbiddenException('User is suspended or inactive'); + } + + const completeness = computeProfileCompleteness(user); + if (user.profileCompleteness !== completeness) { + await this.usersRepository.update(user.id, { + profileCompleteness: completeness, + }); + user.profileCompleteness = completeness; + } + + return { + username: user.username, + firstname: user.firstname, + lastname: user.lastname, + profilePicture: user.profilePicture, + memberSince: user.memberSince, + profileCompleteness: user.profileCompleteness, + }; + } catch (error) { + ErrorCatch(error, 'Error fetching public profile'); + } + } +} diff --git a/backend/src/users/providers/users.service.ts b/backend/src/users/providers/users.service.ts index 5a5c617b..07df6c7c 100644 --- a/backend/src/users/providers/users.service.ts +++ b/backend/src/users/providers/users.service.ts @@ -23,6 +23,8 @@ import { FindAdminByIdProvider } from './findAdminById.provider'; import { GetMembersProvider } from './get-members.provider'; import { UpdateMemberStatusProvider } from './update-member-status.provider'; import { GetMemberStatsProvider } from './get-member-stats.provider'; +import { GetCommunityMembersProvider } from './get-community-members.provider'; +import { GetPublicProfileProvider } from './get-public-profile.provider'; import { MemberQueryDto } from '../dto/member-query.dto'; import { MembershipStatus } from '../enums/membership-status.enum'; import { computeProfileCompleteness } from '../utils/profile-completeness.util'; @@ -60,6 +62,8 @@ export class UsersService { private readonly getMembersProvider: GetMembersProvider, private readonly updateMemberStatusProvider: UpdateMemberStatusProvider, private readonly getMemberStatsProvider: GetMemberStatsProvider, + private readonly getCommunityMembersProvider: GetCommunityMembersProvider, + private readonly getPublicProfileProvider: GetPublicProfileProvider, ) {} // CREATE USER @@ -200,4 +204,16 @@ export class UsersService { } return user; } + + async getCommunityMembers(query: { + page?: number; + limit?: number; + search?: string; + }) { + return this.getCommunityMembersProvider.getMembers(query); + } + + async getPublicProfile(username: string) { + return this.getPublicProfileProvider.getPublicProfile(username); + } } diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index 851e10e5..95f6d2b4 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -20,7 +20,10 @@ import { FindAdminByIdProvider } from './providers/findAdminById.provider'; import { GetMembersProvider } from './providers/get-members.provider'; import { UpdateMemberStatusProvider } from './providers/update-member-status.provider'; import { GetMemberStatsProvider } from './providers/get-member-stats.provider'; +import { GetCommunityMembersProvider } from './providers/get-community-members.provider'; +import { GetPublicProfileProvider } from './providers/get-public-profile.provider'; import { MembersController } from './members.controller'; +import { CommunityController } from './community.controller'; @Module({ imports: [ @@ -28,7 +31,7 @@ import { MembersController } from './members.controller'; forwardRef(() => AuthModule), CloudinaryModule, ], - controllers: [UsersController, MembersController], + controllers: [UsersController, MembersController, CommunityController], providers: [ UsersService, CreateUserProvider, @@ -46,6 +49,8 @@ import { MembersController } from './members.controller'; GetMembersProvider, UpdateMemberStatusProvider, GetMemberStatsProvider, + GetCommunityMembersProvider, + GetPublicProfileProvider, ], exports: [UsersService], })