From c686965b3773b5bf16baff42c91c82d7b7e66eec Mon Sep 17 00:00:00 2001 From: Mitchell Magro Date: Tue, 11 Nov 2025 14:46:30 +0100 Subject: [PATCH] Added reset password for admin --- app/api/auth/register/route.ts | 2 + app/api/auth/reset-password/[id]/route.ts | 60 ++++++++++++ app/components/Confirm/Confirm.tsx | 49 ++++++++++ app/features/UserRoles/userRoleCard.tsx | 112 ++++++++++++++++++---- app/redux/auth/authSlice.tsx | 55 +++++++++++ 5 files changed, 261 insertions(+), 17 deletions(-) create mode 100644 app/api/auth/reset-password/[id]/route.ts create mode 100644 app/components/Confirm/Confirm.tsx diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 6e03969..31982d3 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -48,10 +48,12 @@ export async function POST(request: Request) { // Handle backend response if (!resp.ok) { const errorData = await safeJson(resp); + console.error("Registration proxy error:", errorData); return NextResponse.json( { success: false, message: errorData?.message || "Registration failed", + error: errorData?.error || "Unknown error", }, { status: resp.status } ); diff --git a/app/api/auth/reset-password/[id]/route.ts b/app/api/auth/reset-password/[id]/route.ts new file mode 100644 index 0000000..8f49468 --- /dev/null +++ b/app/api/auth/reset-password/[id]/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583"; +const COOKIE_NAME = "auth_token"; + +export async function PUT( + request: Request, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + if (!id) { + return NextResponse.json( + { success: false, message: "User ID is required" }, + { status: 400 } + ); + } + + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + + if (!token) { + return NextResponse.json( + { success: false, message: "No authentication token found" }, + { status: 401 } + ); + } + + const resp = await fetch( + `${BE_BASE_URL}/api/v1/auth/reset-password/${encodeURIComponent(id)}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + // Attempt to parse JSON; fall back to status-only response + let data: unknown = null; + try { + data = await resp.json(); + } catch { + data = { success: resp.ok }; + } + + return NextResponse.json(data ?? { success: resp.ok }, { + status: resp.status, + }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Unknown error"; + return NextResponse.json( + { success: false, message: "Internal server error", error: message }, + { status: 500 } + ); + } +} diff --git a/app/components/Confirm/Confirm.tsx b/app/components/Confirm/Confirm.tsx new file mode 100644 index 0000000..c815ef5 --- /dev/null +++ b/app/components/Confirm/Confirm.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { Stack, Typography, Button } from "@mui/material"; + +interface ConfirmProps { + onSubmit: () => void | Promise; + onClose: () => void; + message?: string; + confirmLabel?: string; + cancelLabel?: string; + disabled?: boolean; +} + +/** + * Simple confirmation content to be rendered inside the shared Modal. + * Shows an "Are you sure?" message and calls the parent's onSubmit when confirmed. + */ +const Confirm: React.FC = ({ + onSubmit, + onClose, + message = "Are you sure you want to continue?", + confirmLabel = "Yes, continue", + cancelLabel = "Cancel", + disabled = false, +}) => { + const handleConfirm = async () => { + await Promise.resolve(onSubmit()); + }; + + return ( + + {message} + + + + + + ); +}; + +export default Confirm; diff --git a/app/features/UserRoles/userRoleCard.tsx b/app/features/UserRoles/userRoleCard.tsx index 4d6e6f5..ce6578c 100644 --- a/app/features/UserRoles/userRoleCard.tsx +++ b/app/features/UserRoles/userRoleCard.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import toast from "react-hot-toast"; import { Card, CardContent, @@ -13,15 +14,20 @@ import { import { Edit, Delete, - Visibility, VpnKey, InfoOutlined, AdminPanelSettings, - History, + ContentCopy, } from "@mui/icons-material"; import EditUser from "./EditUser/EditUser"; import { IUser } from "../Pages/Admin/Users/interfaces"; import DeleteUser from "./DeleteUser/DeleteUser"; +import Modal from "@/app/components/Modal/Modal"; +import { useDispatch } from "react-redux"; +import { AppDispatch } from "@/app/redux/store"; +import { resetPassword } from "@/app/redux/auth/authSlice"; +import Confirm from "../../components/Confirm/Confirm"; +import { ThunkSuccess } from "@/app/redux/types"; import "./User.scss"; interface Props { @@ -30,9 +36,11 @@ interface Props { export default function UserRoleCard({ user }: Props) { const [isEditing, setIsEditing] = useState(false); + const [showConfirmModal, setShowConfirmModal] = useState(false); const [openDeleteUser, setOpenDeleteUser] = useState(false); + const dispatch = useDispatch(); const { username, first_name, last_name, email, groups } = user; - + const [newPassword, setNewPassword] = useState(null); const handleEditClick = () => { setIsEditing(!isEditing); }; @@ -41,6 +49,41 @@ export default function UserRoleCard({ user }: Props) { setOpenDeleteUser(true); }; + const handleResetPasswordSubmit = async () => { + try { + const resultAction = await dispatch( + resetPassword({ id: user.id as string }) + ); + if (resetPassword.fulfilled.match(resultAction)) { + setNewPassword( + ( + resultAction.payload as ThunkSuccess<{ + success: boolean; + message: string; + newPassword: string | null; + }> + )?.newPassword || null + ); + toast.success( + ( + resultAction.payload as ThunkSuccess<{ + success: boolean; + message: string; + newPassword: string | null; + }> + )?.message || "Password reset successfully" + ); + } else if (resetPassword.rejected.match(resultAction)) { + toast.error( + (resultAction.payload as string) || "Failed to reset password" + ); + } + } catch (e: unknown) { + const message = e instanceof Error ? e.message : "Unexpected error"; + toast.error(message); + } + }; + return ( @@ -57,20 +100,18 @@ export default function UserRoleCard({ user }: Props) { {true && ( } label="Admin" size="small" /> )} - - - - - - - - - + { + setNewPassword(null); + setShowConfirmModal(true); + }} + > @@ -85,11 +126,6 @@ export default function UserRoleCard({ user }: Props) { {/* Merchants + Roles */} Merchants - {/* - {merchants.map((m) => ( - - ))} - */} @@ -121,6 +157,48 @@ export default function UserRoleCard({ user }: Props) { onClose={() => setOpenDeleteUser(false)} /> )} + {showConfirmModal && ( + setShowConfirmModal(false)} + title="Reset Password" + > + {newPassword && ( +
+ + + {newPassword} + + + { + try { + await navigator.clipboard.writeText(newPassword); + toast.success("Copied to clipboard"); + } catch { + toast.error("Failed to copy"); + } + }} + size="small" + > + + + + +
+ )} + {!newPassword && ( + setShowConfirmModal(false)} + onSubmit={handleResetPasswordSubmit} + message="Are you sure you want to reset the password for this user?" + confirmLabel="Reset Password" + cancelLabel="Cancel" + /> + )} +
+ )}
); diff --git a/app/redux/auth/authSlice.tsx b/app/redux/auth/authSlice.tsx index 32ed97b..d9bb870 100644 --- a/app/redux/auth/authSlice.tsx +++ b/app/redux/auth/authSlice.tsx @@ -154,6 +154,44 @@ export const changePassword = createAsyncThunk< } ); +// ---------------- Reset Password (Admin) ---------------- +export const resetPassword = createAsyncThunk< + ThunkSuccess<{ success: boolean }>, + { id: string }, + { rejectValue: ThunkError } +>("auth/resetPassword", async ({ id }, { rejectWithValue }) => { + try { + const res = await fetch( + `/api/auth/reset-password/${encodeURIComponent(id)}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + } + ); + + const data = await res.json(); + if (!res.ok) { + return rejectWithValue( + (data.message as string) || "Password reset failed" + ); + } + + return { + success: data.success ?? true, + message: data.message || "Password reset successfully", + newPassword: data.user?.password || null, + } as ThunkSuccess<{ + success: boolean; + message: string; + newPassword: string | null; + }>; + } catch (err: unknown) { + return rejectWithValue( + (err as Error).message || "Network error during password reset" + ); + } +}); + // ---------------- Unified Validate Auth ---------------- export const validateAuth = createAsyncThunk< { tokenInfo: TokenInfo | null }, @@ -327,6 +365,23 @@ const authSlice = createSlice({ state.error = action.payload as string; state.authMessage = action.payload as string; state.addedUser = null; + }) + // Reset Password (Admin) + .addCase(resetPassword.pending, state => { + state.status = "loading"; + state.authMessage = "Resetting password..."; + }) + .addCase(resetPassword.fulfilled, (state, action) => { + state.status = "succeeded"; + state.authMessage = action.payload.message; + state.mustChangePassword = action.payload.success; + toast.success(action.payload.message || "Password reset successfully"); + }) + .addCase(resetPassword.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload as string; + state.authMessage = action.payload as string; + toast.error((action.payload as string) || "Password reset failed"); }); }, });