generated from nhcarrigan/template
34c7ca8ba2
Add comprehensive user profile system allowing users to showcase their activity and customize their profiles. Database Changes: - Added profile fields to User model: slug, displayName, bio, profilePublic - Added index on slug field for efficient lookups API Changes: - Added GET /users/me endpoint to fetch current user - Added PUT /users/me endpoint to update user settings - Added GET /users/profile/:identifier endpoint for public profiles - Updated UserService with profile methods and statistics - Modified AuthService to include profile fields in user responses Frontend Changes: - Created ProfileComponent to display user profiles with stats - Created SettingsComponent for profile customization - Added profile and settings routes - Updated header dropdown menu with profile links - Enhanced UserService with profile methods - Added updateUser method to AuthService Features: - Custom profile slugs for clean URLs - Display names separate from usernames - User bios (up to 500 characters) - Public/private profile toggle - Activity statistics (suggestions, likes, comments, acceptance rate) - Badge display (Staff, Mod, VIP, Discord Member) - Beautiful witch-themed styling Closes #45
258 lines
6.4 KiB
TypeScript
258 lines
6.4 KiB
TypeScript
/**
|
|
* @copyright 2026 NHCarrigan
|
|
* @license Naomi's Public License
|
|
* @author Naomi Carrigan
|
|
*/
|
|
|
|
import { User } from "@library/shared-types";
|
|
import { prisma } from "../lib/prisma";
|
|
import { SuggestionStatus } from "@prisma/client";
|
|
|
|
export class UserService {
|
|
private prisma = prisma;
|
|
|
|
async getAllUsers(): Promise<User[]> {
|
|
const users = await this.prisma.user.findMany({
|
|
orderBy: { username: "asc" },
|
|
});
|
|
|
|
return users.map((user) => ({
|
|
id: user.id,
|
|
discordId: user.discordId,
|
|
username: user.username,
|
|
email: user.email,
|
|
avatar: user.avatar || undefined,
|
|
slug: user.slug || undefined,
|
|
displayName: user.displayName || undefined,
|
|
bio: user.bio || undefined,
|
|
profilePublic: user.profilePublic,
|
|
isAdmin: user.isAdmin,
|
|
isBanned: user.isBanned,
|
|
inDiscord: user.inDiscord,
|
|
isVip: user.isVip,
|
|
isMod: user.isMod,
|
|
isStaff: user.isStaff,
|
|
}));
|
|
}
|
|
|
|
async getUserById(id: string): Promise<User | null> {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id },
|
|
});
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
discordId: user.discordId,
|
|
username: user.username,
|
|
email: user.email,
|
|
avatar: user.avatar || undefined,
|
|
slug: user.slug || undefined,
|
|
displayName: user.displayName || undefined,
|
|
bio: user.bio || undefined,
|
|
profilePublic: user.profilePublic,
|
|
isAdmin: user.isAdmin,
|
|
isBanned: user.isBanned,
|
|
inDiscord: user.inDiscord,
|
|
isVip: user.isVip,
|
|
isMod: user.isMod,
|
|
isStaff: user.isStaff,
|
|
};
|
|
}
|
|
|
|
async banUser(id: string): Promise<User | null> {
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data: { isBanned: true },
|
|
});
|
|
|
|
return {
|
|
id: user.id,
|
|
discordId: user.discordId,
|
|
username: user.username,
|
|
email: user.email,
|
|
avatar: user.avatar || undefined,
|
|
slug: user.slug || undefined,
|
|
displayName: user.displayName || undefined,
|
|
bio: user.bio || undefined,
|
|
profilePublic: user.profilePublic,
|
|
isAdmin: user.isAdmin,
|
|
isBanned: user.isBanned,
|
|
inDiscord: user.inDiscord,
|
|
isVip: user.isVip,
|
|
isMod: user.isMod,
|
|
isStaff: user.isStaff,
|
|
};
|
|
}
|
|
|
|
async unbanUser(id: string): Promise<User | null> {
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data: { isBanned: false },
|
|
});
|
|
|
|
return {
|
|
id: user.id,
|
|
discordId: user.discordId,
|
|
username: user.username,
|
|
email: user.email,
|
|
avatar: user.avatar || undefined,
|
|
slug: user.slug || undefined,
|
|
displayName: user.displayName || undefined,
|
|
bio: user.bio || undefined,
|
|
profilePublic: user.profilePublic,
|
|
isAdmin: user.isAdmin,
|
|
isBanned: user.isBanned,
|
|
inDiscord: user.inDiscord,
|
|
isVip: user.isVip,
|
|
isMod: user.isMod,
|
|
isStaff: user.isStaff,
|
|
};
|
|
}
|
|
|
|
async isUserBanned(id: string): Promise<boolean> {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id },
|
|
select: { isBanned: true },
|
|
});
|
|
|
|
return user?.isBanned ?? false;
|
|
}
|
|
|
|
async getUserBySlug(slug: string): Promise<User | null> {
|
|
const user = await this.prisma.user.findFirst({
|
|
where: { slug },
|
|
});
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
discordId: user.discordId,
|
|
username: user.username,
|
|
email: user.email,
|
|
avatar: user.avatar || undefined,
|
|
slug: user.slug || undefined,
|
|
displayName: user.displayName || undefined,
|
|
bio: user.bio || undefined,
|
|
profilePublic: user.profilePublic,
|
|
isAdmin: user.isAdmin,
|
|
isBanned: user.isBanned,
|
|
inDiscord: user.inDiscord,
|
|
isVip: user.isVip,
|
|
isMod: user.isMod,
|
|
isStaff: user.isStaff,
|
|
};
|
|
}
|
|
|
|
async updateUserSettings(
|
|
id: string,
|
|
updates: {
|
|
slug?: string;
|
|
displayName?: string;
|
|
bio?: string;
|
|
profilePublic?: boolean;
|
|
}
|
|
): Promise<User | null> {
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data: updates,
|
|
});
|
|
|
|
return {
|
|
id: user.id,
|
|
discordId: user.discordId,
|
|
username: user.username,
|
|
email: user.email,
|
|
avatar: user.avatar || undefined,
|
|
slug: user.slug || undefined,
|
|
displayName: user.displayName || undefined,
|
|
bio: user.bio || undefined,
|
|
profilePublic: user.profilePublic,
|
|
isAdmin: user.isAdmin,
|
|
isBanned: user.isBanned,
|
|
inDiscord: user.inDiscord,
|
|
isVip: user.isVip,
|
|
isMod: user.isMod,
|
|
isStaff: user.isStaff,
|
|
};
|
|
}
|
|
|
|
async getUserProfile(identifier: string): Promise<{
|
|
id: string;
|
|
username: string;
|
|
displayName?: string | null;
|
|
avatar?: string | null;
|
|
bio?: string | null;
|
|
slug?: string | null;
|
|
isStaff: boolean;
|
|
isMod: boolean;
|
|
isVip: boolean;
|
|
inDiscord: boolean;
|
|
profilePublic: boolean;
|
|
createdAt: Date;
|
|
stats: {
|
|
suggestionsCount: number;
|
|
suggestionsAcceptedCount: number;
|
|
likesCount: number;
|
|
commentsCount: number;
|
|
};
|
|
} | null> {
|
|
// Try to find by slug first, then by id if it's a valid ObjectId
|
|
const isValidObjectId = /^[0-9a-f]{24}$/i.test(identifier);
|
|
|
|
const whereConditions = isValidObjectId
|
|
? [{ slug: identifier }, { id: identifier }]
|
|
: [{ slug: identifier }];
|
|
|
|
const user = await this.prisma.user.findFirst({
|
|
where: {
|
|
OR: whereConditions,
|
|
},
|
|
include: {
|
|
suggestions: {
|
|
select: { id: true, status: true },
|
|
},
|
|
likes: {
|
|
select: { id: true },
|
|
},
|
|
comments: {
|
|
select: { id: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
displayName: user.displayName,
|
|
avatar: user.avatar,
|
|
bio: user.bio,
|
|
slug: user.slug,
|
|
isStaff: user.isStaff,
|
|
isMod: user.isMod,
|
|
isVip: user.isVip,
|
|
inDiscord: user.inDiscord,
|
|
profilePublic: user.profilePublic,
|
|
createdAt: user.createdAt,
|
|
stats: {
|
|
suggestionsCount: user.suggestions.length,
|
|
suggestionsAcceptedCount: user.suggestions.filter(
|
|
(suggestion) => suggestion.status === SuggestionStatus.ACCEPTED
|
|
).length,
|
|
likesCount: user.likes.length,
|
|
commentsCount: user.comments.length,
|
|
},
|
|
};
|
|
}
|
|
}
|