diff --git a/app/api/auth/change-password/route.ts b/app/api/auth/change-password/route.ts index c21da7a..559d50c 100644 --- a/app/api/auth/change-password/route.ts +++ b/app/api/auth/change-password/route.ts @@ -25,10 +25,6 @@ export async function POST(request: Request) { try { const payload = decodeJwt(token); mustChangePassword = payload.MustChangePassword || false; - console.log( - "🔍 Current JWT MustChangePassword flag:", - mustChangePassword - ); } catch (err) { console.error("❌ Failed to decode current JWT:", err); } @@ -71,10 +67,6 @@ export async function POST(request: Request) { const data = await resp.json(); if (!resp.ok) { - console.log("[DEBUG] [CHANGE-PASSWORD] Error response:", { - status: resp.status, - data, - }); return NextResponse.json( { success: false, message: data?.message || "Password change failed" }, { status: resp.status } @@ -89,17 +81,6 @@ export async function POST(request: Request) { }); if (newToken) { - try { - const payload = decodeJwt(newToken); - console.log("🔍 New JWT payload:", payload); - console.log( - "🔍 must_change_password flag:", - payload.must_change_password - ); - } catch (err) { - console.error("❌ Failed to decode new JWT:", err); - } - // Derive maxAge from JWT exp if available; fallback to 12h let maxAge = 60 * 60 * 12; try { diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index b3a9b41..6e03969 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,11 +1,10 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { formatPhoneDisplay } from "@/app/features/UserRoles/utils"; const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583"; const COOKIE_NAME = "auth_token"; -// Interface matching the backend RegisterRequest +// Interface matching the backend RegisterRequest and frontend IEditUserForm interface RegisterRequest { creator: string; email: string; @@ -18,23 +17,9 @@ interface RegisterRequest { username: string; } -// Frontend form interface -interface FrontendRegisterForm { - email: string; - firstName: string; - lastName: string; - username: string; - phone?: string; - jobTitle?: string; - groups?: string[]; - merchants?: string[]; - creator?: string; - countryCode: string; -} - export async function POST(request: Request) { try { - const body: FrontendRegisterForm = await request.json(); + const body: RegisterRequest = await request.json(); // Get the auth token from cookies const cookieStore = await cookies(); @@ -50,35 +35,6 @@ export async function POST(request: Request) { ); } - // Validate required fields - const requiredFields = ["email", "firstName", "lastName", "username"]; - const missingFields = requiredFields.filter( - field => !body[field as keyof FrontendRegisterForm] - ); - - if (missingFields.length > 0) { - return NextResponse.json( - { - success: false, - message: `Missing required fields: ${missingFields.join(", ")}`, - }, - { status: 400 } - ); - } - - // Map frontend payload to backend RegisterRequest format - const registerPayload: RegisterRequest = { - creator: body.creator || "", - email: body.email, - first_name: body.firstName, - groups: body.groups || ["Reader"], // Default to empty array if not provided - job_title: body.jobTitle || "Reader", - last_name: body.lastName, - merchants: body.merchants || ["Win Bot"], // Default to empty array if not provided - phone: body.phone ? formatPhoneDisplay(body.phone, body.countryCode) : "", - username: body.username, - }; - // Call backend registration endpoint const resp = await fetch(`${BE_BASE_URL}/api/v1/auth/register`, { method: "POST", @@ -86,15 +42,12 @@ export async function POST(request: Request) { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify(registerPayload), + body: JSON.stringify(body), }); - console.log("[DEBUG] [REGISTER-PAYLOAD]: ", registerPayload); - // Handle backend response if (!resp.ok) { const errorData = await safeJson(resp); - console.log("[DEBUG] [REGISTER-ERROR]: ", errorData); return NextResponse.json( { success: false, @@ -106,8 +59,6 @@ export async function POST(request: Request) { const data = await resp.json(); - console.log("[DEBUG] [REGISTER]: ", data); - return NextResponse.json( { success: true, diff --git a/app/api/auth/validate/route.ts b/app/api/auth/validate/route.ts index ea7d7eb..ea6b10b 100644 --- a/app/api/auth/validate/route.ts +++ b/app/api/auth/validate/route.ts @@ -23,9 +23,6 @@ export async function POST() { }); const data = await resp.json(); - - console.log("[DEBUG] [VALIDATE-AUTH][ROUTE][data]: ", data); - return NextResponse.json(data, { status: resp.status }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Unknown error"; diff --git a/app/api/dashboard/admin/users/[id]/route.ts b/app/api/dashboard/admin/users/[id]/route.ts index a3fb66a..16f7230 100644 --- a/app/api/dashboard/admin/users/[id]/route.ts +++ b/app/api/dashboard/admin/users/[id]/route.ts @@ -4,18 +4,79 @@ import { NextResponse } from "next/server"; const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; const COOKIE_NAME = "auth_token"; -export async function PATCH( +// Field mapping: snake_case input -> { snake_case for data, PascalCase for fields } +// Matches API metadata field_names.users mapping +const FIELD_MAPPING: Record = { + id: { dataKey: "id", fieldName: "ID" }, + email: { dataKey: "email", fieldName: "Email" }, + first_name: { dataKey: "first_name", fieldName: "FirstName" }, + last_name: { dataKey: "last_name", fieldName: "LastName" }, + username: { dataKey: "username", fieldName: "Username" }, + phone: { dataKey: "phone", fieldName: "Phone" }, + job_title: { dataKey: "job_title", fieldName: "JobTitle" }, + password: { dataKey: "password", fieldName: "Password" }, + temp_link: { dataKey: "temp_link", fieldName: "TempLink" }, + temp_password: { dataKey: "temp_password", fieldName: "TempPassword" }, + temp_expiry: { dataKey: "temp_expiry", fieldName: "TempExpiry" }, + groups: { dataKey: "groups", fieldName: "Groups" }, + merchants: { dataKey: "merchants", fieldName: "Merchants" }, + enabled: { dataKey: "enabled", fieldName: "Enabled" }, +}; + +/** + * Transforms frontend snake_case data to backend format + * with data (snake_case) and fields (PascalCase) arrays + */ +function transformUserUpdateData(updates: Record): { + data: Record; + fields: string[]; +} { + const data: Record = {}; + const fields: string[] = []; + + for (const [key, value] of Object.entries(updates)) { + // Skip undefined/null values + if (value === undefined || value === null) { + continue; + } + + const mapping = FIELD_MAPPING[key]; + if (mapping) { + // Use the dataKey for the data object (snake_case) + data[mapping.dataKey] = value; + // Use the fieldName for the fields array (PascalCase) + fields.push(mapping.fieldName); + } else { + // If no mapping exists, use the key as-is (for backwards compatibility) + data[key] = value; + // Convert snake_case to PascalCase for fields + const pascalCase = key + .split("_") + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(""); + fields.push(pascalCase); + } + } + + return { data, fields }; +} + +export async function PUT( request: Request, { params }: { params: { id: string } } ) { try { - console.log("[PATCH /users] - params", params); - const { id } = params; + const { id } = await params; const body = await request.json(); + + // Transform the request body to match backend format + const transformedBody = transformUserUpdateData(body); + console.log("[PUT /api/v1/users/{id}] - transformed body", transformedBody); + // Get the auth token from cookies const { cookies } = await import("next/headers"); - const cookieStore = cookies(); - const token = (await cookieStore).get(COOKIE_NAME)?.value; + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; if (!token) { return NextResponse.json( @@ -24,21 +85,23 @@ export async function PATCH( ); } - const response = await fetch(`${BE_BASE_URL}/users/${id}`, { - method: "PATCH", + // According to swagger: /api/v1/users/{id} + const response = await fetch(`${BE_BASE_URL}/api/v1/users/${id}`, { + method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify(body), + body: JSON.stringify(transformedBody), }); const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (err: any) { - console.error("Proxy PATCH /users error:", err); + } catch (err: unknown) { + const errorMessage = + err instanceof Error ? err.message : "Unknown error occurred"; return NextResponse.json( - { message: "Internal server error", error: err.message }, + { message: "Internal server error", error: errorMessage }, { status: 500 } ); } @@ -49,7 +112,7 @@ export async function DELETE( { params }: { params: { id: string } } ) { try { - const { id } = params; + const { id } = await params; const { cookies } = await import("next/headers"); const cookieStore = await cookies(); const token = cookieStore.get(COOKIE_NAME)?.value; @@ -70,7 +133,7 @@ export async function DELETE( }); // Some backends return empty body for DELETE; handle safely - let data: any = null; + let data: unknown = null; try { data = await response.json(); } catch { @@ -80,10 +143,12 @@ export async function DELETE( return NextResponse.json(data ?? { success: response.ok }, { status: response.status, }); - } catch (err: any) { + } catch (err: unknown) { console.error("Proxy DELETE /api/v1/users/{id} error:", err); + const errorMessage = + err instanceof Error ? err.message : "Unknown error occurred"; return NextResponse.json( - { message: "Internal server error", error: err.message }, + { message: "Internal server error", error: errorMessage }, { status: 500 } ); } diff --git a/app/api/dashboard/admin/users/route.ts b/app/api/dashboard/admin/users/route.ts index 979d8e9..759225a 100644 --- a/app/api/dashboard/admin/users/route.ts +++ b/app/api/dashboard/admin/users/route.ts @@ -28,7 +28,6 @@ export async function GET(request: Request) { cache: "no-store", }); - console.log("[DEBUG] - Response", response); const data = await response.json(); return NextResponse.json(data, { status: response.status }); } catch (err: unknown) { diff --git a/app/features/Pages/Admin/Users/users.tsx b/app/features/Pages/Admin/Users/users.tsx index 92d8c6a..567bb6f 100644 --- a/app/features/Pages/Admin/Users/users.tsx +++ b/app/features/Pages/Admin/Users/users.tsx @@ -17,6 +17,8 @@ const Users: React.FC = ({ users }) => { const [showAddUser, setShowAddUser] = useState(false); const dispatch = useDispatch(); + console.log("[Users] - users", users); + return (
= ({ users }) => { {user.username} - diff --git a/app/features/Pages/Settings/SettingsPersonalInfo.tsx b/app/features/Pages/Settings/SettingsPersonalInfo.tsx index 89f4c8d..9b21b09 100644 --- a/app/features/Pages/Settings/SettingsPersonalInfo.tsx +++ b/app/features/Pages/Settings/SettingsPersonalInfo.tsx @@ -11,7 +11,7 @@ import { } from "@mui/material"; import { useSelector, useDispatch } from "react-redux"; import { AppDispatch, RootState } from "@/app/redux/store"; -import { updateUserDetails } from "@/app/redux/auth/authSlice"; +import { updateUserDetails } from "@/app/redux/user/userSlice"; const SettingsPersonalInfo: React.FC = () => { const user = useSelector((state: RootState) => state.auth.user); diff --git a/app/features/UserRoles/AddUser/AddUser.tsx b/app/features/UserRoles/AddUser/AddUser.tsx index 2e84c2d..8be2edb 100644 --- a/app/features/UserRoles/AddUser/AddUser.tsx +++ b/app/features/UserRoles/AddUser/AddUser.tsx @@ -26,14 +26,13 @@ const AddUser: React.FC = ({ open, onClose }) => { ); const [form, setForm] = useState({ username: "", - firstName: "", - lastName: "", + first_name: "", + last_name: "", email: "", phone: "", - role: "", merchants: [], groups: [], - jobTitle: "", + job_title: "", }); const [phoneError, setPhoneError] = useState(""); @@ -94,12 +93,12 @@ const AddUser: React.FC = ({ open, onClose }) => { } if ( - !form.firstName || - !form.lastName || + !form.first_name || + !form.last_name || !form.email || form.merchants.length === 0 || form.groups.length === 0 || - !form.jobTitle + !form.job_title ) { return; } @@ -136,16 +135,16 @@ const AddUser: React.FC = ({ open, onClose }) => { required /> @@ -235,8 +234,8 @@ const AddUser: React.FC = ({ open, onClose }) => {
{ /> +
+ + + {form.merchants.length > 0 && ( +
+ {form.merchants.map((merchant, index) => ( + + {merchant} + + + ))} +
+ )} +
+ +
+ + + {form.groups.length > 0 && ( +
+ {form.groups.map((group, index) => ( + + {group} + + + ))} +
+ )} +
+ +
+ + +
+ { inputMode="tel" autoComplete="tel" /> +
- -
diff --git a/app/features/UserRoles/User.interfaces.ts b/app/features/UserRoles/User.interfaces.ts index 7771892..f0f118c 100644 --- a/app/features/UserRoles/User.interfaces.ts +++ b/app/features/UserRoles/User.interfaces.ts @@ -1,22 +1,20 @@ export interface IEditUserForm { username: string; - firstName: string; - lastName: string; + first_name: string; + last_name: string; email: string; - role: string; phone: string; merchants: string[]; groups: string[]; - jobTitle: string; + job_title: string; } export type EditUserField = | "merchants" | "groups" - | "jobTitle" + | "job_title" | "username" - | "firstName" - | "lastName" + | "first_name" + | "last_name" | "email" - | "role" | "phone"; diff --git a/app/features/UserRoles/userRoleCard.tsx b/app/features/UserRoles/userRoleCard.tsx index 886c195..4d6e6f5 100644 --- a/app/features/UserRoles/userRoleCard.tsx +++ b/app/features/UserRoles/userRoleCard.tsx @@ -20,9 +20,9 @@ import { History, } from "@mui/icons-material"; import EditUser from "./EditUser/EditUser"; -import "./User.scss"; import { IUser } from "../Pages/Admin/Users/interfaces"; import DeleteUser from "./DeleteUser/DeleteUser"; +import "./User.scss"; interface Props { user: IUser; @@ -32,6 +32,7 @@ export default function UserRoleCard({ user }: Props) { const [isEditing, setIsEditing] = useState(false); const [openDeleteUser, setOpenDeleteUser] = useState(false); const { username, first_name, last_name, email, groups } = user; + const handleEditClick = () => { setIsEditing(!isEditing); }; diff --git a/app/redux/auth/authSlice.tsx b/app/redux/auth/authSlice.tsx index 97f9164..32ed97b 100644 --- a/app/redux/auth/authSlice.tsx +++ b/app/redux/auth/authSlice.tsx @@ -219,38 +219,6 @@ export const addUser = createAsyncThunk< } }); -// ---------------- Update User Details ---------------- -export const updateUserDetails = createAsyncThunk< - ThunkSuccess<{ user: IUserResponse }>, - { id: string; updates: Partial }, - { rejectValue: ThunkError } ->("auth/updateUserDetails", async ({ id, updates }, { rejectWithValue }) => { - try { - const res = await fetch(`/api/dashboard/admin/users/${id}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(updates), - }); - - const data = await res.json(); - - if (!res.ok) { - return rejectWithValue(data.message || "Failed to update user details"); - } - - return { - message: data.message || "User details updated successfully", - user: data.user, - }; - } catch (err: unknown) { - return rejectWithValue( - (err as Error).message || "Network error during user update" - ); - } -}); - // ---------------- Slice ---------------- const authSlice = createSlice({ name: "auth", @@ -359,21 +327,6 @@ const authSlice = createSlice({ state.error = action.payload as string; state.authMessage = action.payload as string; state.addedUser = null; - }) - // Update User Details - .addCase(updateUserDetails.pending, state => { - state.status = "loading"; - state.authMessage = "Updating user details..."; - }) - .addCase(updateUserDetails.fulfilled, (state, action) => { - state.status = "succeeded"; - state.user = action.payload.user; - state.authMessage = action.payload.message; - }) - .addCase(updateUserDetails.rejected, (state, action) => { - state.status = "failed"; - state.error = action.payload as string; - state.authMessage = action.payload as string; }); }, }); diff --git a/app/redux/user/userSlice.ts b/app/redux/user/userSlice.ts index 56188f0..70e05e5 100644 --- a/app/redux/user/userSlice.ts +++ b/app/redux/user/userSlice.ts @@ -63,7 +63,7 @@ export const editUser = createAsyncThunk< >("user/editUser", async ({ id, updates }, { rejectWithValue }) => { try { const res = await fetch(`/api/dashboard/admin/users/${id}`, { - method: "PATCH", + method: "PUT", headers: { "Content-Type": "application/json", }, @@ -87,6 +87,38 @@ export const editUser = createAsyncThunk< } }); +// ---------------- Update User Details ---------------- +export const updateUserDetails = createAsyncThunk< + ThunkSuccess<{ user: IUserResponse }>, + { id: string; updates: Record }, + { rejectValue: ThunkError } +>("user/updateUserDetails", async ({ id, updates }, { rejectWithValue }) => { + try { + const res = await fetch(`/api/dashboard/admin/users/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updates), + }); + + const data = await res.json(); + + if (!res.ok) { + return rejectWithValue(data.message || "Failed to update user details"); + } + + return { + message: data.message || "User details updated successfully", + user: data.user, + }; + } catch (err: unknown) { + return rejectWithValue( + (err as Error).message || "Network error during user update" + ); + } +}); + // ---------------- Delete User ---------------- export const deleteUser = createAsyncThunk< ThunkSuccess<{ id: string }>, @@ -176,6 +208,25 @@ const userSlice = createSlice({ state.message = action.payload as string; state.editedUser = null; }) + // Update User Details + .addCase(updateUserDetails.pending, state => { + state.status = "loading"; + state.message = "Updating user details..."; + state.editedUser = null; + state.error = null; + }) + .addCase(updateUserDetails.fulfilled, (state, action) => { + state.status = "succeeded"; + state.message = action.payload.message; + state.editedUser = action.payload.user; + state.error = null; + }) + .addCase(updateUserDetails.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload as string; + state.message = action.payload as string; + state.editedUser = null; + }) // Delete User .addCase(deleteUser.pending, state => { state.status = "loading";