diff --git a/prisma/schema.prisma b/prisma/schema.prisma index feb21c7..c6be34e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,6 +25,23 @@ model User { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + // Advanced profile fields + bio String? + location String? + avatarUrl String? @map("avatar_url") + + // Preferences and privacy + preferences Json? + privacySettings Json? @map("privacy_settings") + exportRequestedAt DateTime? @map("export_requested_at") + + // Relationships + followers UserRelationship[] @relation("Followers") + following UserRelationship[] @relation("Following") + + // Activity + activities UserActivity[] + properties Property[] receivedTransactions Transaction[] @relation("UserTransactions") userRole Role? @relation(fields: [roleId], references: [id], onDelete: SetNull) @@ -36,9 +53,41 @@ model User { @@index([walletAddress]) // For searching users by wallet address @@index([role]) // For filtering users by role @@index([createdAt]) // For sorting/filtering by creation date - // Consider adding an index on isVerified if you often filter by verification status @@index([isVerified]) + @@index([location]) @@map("users") +// User activity tracking +model UserActivity { + id String @id @default(cuid()) + userId String @map("user_id") + action String + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([action]) + @@index([createdAt]) + @@map("user_activities") +} + +// User relationship (followers, connections) +model UserRelationship { + id String @id @default(cuid()) + followerId String @map("follower_id") + followingId String @map("following_id") + status String @default("active") + createdAt DateTime @default(now()) @map("created_at") + + follower User @relation("Followers", fields: [followerId], references: [id], onDelete: Cascade) + following User @relation("Following", fields: [followingId], references: [id], onDelete: Cascade) + + @@unique([followerId, followingId]) + @@index([followerId]) + @@index([followingId]) + @@map("user_relationships") +} } model Property { diff --git a/src/models/user.entity.ts b/src/models/user.entity.ts index c9243b3..cb0f800 100644 --- a/src/models/user.entity.ts +++ b/src/models/user.entity.ts @@ -19,10 +19,46 @@ export class User implements PrismaUser { createdAt: Date; updatedAt: Date; + + // Advanced profile fields + bio?: string | null; + location?: string | null; + avatarUrl?: string | null; + + // Preferences and privacy + preferences?: any | null; + privacySettings?: any | null; + exportRequestedAt?: Date | null; + + // Relationships + followers?: UserRelationship[]; + following?: UserRelationship[]; + + // Activity + activities?: UserActivity[]; } /** * Input used when creating a user +// User activity entity +export class UserActivity { + id: string; + userId: string; + action: string; + metadata?: any; + createdAt: Date; +} + +// User relationship entity +export class UserRelationship { + id: string; + followerId: string; + followingId: string; + status: string; + createdAt: Date; + follower?: User; + following?: User; +} * Flexible enough for email/password and Web3 users */ export type CreateUserInput = { diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index 0c4a615..cc8c00d 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -76,4 +76,48 @@ export class CreateUserDto { @IsXssSafe({ message: 'Wallet address contains potentially malicious content' }) @IsNotSqlInjection({ message: 'Wallet address contains potential SQL injection' }) walletAddress?: string; + + @ApiPropertyOptional({ + description: 'User biography', + example: 'Blockchain enthusiast and property investor.', + maxLength: 500, + }) + @IsOptional() + @IsString() + @MaxLength(500) + bio?: string; + + @ApiPropertyOptional({ + description: 'User location', + example: 'London, UK', + maxLength: 100, + }) + @IsOptional() + @IsString() + @MaxLength(100) + location?: string; + + @ApiPropertyOptional({ + description: 'Avatar image URL', + example: 'https://example.com/avatar.jpg', + }) + @IsOptional() + @IsString() + avatarUrl?: string; + + @ApiPropertyOptional({ + description: 'User preferences (JSON object)', + example: '{ "theme": "dark", "notifications": true }', + type: Object, + }) + @IsOptional() + preferences?: any; + + @ApiPropertyOptional({ + description: 'User privacy settings (JSON object)', + example: '{ "profileVisible": true }', + type: Object, + }) + @IsOptional() + privacySettings?: any; } diff --git a/src/users/dto/user-response.dto.ts b/src/users/dto/user-response.dto.ts index 79dc4f1..6e75300 100644 --- a/src/users/dto/user-response.dto.ts +++ b/src/users/dto/user-response.dto.ts @@ -31,6 +31,62 @@ export class UserResponseDto { }) walletAddress?: string; + @ApiPropertyOptional({ + description: 'User biography', + example: 'Blockchain enthusiast and property investor.', + }) + bio?: string; + + @ApiPropertyOptional({ + description: 'User location', + example: 'London, UK', + }) + location?: string; + + @ApiPropertyOptional({ + description: 'Avatar image URL', + example: 'https://example.com/avatar.jpg', + }) + avatarUrl?: string; + + @ApiPropertyOptional({ + description: 'User preferences (JSON object)', + example: '{ "theme": "dark", "notifications": true }', + type: Object, + }) + preferences?: any; + + @ApiPropertyOptional({ + description: 'User privacy settings (JSON object)', + example: '{ "profileVisible": true }', + type: Object, + }) + privacySettings?: any; + + @ApiPropertyOptional({ + description: 'Followers count', + example: 10, + }) + followersCount?: number; + + @ApiPropertyOptional({ + description: 'Following count', + example: 5, + }) + followingCount?: number; + + @ApiPropertyOptional({ + description: 'User activity count', + example: 100, + }) + activityCount?: number; + + @ApiPropertyOptional({ + description: 'User login count', + example: 20, + }) + loginCount?: number; + @ApiProperty({ description: 'Whether the user email is verified', example: true, diff --git a/src/users/user.controller.ts b/src/users/user.controller.ts index b8dd75d..d8e1e99 100644 --- a/src/users/user.controller.ts +++ b/src/users/user.controller.ts @@ -23,4 +23,84 @@ export class UserController { findOne(@Param('id') id: string) { return this.userService.findById(id); } + + // --- Advanced Features --- + + @Patch(':id/profile') + @ApiOperation({ summary: 'Update user profile (bio, location, avatar)' }) + updateProfile(@Param('id') id: string, @Body() profile: { bio?: string; location?: string; avatarUrl?: string }) { + return this.userService.updateProfile(id, profile); + } + + @Patch(':id/preferences') + @ApiOperation({ summary: 'Update user preferences' }) + updatePreferences(@Param('id') id: string, @Body() preferences: any) { + return this.userService.updatePreferences(id, preferences); + } + + @Post(':id/activity') + @ApiOperation({ summary: 'Log user activity' }) + logActivity(@Param('id') id: string, @Body() body: { action: string; metadata?: any }) { + return this.userService.logActivity(id, body.action, body.metadata); + } + + @Get(':id/activity') + @ApiOperation({ summary: 'Get user activity history' }) + getActivityHistory(@Param('id') id: string) { + return this.userService.getActivityHistory(id); + } + + @Patch(':id/avatar') + @ApiOperation({ summary: 'Update user avatar' }) + updateAvatar(@Param('id') id: string, @Body() body: { avatarUrl: string }) { + return this.userService.updateAvatar(id, body.avatarUrl); + } + + @Get('search/:query') + @ApiOperation({ summary: 'Search users by name, email, or location' }) + searchUsers(@Param('query') query: string) { + return this.userService.searchUsers(query); + } + + @Post(':id/follow/:targetId') + @ApiOperation({ summary: 'Follow another user' }) + followUser(@Param('id') id: string, @Param('targetId') targetId: string) { + return this.userService.followUser(id, targetId); + } + + @Delete(':id/follow/:targetId') + @ApiOperation({ summary: 'Unfollow a user' }) + unfollowUser(@Param('id') id: string, @Param('targetId') targetId: string) { + return this.userService.unfollowUser(id, targetId); + } + + @Get(':id/followers') + @ApiOperation({ summary: 'List followers of a user' }) + getFollowers(@Param('id') id: string) { + return this.userService.getFollowers(id); + } + + @Get(':id/following') + @ApiOperation({ summary: 'List users a user is following' }) + getFollowing(@Param('id') id: string) { + return this.userService.getFollowing(id); + } + + @Get(':id/analytics') + @ApiOperation({ summary: 'Get user analytics and engagement metrics' }) + getUserAnalytics(@Param('id') id: string) { + return this.userService.getUserAnalytics(id); + } + + @Patch(':id/privacy') + @ApiOperation({ summary: 'Update user privacy settings' }) + updatePrivacySettings(@Param('id') id: string, @Body() privacySettings: any) { + return this.userService.updatePrivacySettings(id, privacySettings); + } + + @Post(':id/export') + @ApiOperation({ summary: 'Request user data export' }) + requestDataExport(@Param('id') id: string) { + return this.userService.requestDataExport(id); + } } diff --git a/src/users/user.service.ts b/src/users/user.service.ts index a553ab3..0de2726 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -283,4 +283,149 @@ export class UserService { data, }); } + /** + * Update user profile (bio, location, avatar) + */ + async updateProfile(userId: string, profile: { bio?: string; location?: string; avatarUrl?: string }) { + return this.prisma.user.update({ + where: { id: userId }, + data: profile, + }); + } + + /** + * Update user preferences (JSON) + */ + async updatePreferences(userId: string, preferences: any) { + return this.prisma.user.update({ + where: { id: userId }, + data: { preferences }, + }); + } + + /** + * Track user activity + */ + async logActivity(userId: string, action: string, metadata?: any) { + return this.prisma.userActivity.create({ + data: { userId, action, metadata }, + }); + } + + /** + * Get user activity history + */ + async getActivityHistory(userId: string, limit = 50) { + return this.prisma.userActivity.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + take: limit, + }); + } + + /** + * Update user avatar + */ + async updateAvatar(userId: string, avatarUrl: string) { + return this.prisma.user.update({ + where: { id: userId }, + data: { avatarUrl }, + }); + } + + /** + * Search users by name, email, or location + */ + async searchUsers(query: string, limit = 20) { + return this.prisma.user.findMany({ + where: { + OR: [ + { email: { contains: query, mode: 'insensitive' } }, + { firstName: { contains: query, mode: 'insensitive' } }, + { lastName: { contains: query, mode: 'insensitive' } }, + { location: { contains: query, mode: 'insensitive' } }, + ], + }, + take: limit, + }); + } + + /** + * Follow another user + */ + async followUser(followerId: string, followingId: string) { + if (followerId === followingId) throw new BadRequestException('Cannot follow yourself'); + // Prevent duplicate follows + const existing = await this.prisma.userRelationship.findUnique({ + where: { followerId_followingId: { followerId, followingId } }, + }); + if (existing) return existing; + return this.prisma.userRelationship.create({ + data: { followerId, followingId }, + }); + } + + /** + * Unfollow a user + */ + async unfollowUser(followerId: string, followingId: string) { + return this.prisma.userRelationship.delete({ + where: { followerId_followingId: { followerId, followingId } }, + }); + } + + /** + * List followers of a user + */ + async getFollowers(userId: string, limit = 50) { + return this.prisma.userRelationship.findMany({ + where: { followingId: userId }, + take: limit, + include: { follower: true }, + }); + } + + /** + * List users a user is following + */ + async getFollowing(userId: string, limit = 50) { + return this.prisma.userRelationship.findMany({ + where: { followerId: userId }, + take: limit, + include: { following: true }, + }); + } + + /** + * Get user analytics (basic engagement metrics) + */ + async getUserAnalytics(userId: string) { + const [loginCount, activityCount, followers, following] = await Promise.all([ + this.prisma.userActivity.count({ where: { userId, action: 'login' } }), + this.prisma.userActivity.count({ where: { userId } }), + this.prisma.userRelationship.count({ where: { followingId: userId } }), + this.prisma.userRelationship.count({ where: { followerId: userId } }), + ]); + return { loginCount, activityCount, followers, following }; + } + + /** + * Update privacy settings + */ + async updatePrivacySettings(userId: string, privacySettings: any) { + return this.prisma.user.update({ + where: { id: userId }, + data: { privacySettings }, + }); + } + + /** + * Request user data export + */ + async requestDataExport(userId: string) { + return this.prisma.user.update({ + where: { id: userId }, + data: { exportRequestedAt: new Date() }, + }); + } }