From ac22f5206de681bff2b9f427d9de6966a534bd70 Mon Sep 17 00:00:00 2001 From: tbui-quo Date: Thu, 7 May 2026 17:53:50 +0200 Subject: [PATCH 01/26] [New][#142] - added UserSearchForm component with search debouncing - added UserTable component to display UserAuthView data - modified auth.types.ts with Admin Dtos copied from the bff to not depend on them --- .../src/components/admin/UserSearchForm.tsx | 38 ++++++++++++ .../src/components/admin/UserTable.tsx | 58 +++++++++++++++++++ apps/frontend/src/core/api/auth/auth.types.ts | 39 ++++++++++++- 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/src/components/admin/UserSearchForm.tsx create mode 100644 apps/frontend/src/components/admin/UserTable.tsx diff --git a/apps/frontend/src/components/admin/UserSearchForm.tsx b/apps/frontend/src/components/admin/UserSearchForm.tsx new file mode 100644 index 00000000..fe195df3 --- /dev/null +++ b/apps/frontend/src/components/admin/UserSearchForm.tsx @@ -0,0 +1,38 @@ +import React, { useState, useEffect } from 'react'; + +interface UserSearchFormProps { + onSearch: (query: string) => void; +} + +export const UserSearchForm: React.FC = ({ onSearch }) => { + const [searchTerm, setSearchTerm] = useState(''); + + // Wait 500ms after the user stops typing before searching + useEffect(() => { + const delayDebounceFn = setTimeout(() => { + onSearch(searchTerm); + }, 500); + + return () => clearTimeout(delayDebounceFn); + }, [searchTerm, onSearch]); + + return ( +
+ +
+ setSearchTerm(e.target.value)} + /> +
+

Searching by username, email, or ID...

+
+ ); +}; \ No newline at end of file diff --git a/apps/frontend/src/components/admin/UserTable.tsx b/apps/frontend/src/components/admin/UserTable.tsx new file mode 100644 index 00000000..4197235a --- /dev/null +++ b/apps/frontend/src/components/admin/UserTable.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import {UserAuthView} from "@/src/core/api/auth/auth.types"; + +interface UserTableProps { + users: UserAuthView[]; + isLoading: boolean; + onEditStats: (userId: string) => void; + onToggleStatus: (user: UserAuthView) => void; +} + +export const UserTable: React.FC = ({ users, isLoading, onEditStats, onToggleStatus }) => { + if (isLoading) return
Loading users...
; + + return ( + + + + + + + + + + + {users.map((user) => ( + + + + + + + ))} + +
UserStatusRolesActions
+
{user.username || 'No Username'}
+
{user.email}
+
+ + {user.status} + + + {user.roles?.join(', ')} + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/frontend/src/core/api/auth/auth.types.ts b/apps/frontend/src/core/api/auth/auth.types.ts index 7188f7e7..34c858a9 100644 --- a/apps/frontend/src/core/api/auth/auth.types.ts +++ b/apps/frontend/src/core/api/auth/auth.types.ts @@ -121,6 +121,43 @@ export interface InternalRequestInit extends RequestInit { _retry?: boolean; } +/** + * --- Admin Management DTOs --- + * These types correspond to the /admin endpoints in the BFF + */ + +export interface PageInfoDto { + nextCursor?: string | null; + hasNextPage: boolean; +} + +export interface UserSearchQueryDto { + query?: string; + cursor?: string; + limit?: number; +} + +export interface UserSearchResponseDto { + items: UserAuthView[]; + pageInfo: PageInfoDto; +} + +export interface DisableUserRequestDto { + reason: string; + revokeSessions?: boolean; +} + +export interface EnableUserRequestDto { + reason?: string; +} + +export interface UpdatePlayerStatsDto { + wins?: number; + losses?: number; + xp?: number; + level?: number; +} + /** * --- Errors --- */ export interface ApiError { @@ -131,4 +168,4 @@ export interface ApiError { export interface SetPasswordResponse { success: boolean; -} \ No newline at end of file +} From 4b02cafd2aee394a046dde5ebe75fad80fa9d932 Mon Sep 17 00:00:00 2001 From: tbui-quo Date: Mon, 11 May 2026 18:12:16 +0200 Subject: [PATCH 02/26] - added new interfaces for admin request and responses in auth.types.ts - implement admin methods in auth.client.ts --- .../frontend/src/core/api/auth/auth.client.ts | 54 +++++++++++++++- apps/frontend/src/core/api/auth/auth.types.ts | 64 +++++++++++++++++-- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/apps/frontend/src/core/api/auth/auth.client.ts b/apps/frontend/src/core/api/auth/auth.client.ts index 0b0b1e11..d363df21 100644 --- a/apps/frontend/src/core/api/auth/auth.client.ts +++ b/apps/frontend/src/core/api/auth/auth.client.ts @@ -14,7 +14,17 @@ import type SetPasswordResponse, PendingRequest, InternalRequestInit, - ApiError + ApiError, + UserSearchRequest, + UserSearchResponse, + UpdatePlayerStatsRequest, + UserDisabledRequest, + UserDisabledResponse, + UserEnabledRequest, + UserEnabledResponse, + SetUserRolesRequest, + UserRolesResponse, + UpdatePlayerStatsResponse, } from "@/src/core/api/auth/auth.types"; @@ -253,4 +263,46 @@ export const authClient = { body: JSON.stringify(data), }); }, + + // --- Admin Management --- + + /** + * Search and list users with pagination + */ + async searchUsers(params: UserSearchRequest): Promise> { + const queryParams = new URLSearchParams(); + if (params.query) queryParams.append('query', params.query); + if (params.cursor) queryParams.append('cursor', params.cursor); + if (params.limit) queryParams.append('limit', params.limit.toString()); + + return apiFetch(`${BASE_URL}/admin/users?${queryParams.toString()}`); + }, + + async disableUser(userId: string, data: UserDisabledRequest): Promise> { + return apiFetch(`${BASE_URL}/admin/users/${userId}/disable`, { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + async enableUser(userId: string, data: UserEnabledRequest = {}): Promise> { + return apiFetch(`${BASE_URL}/admin/users/${userId}/enable`, { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + async setUserRoles(userId: string, data: SetUserRolesRequest): Promise> { + return apiFetch(`${BASE_URL}/admin/users/${userId}/role`, { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + async updatePlayerStats(userId: string, data: UpdatePlayerStatsRequest): Promise> { + return apiFetch(`${BASE_URL}/admin/users/${userId}/stats`, { + method: 'PUT', + body: JSON.stringify(data), + }); + }, } \ No newline at end of file diff --git a/apps/frontend/src/core/api/auth/auth.types.ts b/apps/frontend/src/core/api/auth/auth.types.ts index 34c858a9..5a244380 100644 --- a/apps/frontend/src/core/api/auth/auth.types.ts +++ b/apps/frontend/src/core/api/auth/auth.types.ts @@ -126,36 +126,86 @@ export interface InternalRequestInit extends RequestInit { * These types correspond to the /admin endpoints in the BFF */ -export interface PageInfoDto { +export interface PageInfo { nextCursor?: string | null; hasNextPage: boolean; } -export interface UserSearchQueryDto { +export interface UserSearchRequest { query?: string; cursor?: string; limit?: number; } -export interface UserSearchResponseDto { +export interface UserSearchResponse { items: UserAuthView[]; - pageInfo: PageInfoDto; + pageInfo: PageInfo; } -export interface DisableUserRequestDto { +export interface UserDisabledRequest { reason: string; revokeSessions?: boolean; } -export interface EnableUserRequestDto { +export interface UserDisabledResponse { + userId: string; + status: string; + revokedSessions: number; +} + +export interface UserEnabledRequest { reason?: string; } -export interface UpdatePlayerStatsDto { +export interface UserEnabledResponse { + userId: string; + status: string; +} + +export interface SetUserRolesRequest { + roles: string[]; +} + +export interface UserRolesResponse { + userId: string; + roles: string[]; + updatedAt: string; +} + +export interface UpdatePlayerStatsRequest { + matchesWon?: string[]; + matchesLost?: string[]; + achievements?: string[]; + weapons?: string[]; + matchParticipants?: string[]; + xp?: number; + level?: number; wins?: number; losses?: number; + kills?: number; + deaths?: number; + damageDealt?: number; + damageTaken?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface UpdatePlayerStatsResponse { + matchesWon?: string[]; + matchesLost?: string[]; + achievements?: string[]; + weapons?: string[]; + matchParticipants?: string[]; xp?: number; level?: number; + wins?: number; + losses?: number; + kills?: number; + deaths?: number; + damageDealt?: number; + damageTaken?: number; + createdAt?: string; + updatedAt?: string; } /** * --- Errors --- From 93135bb78843c9c8d4237684c82d30b2c350c25e Mon Sep 17 00:00:00 2001 From: tbui-quo Date: Mon, 11 May 2026 19:50:22 +0200 Subject: [PATCH 03/26] - some imports where is missing that's why we got ECONNREFUSED bug during --- apps/auth-user/package-lock.json | 10 +++++++++- apps/auth-user/package.json | 5 +++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/auth-user/package-lock.json b/apps/auth-user/package-lock.json index a7d76db5..014af2eb 100644 --- a/apps/auth-user/package-lock.json +++ b/apps/auth-user/package-lock.json @@ -17,7 +17,7 @@ "@prisma/adapter-pg": "^7.5.0", "@prisma/client": "^7.5.0", "argon2": "^0.44.0", - "axios": "^1.4.0", + "axios": "^1.16.0", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.15.1", @@ -35,6 +35,7 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", + "@types/axios": "^0.9.36", "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", @@ -3084,6 +3085,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/axios": { + "version": "0.9.36", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", + "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/apps/auth-user/package.json b/apps/auth-user/package.json index d3bbdcd5..3786ca0f 100644 --- a/apps/auth-user/package.json +++ b/apps/auth-user/package.json @@ -20,7 +20,6 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "axios": "^1.4.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", @@ -29,6 +28,7 @@ "@prisma/adapter-pg": "^7.5.0", "@prisma/client": "^7.5.0", "argon2": "^0.44.0", + "axios": "^1.16.0", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.15.1", @@ -46,6 +46,7 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", + "@types/axios": "^0.9.36", "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", @@ -85,4 +86,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} From 234c7e1dd75dd8f55fe26436df72701f2469dcaf Mon Sep 17 00:00:00 2001 From: tbui-quo Date: Mon, 11 May 2026 19:51:18 +0200 Subject: [PATCH 04/26] - working on action modal for admin - added adminusermanage page --- apps/BFF/src/modules/config/env.validation.ts | 2 +- apps/frontend/app/(site)/admin/users/page.tsx | 84 +++++++++++++++++++ .../src/components/admin/AdminActionModal.tsx | 49 +++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/app/(site)/admin/users/page.tsx create mode 100644 apps/frontend/src/components/admin/AdminActionModal.tsx diff --git a/apps/BFF/src/modules/config/env.validation.ts b/apps/BFF/src/modules/config/env.validation.ts index 6cc7f9bc..73d649df 100644 --- a/apps/BFF/src/modules/config/env.validation.ts +++ b/apps/BFF/src/modules/config/env.validation.ts @@ -6,7 +6,7 @@ const baseSchema = z.object({ .default('development'), PORT: z.coerce.number().int().min(1).max(65535).default(3000), - AUTH_SERVICE_URL: z.string().url(), + AUTH_SERVICE_URL: z.string().url().default('http://auth_service:3000'), STATS_SERVICE_URL: z.string().url().default('http://stats_service:3000'), GOOGLE_CLIENT_ID: z.string().min(1), diff --git a/apps/frontend/app/(site)/admin/users/page.tsx b/apps/frontend/app/(site)/admin/users/page.tsx new file mode 100644 index 00000000..d0f3ccb0 --- /dev/null +++ b/apps/frontend/app/(site)/admin/users/page.tsx @@ -0,0 +1,84 @@ +'use client'; + +import React, { useEffect, useState, useCallback } from 'react'; +import { authClient } from '@/src/core/api/auth/auth.client'; +import { UserAuthView, UserSearchResponse } from '@/src/core/api/auth/auth.types'; +import { UserTable } from '@/src/components/admin/UserTable'; +import { UserSearchForm } from '@/src/components/admin/UserSearchForm'; +import { AdminActionModal } from '@/src/components/admin/AdminActionModal'; + +export default function AdminUserManagement() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + + const [modalConfig, setModalConfig] = useState<{ + isOpen: boolean; + targetUser: UserAuthView | null; + }>({ isOpen: false, targetUser: null }); + + const fetchUsers = useCallback(async (query = '') => { + setLoading(true); + const result = await authClient.searchUsers({ query, limit: 10 }); + if (result.ok) { + setData(result.data); + } else { + console.error("Failed to fetch users:", result.error.message); + } + setLoading(false); + }, []); + + useEffect(() => { + fetchUsers(); + }, [fetchUsers]); + + const handleSearch = (query: string) => { + setSearchQuery(query); + fetchUsers(query); + }; + + const handleToggleStatusClick = (user: UserAuthView) => { + setModalConfig({ isOpen: true, targetUser: user }); + }; + + const confirmToggleStatus = async (reason?: string) => { + const user = modalConfig.targetUser; + if (!user) return; + + const isBanning = user.status === 'active'; + const result = isBanning + ? await authClient.disableUser(user.id, { reason: reason || 'Admin action' }) + : await authClient.enableUser(user.id, { reason: 'Admin unban' }); + + if (result.ok) { + fetchUsers(searchQuery); + } + setModalConfig({ isOpen: false, targetUser: null }); + }; + + return ( +
+

User Management

+ +
+ +
+ + console.log("Navigate to stats for", id)} + onToggleStatus={handleToggleStatusClick} + /> + + setModalConfig({ isOpen: false, targetUser: null })} + /> +
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/components/admin/AdminActionModal.tsx b/apps/frontend/src/components/admin/AdminActionModal.tsx new file mode 100644 index 00000000..9afe1d24 --- /dev/null +++ b/apps/frontend/src/components/admin/AdminActionModal.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; + +interface AdminModalProps { + isOpen: boolean; + title: string; + description: string; + requireReason?: boolean; + onConfirm: (reason?: string) => void; + onClose: () => void; +} + +export const AdminActionModal: React.FC = ( + {isOpen, title, description, requireReason, onConfirm, onClose +}) => { + const [reason, setReason] = useState(''); + + if (!isOpen) return null; + + return ( +
+
+

{title}

+

{description}

+ + {requireReason && ( +