Added reset password for admin
This commit is contained in:
parent
a3ad2a8937
commit
c686965b37
@ -48,10 +48,12 @@ export async function POST(request: Request) {
|
|||||||
// Handle backend response
|
// Handle backend response
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const errorData = await safeJson(resp);
|
const errorData = await safeJson(resp);
|
||||||
|
console.error("Registration proxy error:", errorData);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
message: errorData?.message || "Registration failed",
|
message: errorData?.message || "Registration failed",
|
||||||
|
error: errorData?.error || "Unknown error",
|
||||||
},
|
},
|
||||||
{ status: resp.status }
|
{ status: resp.status }
|
||||||
);
|
);
|
||||||
|
|||||||
60
app/api/auth/reset-password/[id]/route.ts
Normal file
60
app/api/auth/reset-password/[id]/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/components/Confirm/Confirm.tsx
Normal file
49
app/components/Confirm/Confirm.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Typography, Button } from "@mui/material";
|
||||||
|
|
||||||
|
interface ConfirmProps {
|
||||||
|
onSubmit: () => void | Promise<void>;
|
||||||
|
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<ConfirmProps> = ({
|
||||||
|
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 (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Typography variant="body1">{message}</Typography>
|
||||||
|
<Stack direction="row" spacing={2} justifyContent="flex-end">
|
||||||
|
<Button variant="outlined" onClick={onClose} disabled={disabled}>
|
||||||
|
{cancelLabel}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Confirm;
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -13,15 +14,20 @@ import {
|
|||||||
import {
|
import {
|
||||||
Edit,
|
Edit,
|
||||||
Delete,
|
Delete,
|
||||||
Visibility,
|
|
||||||
VpnKey,
|
VpnKey,
|
||||||
InfoOutlined,
|
InfoOutlined,
|
||||||
AdminPanelSettings,
|
AdminPanelSettings,
|
||||||
History,
|
ContentCopy,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import EditUser from "./EditUser/EditUser";
|
import EditUser from "./EditUser/EditUser";
|
||||||
import { IUser } from "../Pages/Admin/Users/interfaces";
|
import { IUser } from "../Pages/Admin/Users/interfaces";
|
||||||
import DeleteUser from "./DeleteUser/DeleteUser";
|
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";
|
import "./User.scss";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -30,9 +36,11 @@ interface Props {
|
|||||||
|
|
||||||
export default function UserRoleCard({ user }: Props) {
|
export default function UserRoleCard({ user }: Props) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||||
const [openDeleteUser, setOpenDeleteUser] = useState(false);
|
const [openDeleteUser, setOpenDeleteUser] = useState(false);
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const { username, first_name, last_name, email, groups } = user;
|
const { username, first_name, last_name, email, groups } = user;
|
||||||
|
const [newPassword, setNewPassword] = useState<string | null>(null);
|
||||||
const handleEditClick = () => {
|
const handleEditClick = () => {
|
||||||
setIsEditing(!isEditing);
|
setIsEditing(!isEditing);
|
||||||
};
|
};
|
||||||
@ -41,6 +49,41 @@ export default function UserRoleCard({ user }: Props) {
|
|||||||
setOpenDeleteUser(true);
|
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 (
|
return (
|
||||||
<Card sx={{ mb: 2, minWidth: "100%" }}>
|
<Card sx={{ mb: 2, minWidth: "100%" }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -57,20 +100,18 @@ export default function UserRoleCard({ user }: Props) {
|
|||||||
{true && (
|
{true && (
|
||||||
<Chip icon={<AdminPanelSettings />} label="Admin" size="small" />
|
<Chip icon={<AdminPanelSettings />} label="Admin" size="small" />
|
||||||
)}
|
)}
|
||||||
<IconButton>
|
|
||||||
<History />
|
|
||||||
</IconButton>
|
|
||||||
<Tooltip title="Edit">
|
<Tooltip title="Edit">
|
||||||
<IconButton onClick={handleEditClick}>
|
<IconButton onClick={handleEditClick}>
|
||||||
<Edit />
|
<Edit />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="View">
|
<Tooltip
|
||||||
<IconButton>
|
title="Reset Password"
|
||||||
<Visibility />
|
onClick={() => {
|
||||||
</IconButton>
|
setNewPassword(null);
|
||||||
</Tooltip>
|
setShowConfirmModal(true);
|
||||||
<Tooltip title="Reset Password">
|
}}
|
||||||
|
>
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<VpnKey />
|
<VpnKey />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -85,11 +126,6 @@ export default function UserRoleCard({ user }: Props) {
|
|||||||
{/* Merchants + Roles */}
|
{/* Merchants + Roles */}
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
<Typography fontWeight="bold">Merchants</Typography>
|
<Typography fontWeight="bold">Merchants</Typography>
|
||||||
{/* <Stack direction="row" spacing={1} mt={1}>
|
|
||||||
{merchants.map((m) => (
|
|
||||||
<Chip key={m} label={m} size="small" />
|
|
||||||
))}
|
|
||||||
</Stack> */}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
@ -121,6 +157,48 @@ export default function UserRoleCard({ user }: Props) {
|
|||||||
onClose={() => setOpenDeleteUser(false)}
|
onClose={() => setOpenDeleteUser(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showConfirmModal && (
|
||||||
|
<Modal
|
||||||
|
open={showConfirmModal}
|
||||||
|
onClose={() => setShowConfirmModal(false)}
|
||||||
|
title="Reset Password"
|
||||||
|
>
|
||||||
|
{newPassword && (
|
||||||
|
<div className="reset-password__content">
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<code>{newPassword}</code>
|
||||||
|
</Typography>
|
||||||
|
<Tooltip title="Copy to clipboard">
|
||||||
|
<IconButton
|
||||||
|
aria-label="Copy temporary password"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(newPassword);
|
||||||
|
toast.success("Copied to clipboard");
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to copy");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<ContentCopy fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!newPassword && (
|
||||||
|
<Confirm
|
||||||
|
onClose={() => setShowConfirmModal(false)}
|
||||||
|
onSubmit={handleResetPasswordSubmit}
|
||||||
|
message="Are you sure you want to reset the password for this user?"
|
||||||
|
confirmLabel="Reset Password"
|
||||||
|
cancelLabel="Cancel"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 ----------------
|
// ---------------- Unified Validate Auth ----------------
|
||||||
export const validateAuth = createAsyncThunk<
|
export const validateAuth = createAsyncThunk<
|
||||||
{ tokenInfo: TokenInfo | null },
|
{ tokenInfo: TokenInfo | null },
|
||||||
@ -327,6 +365,23 @@ const authSlice = createSlice({
|
|||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
state.authMessage = action.payload as string;
|
state.authMessage = action.payload as string;
|
||||||
state.addedUser = null;
|
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");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user