feat: implement leaderboard page with top players, teams, and high scores
- Added a new leaderboard route with server-side loading for top players, teams, and recent high scores. - Created a new Svelte component for the leaderboard UI, including tabs for players, teams, and scores. - Implemented ELO ranking display and win/loss statistics for players. - Added functionality to display team wins and member counts. - Included recent high scores with player context and map details. - Enhanced team management features in the teams route, including renaming and member management with permission checks.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
// src/lib/server/permissions.ts
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user } from '$lib/server/db/auth.schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export type UserRole = 'player' | 'referee' | 'admin';
|
||||
|
||||
@@ -22,8 +22,16 @@ export function hasRole(userRole: string | undefined, minRole: UserRole): boolea
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw 403 if user doesn't have the required role.
|
||||
* Use in +page.server.ts load/actions.
|
||||
* Check if a user is the root admin (set via ROOT_ADMIN_EMAIL env var).
|
||||
* The root admin can never be demoted and is the only one who can promote others to admin.
|
||||
*/
|
||||
export function isRootAdmin(email: string | undefined | null): boolean {
|
||||
if (!email || !env.ROOT_ADMIN_EMAIL) return false;
|
||||
return email.toLowerCase() === env.ROOT_ADMIN_EMAIL.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw 401/403 if user doesn't have the required role.
|
||||
*/
|
||||
export function requireRole(
|
||||
locals: App.Locals,
|
||||
@@ -39,14 +47,72 @@ export function requireRole(
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote/demote a user's global role. Only admins can do this.
|
||||
* Throw 401 if not logged in. Returns the user for convenience.
|
||||
*/
|
||||
export async function setUserRole(userId: string, role: UserRole) {
|
||||
await db.update(user).set({ role }).where(eq(user.id, userId));
|
||||
export function requireAuth(locals: App.Locals) {
|
||||
if (!locals.user) {
|
||||
error(401, 'Not logged in');
|
||||
}
|
||||
return locals.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's role from the database (for fresh reads).
|
||||
* Check that the current user owns the resource OR is an admin.
|
||||
*/
|
||||
export function requireOwnerOrAdmin(
|
||||
locals: App.Locals,
|
||||
ownerId: string | null | undefined,
|
||||
message = 'Not authorized'
|
||||
) {
|
||||
const u = requireAuth(locals);
|
||||
if (u.id !== ownerId && !hasRole(u.role, 'admin')) {
|
||||
error(403, message);
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote/demote a user's global role with full safety checks.
|
||||
* - Only admins can change roles
|
||||
* - Only root admin can promote TO admin or demote FROM admin
|
||||
* - Root admin cannot be demoted by anyone
|
||||
*/
|
||||
export async function setUserRole(
|
||||
actorLocals: App.Locals,
|
||||
targetUserId: string,
|
||||
newRole: UserRole
|
||||
) {
|
||||
const actor = requireAuth(actorLocals);
|
||||
|
||||
if (!hasRole(actor.role, 'admin')) {
|
||||
error(403, 'Only admins can change roles');
|
||||
}
|
||||
|
||||
const target = await db.query.user.findFirst({
|
||||
where: eq(user.id, targetUserId)
|
||||
});
|
||||
if (!target) error(404, 'User not found');
|
||||
|
||||
// Protect root admin from demotion
|
||||
if (isRootAdmin(target.email)) {
|
||||
error(403, "Cannot change the root admin's role");
|
||||
}
|
||||
|
||||
// Only root admin can promote TO admin
|
||||
if (newRole === 'admin' && !isRootAdmin(actor.email)) {
|
||||
error(403, 'Only the root admin can promote users to admin');
|
||||
}
|
||||
|
||||
// Only root admin can demote FROM admin
|
||||
if (target.role === 'admin' && newRole !== 'admin' && !isRootAdmin(actor.email)) {
|
||||
error(403, 'Only the root admin can demote admins');
|
||||
}
|
||||
|
||||
await db.update(user).set({ role: newRole }).where(eq(user.id, targetUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's role from the database.
|
||||
*/
|
||||
export async function getUserRole(userId: string): Promise<UserRole> {
|
||||
const u = await db.query.user.findFirst({
|
||||
@@ -54,3 +120,18 @@ export async function getUserRole(userId: string): Promise<UserRole> {
|
||||
});
|
||||
return (u?.role as UserRole) ?? 'player';
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-promote root admin on login if they're still 'player'.
|
||||
*/
|
||||
export async function ensureRootAdminRole(userRecord: {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}) {
|
||||
if (isRootAdmin(userRecord.email) && userRecord.role !== 'admin') {
|
||||
await db.update(user).set({ role: 'admin' }).where(eq(user.id, userRecord.id));
|
||||
return 'admin';
|
||||
}
|
||||
return userRecord.role;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user