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:
2026-02-28 18:18:11 +01:00
parent fb9140a03f
commit b506dba0eb
285 changed files with 1184 additions and 47362 deletions

View File

@@ -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;
}