Add report options and profile sharing support
هذا الالتزام موجود في:
@@ -4,6 +4,7 @@ NODE_ENV=development
|
||||
PORT=4000
|
||||
HOST=0.0.0.0
|
||||
PUBLIC_BASE_URL=http://localhost:4000
|
||||
PUBLIC_APP_URL=https://oudelaa.com
|
||||
RESPONSE_ENVELOPE_ENABLED=false
|
||||
GLOBAL_PREFIX=api/v1
|
||||
# Add every frontend origin used by web/mobile debug tools.
|
||||
|
||||
@@ -1584,6 +1584,7 @@
|
||||
"pm.test('Status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"const json = pm.response.json();",
|
||||
"pm.expect(json).to.have.property('stats');",
|
||||
"pm.expect(json).to.have.property('profileShareUrl');",
|
||||
"pm.expect(json).to.have.property('contentCounts');",
|
||||
"pm.expect(json).to.have.property('viewerState');"
|
||||
]
|
||||
@@ -1591,6 +1592,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get Profile Overview By Username",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{accessToken}}"
|
||||
}
|
||||
],
|
||||
"url": "{{baseUrl}}/users/by-username/{{username}}"
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"const json = pm.response.json();",
|
||||
"pm.expect(json).to.have.property('stats');",
|
||||
"pm.expect(json).to.have.property('profileShareUrl');",
|
||||
"pm.expect(json).to.have.property('viewerState');"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get My Artist Dashboard",
|
||||
"request": {
|
||||
@@ -10251,6 +10280,11 @@
|
||||
"value": "true",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "artist_one",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "reactionType",
|
||||
"value": "love",
|
||||
|
||||
@@ -1106,6 +1106,7 @@
|
||||
"pm.test('Status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"const json = pm.response.json();",
|
||||
"pm.expect(json).to.have.property('stats');",
|
||||
"pm.expect(json).to.have.property('profileShareUrl');",
|
||||
"pm.expect(json).to.have.property('contentCounts');",
|
||||
"pm.expect(json).to.have.property('viewerState');"
|
||||
]
|
||||
@@ -1113,6 +1114,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get Profile Overview By Username",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{accessToken}}"
|
||||
}
|
||||
],
|
||||
"url": "{{baseUrl}}/users/by-username/{{username}}"
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"const json = pm.response.json();",
|
||||
"pm.expect(json).to.have.property('stats');",
|
||||
"pm.expect(json).to.have.property('profileShareUrl');",
|
||||
"pm.expect(json).to.have.property('viewerState');"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get My Artist Dashboard",
|
||||
"request": {
|
||||
@@ -7578,6 +7607,11 @@
|
||||
"value": "true",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "artist_one",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "reactionType",
|
||||
"value": "love",
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم