Add report options and profile sharing support

This commit is contained in:
boutmoun123
2026-06-11 12:04:51 +03:00
parent a3628d19a2
commit 20fe06b5ed
7 changed files with 186 additions and 1 deletions
@@ -13,3 +13,22 @@ describe('UsersController artist dashboard', () => {
expect(result).toEqual({ profile: { _id: 'user-1' } });
});
});
describe('UsersController profile lookup', () => {
it('uses the authenticated user id when loading profile overview by username', async () => {
const usersService = {
getProfileOverviewByUsername: jest.fn().mockResolvedValue({
profileShareUrl: 'https://oudelaa.com/u/artist',
}),
};
const controller = new UsersController(usersService as any);
const result = await controller.getProfileOverviewByUsername(
{ sub: 'viewer-1' } as any,
'artist',
);
expect(usersService.getProfileOverviewByUsername).toHaveBeenCalledWith('artist', 'viewer-1');
expect(result).toEqual({ profileShareUrl: 'https://oudelaa.com/u/artist' });
});
});
+10
View File
@@ -288,6 +288,16 @@ export class UsersController {
return this.usersService.getMyDashboard(user.sub);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('by-username/:username')
async getProfileOverviewByUsername(
@CurrentUser() user: JwtPayload,
@Param('username') username: string,
) {
return this.usersService.getProfileOverviewByUsername(username, user.sub);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':id/profile-overview')
+56 -1
View File
@@ -17,11 +17,13 @@ const createService = (options: {
newFollowersThisWeek?: number;
newFollowersThisMonth?: number;
recentActivity?: Record<string, any>[];
publicAppUrl?: string;
} = {}) => {
const userId = new Types.ObjectId().toString();
const user = options.user ?? {
_id: new Types.ObjectId(userId),
name: 'Artist',
username: 'artist_one',
stageName: 'Stage',
bio: 'Bio',
avatar: '',
@@ -34,6 +36,7 @@ const createService = (options: {
return {
_id: userId,
name: this.name,
username: this.username,
stageName: this.stageName,
bio: this.bio,
avatar: this.avatar,
@@ -83,12 +86,13 @@ const createService = (options: {
};
const usersRepository = {
findById: jest.fn().mockResolvedValue(user),
findOne: jest.fn().mockResolvedValue(user),
};
const service = new UsersService(
connection as any,
usersRepository as any,
{ logSuperAdminAction: jest.fn() } as any,
{ get: jest.fn() } as any,
{ get: jest.fn((key: string) => (key === 'PUBLIC_APP_URL' ? options.publicAppUrl : undefined)) } as any,
{ saveFile: jest.fn(), deleteFile: jest.fn() } as any,
);
@@ -183,3 +187,54 @@ describe('UsersService artist dashboard', () => {
);
});
});
describe('UsersService profile sharing', () => {
it('returns profileShareUrl with username in profile overview', async () => {
const { service, userId } = createService({ publicAppUrl: 'https://oudelaa.com/' });
const result = await service.getProfileOverview(userId, userId);
expect(result.profileShareUrl).toBe('https://oudelaa.com/u/artist_one');
});
it('falls back to profile id when username is missing', async () => {
const userId = new Types.ObjectId().toString();
const { service } = createService({
user: {
_id: new Types.ObjectId(userId),
name: 'Artist',
username: '',
followersCount: 0,
followingCount: 0,
postsCount: 0,
isDisabled: false,
},
publicAppUrl: 'https://oudelaa.com',
});
const result = await service.getProfileOverview(userId, userId);
expect(result.profileShareUrl).toBe(`https://oudelaa.com/profile/${userId}`);
});
it('returns profile overview by username', async () => {
const { service, userId, usersRepository } = createService({
publicAppUrl: 'https://oudelaa.com',
});
const result = await service.getProfileOverviewByUsername('Artist_One', userId);
expect(usersRepository.findOne).toHaveBeenCalledWith({ username: 'artist_one' });
expect(usersRepository.findById).toHaveBeenCalledWith(userId);
expect(result.profileShareUrl).toBe('https://oudelaa.com/u/artist_one');
});
it('throws not found for missing username', async () => {
const { service, usersRepository, userId } = createService();
usersRepository.findOne.mockResolvedValueOnce(null);
await expect(service.getProfileOverviewByUsername('missing', userId)).rejects.toThrow(
'User not found',
);
});
});
+32
View File
@@ -759,6 +759,7 @@ export class UsersService {
return {
user,
profileShareUrl: this.buildProfileShareUrl(user),
stats: {
followersCount: user.followersCount ?? 0,
followingCount: user.followingCount ?? 0,
@@ -799,6 +800,16 @@ export class UsersService {
};
}
async getProfileOverviewByUsername(username: string, viewerUserId: string) {
const normalizedUsername = username.trim().toLowerCase();
const user = normalizedUsername ? await this.findByUsername(normalizedUsername) : null;
if (!user || user.isDisabled) {
throw new NotFoundException('User not found');
}
return this.getProfileOverview(this.extractUserId(user), viewerUserId);
}
async getMyDashboard(currentUserId: string): Promise<ArtistDashboardResponse> {
if (!Types.ObjectId.isValid(currentUserId)) {
throw new BadRequestException('Invalid user id');
@@ -964,6 +975,27 @@ export class UsersService {
});
}
private buildProfileShareUrl(user: UserDocument | Record<string, any>): string {
const appUrl = this.resolvePublicAppUrl();
const username = typeof user.username === 'string' ? user.username.trim() : '';
if (username) {
return `${appUrl}/u/${encodeURIComponent(username)}`;
}
return `${appUrl}/profile/${this.extractUserId(user)}`;
}
private resolvePublicAppUrl(): string {
const configured =
this.configService.get<string>('PUBLIC_APP_URL') ?? process.env.PUBLIC_APP_URL ?? '';
return (configured.trim() || 'https://oudelaa.com').replace(/\/+$/, '');
}
private extractUserId(user: UserDocument | Record<string, any>): string {
const candidate = user.id ?? user._id;
return candidate?.toString?.() ?? String(candidate);
}
private async findTopDashboardContent(
authorId: Types.ObjectId,
limit: number,