You've already forked nginx-proxy-manager
mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-11-02 16:53:15 +03:00
Log in as user support
This commit is contained in:
@@ -37,6 +37,7 @@ export * from "./getToken";
|
|||||||
export * from "./getUser";
|
export * from "./getUser";
|
||||||
export * from "./getUsers";
|
export * from "./getUsers";
|
||||||
export * from "./helpers";
|
export * from "./helpers";
|
||||||
|
export * from "./loginAsUser";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./refreshToken";
|
export * from "./refreshToken";
|
||||||
export * from "./renewCertificate";
|
export * from "./renewCertificate";
|
||||||
|
|||||||
8
frontend/src/api/backend/loginAsUser.ts
Normal file
8
frontend/src/api/backend/loginAsUser.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as api from "./base";
|
||||||
|
import type { LoginAsTokenResponse } from "./responseTypes";
|
||||||
|
|
||||||
|
export async function loginAsUser(id: number): Promise<LoginAsTokenResponse> {
|
||||||
|
return await api.post({
|
||||||
|
url: `/users/${id}/login`,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AppVersion } from "./models";
|
import type { AppVersion, User } from "./models";
|
||||||
|
|
||||||
export interface HealthResponse {
|
export interface HealthResponse {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -15,3 +15,7 @@ export interface ValidatedCertificateResponse {
|
|||||||
certificate: Record<string, any>;
|
certificate: Record<string, any>;
|
||||||
certificateKey: boolean;
|
certificateKey: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoginAsTokenResponse extends TokenResponse {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IconLock, IconLogout, IconUser } from "@tabler/icons-react";
|
import { IconLock, IconLogout, IconUser } from "@tabler/icons-react";
|
||||||
import { LocalePicker, ThemeSwitcher, NavLink } from "src/components";
|
import { LocalePicker, NavLink, ThemeSwitcher } from "src/components";
|
||||||
import { useAuthState } from "src/context";
|
import { useAuthState } from "src/context";
|
||||||
import { useUser } from "src/hooks";
|
import { useUser } from "src/hooks";
|
||||||
import { T } from "src/locale";
|
import { T } from "src/locale";
|
||||||
@@ -26,18 +26,18 @@ export function SiteHeader() {
|
|||||||
<span className="navbar-toggler-icon" />
|
<span className="navbar-toggler-icon" />
|
||||||
</button>
|
</button>
|
||||||
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||||
<NavLink to="/">
|
<NavLink to="/">
|
||||||
<div className={styles.logo}>
|
<div className={styles.logo}>
|
||||||
<img
|
<img
|
||||||
src="/images/logo-no-text.svg"
|
src="/images/logo-no-text.svg"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
className="navbar-brand-image"
|
className="navbar-brand-image"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
Nginx Proxy Manager
|
Nginx Proxy Manager
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="navbar-nav flex-row order-md-last">
|
<div className="navbar-nav flex-row order-md-last">
|
||||||
<div className="d-none d-md-flex">
|
<div className="d-none d-md-flex">
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { createContext, type ReactNode, useContext, useState } from "react";
|
import { createContext, type ReactNode, useContext, useState } from "react";
|
||||||
import { useIntervalWhen } from "rooks";
|
import { useIntervalWhen } from "rooks";
|
||||||
import { getToken, refreshToken, type TokenResponse } from "src/api/backend";
|
import { getToken, loginAsUser, refreshToken, type TokenResponse } from "src/api/backend";
|
||||||
import AuthStore from "src/modules/AuthStore";
|
import AuthStore from "src/modules/AuthStore";
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
export interface AuthContextType {
|
export interface AuthContextType {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
login: (username: string, password: string) => Promise<void>;
|
login: (username: string, password: string) => Promise<void>;
|
||||||
|
loginAs: (id: number) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
token?: string;
|
token?: string;
|
||||||
}
|
}
|
||||||
@@ -34,7 +35,20 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props)
|
|||||||
handleTokenUpdate(response);
|
handleTokenUpdate(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loginAs = async (id: number) => {
|
||||||
|
const response = await loginAsUser(id);
|
||||||
|
AuthStore.add(response);
|
||||||
|
queryClient.clear();
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
|
if (AuthStore.count() >= 2) {
|
||||||
|
AuthStore.drop();
|
||||||
|
queryClient.clear();
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
AuthStore.clear();
|
AuthStore.clear();
|
||||||
setAuthenticated(false);
|
setAuthenticated(false);
|
||||||
queryClient.clear();
|
queryClient.clear();
|
||||||
@@ -55,7 +69,7 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props)
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = { authenticated, login, logout };
|
const value = { authenticated, login, logout, loginAs };
|
||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,7 @@
|
|||||||
"user.current-password": "Current Password",
|
"user.current-password": "Current Password",
|
||||||
"user.edit-profile": "Edit Profile",
|
"user.edit-profile": "Edit Profile",
|
||||||
"user.full-name": "Full Name",
|
"user.full-name": "Full Name",
|
||||||
|
"user.login-as": "Sign in as {name}",
|
||||||
"user.logout": "Logout",
|
"user.logout": "Logout",
|
||||||
"user.new-password": "New Password",
|
"user.new-password": "New Password",
|
||||||
"user.nickname": "Nickname",
|
"user.nickname": "Nickname",
|
||||||
|
|||||||
@@ -605,6 +605,9 @@
|
|||||||
"user.full-name": {
|
"user.full-name": {
|
||||||
"defaultMessage": "Full Name"
|
"defaultMessage": "Full Name"
|
||||||
},
|
},
|
||||||
|
"user.login-as": {
|
||||||
|
"defaultMessage": "Sign in as {name}"
|
||||||
|
},
|
||||||
"user.logout": {
|
"user.logout": {
|
||||||
"defaultMessage": "Logout"
|
"defaultMessage": "Logout"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export class AuthStore {
|
|||||||
// const t = this.tokens;
|
// const t = this.tokens;
|
||||||
// return t.length > 0;
|
// return t.length > 0;
|
||||||
// }
|
// }
|
||||||
|
// Start from the END of the stack and work backwards
|
||||||
hasActiveToken() {
|
hasActiveToken() {
|
||||||
const t = this.tokens;
|
const t = this.tokens;
|
||||||
if (!t.length) {
|
if (!t.length) {
|
||||||
@@ -68,22 +69,27 @@ export class AuthStore {
|
|||||||
localStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }]));
|
localStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a token to the stack
|
// Add a token to the END of the stack
|
||||||
add({ token, expires }: TokenResponse) {
|
add({ token, expires }: TokenResponse) {
|
||||||
const t = this.tokens;
|
const t = this.tokens;
|
||||||
t.push({ token, expires });
|
t.push({ token, expires });
|
||||||
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
|
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop a token from the stack
|
// Drop a token from the END of the stack
|
||||||
drop() {
|
drop() {
|
||||||
const t = this.tokens;
|
const t = this.tokens;
|
||||||
localStorage.setItem(TOKEN_KEY, JSON.stringify(t.splice(-1, 1)));
|
t.splice(-1, 1);
|
||||||
|
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
count() {
|
||||||
|
return this.tokens.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AuthStore();
|
export default new AuthStore();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from "@tabler/icons-react";
|
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from "@tabler/icons-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { HasPermission } from "src/components";
|
||||||
import { useHostReport } from "src/hooks";
|
import { useHostReport } from "src/hooks";
|
||||||
import { T } from "src/locale";
|
import { T } from "src/locale";
|
||||||
|
|
||||||
@@ -15,100 +16,111 @@ const Dashboard = () => {
|
|||||||
<div className="row row-deck row-cards">
|
<div className="row row-deck row-cards">
|
||||||
<div className="col-12 my-4">
|
<div className="col-12 my-4">
|
||||||
<div className="row row-cards">
|
<div className="row row-cards">
|
||||||
<div className="col-sm-6 col-lg-3">
|
<HasPermission permission="proxyHosts" type="view" hideError>
|
||||||
<a
|
<div className="col-sm-6 col-lg-3">
|
||||||
href="/nginx/proxy"
|
<a
|
||||||
className="card card-sm card-link card-link-pop"
|
href="/nginx/proxy"
|
||||||
onClick={(e) => {
|
className="card card-sm card-link card-link-pop"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
navigate("/nginx/proxy");
|
e.preventDefault();
|
||||||
}}
|
navigate("/nginx/proxy");
|
||||||
>
|
}}
|
||||||
<div className="card-body">
|
>
|
||||||
<div className="row align-items-center">
|
<div className="card-body">
|
||||||
<div className="col-auto">
|
<div className="row align-items-center">
|
||||||
<span className="bg-green text-white avatar">
|
<div className="col-auto">
|
||||||
<IconBolt />
|
<span className="bg-green text-white avatar">
|
||||||
</span>
|
<IconBolt />
|
||||||
</div>
|
</span>
|
||||||
<div className="col">
|
</div>
|
||||||
<div className="font-weight-medium">
|
<div className="col">
|
||||||
<T id="proxy-hosts.count" data={{ count: hostReport?.proxy }} />
|
<div className="font-weight-medium">
|
||||||
|
<T id="proxy-hosts.count" data={{ count: hostReport?.proxy }} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</HasPermission>
|
||||||
<div className="col-sm-6 col-lg-3">
|
<HasPermission permission="redirectionHosts" type="view" hideError>
|
||||||
<a
|
<div className="col-sm-6 col-lg-3">
|
||||||
href="/nginx/redirection"
|
<a
|
||||||
className="card card-sm card-link card-link-pop"
|
href="/nginx/redirection"
|
||||||
onClick={(e) => {
|
className="card card-sm card-link card-link-pop"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
navigate("/nginx/redirection");
|
e.preventDefault();
|
||||||
}}
|
navigate("/nginx/redirection");
|
||||||
>
|
}}
|
||||||
<div className="card-body">
|
>
|
||||||
<div className="row align-items-center">
|
<div className="card-body">
|
||||||
<div className="col-auto">
|
<div className="row align-items-center">
|
||||||
<span className="bg-yellow text-white avatar">
|
<div className="col-auto">
|
||||||
<IconArrowsCross />
|
<span className="bg-yellow text-white avatar">
|
||||||
</span>
|
<IconArrowsCross />
|
||||||
</div>
|
</span>
|
||||||
<div className="col">
|
</div>
|
||||||
<T id="redirection-hosts.count" data={{ count: hostReport?.redirection }} />
|
<div className="col">
|
||||||
|
<T
|
||||||
|
id="redirection-hosts.count"
|
||||||
|
data={{ count: hostReport?.redirection }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</HasPermission>
|
||||||
<div className="col-sm-6 col-lg-3">
|
<HasPermission permission="streams" type="view" hideError>
|
||||||
<a
|
<div className="col-sm-6 col-lg-3">
|
||||||
href="/nginx/stream"
|
<a
|
||||||
className="card card-sm card-link card-link-pop"
|
href="/nginx/stream"
|
||||||
onClick={(e) => {
|
className="card card-sm card-link card-link-pop"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
navigate("/nginx/stream");
|
e.preventDefault();
|
||||||
}}
|
navigate("/nginx/stream");
|
||||||
>
|
}}
|
||||||
<div className="card-body">
|
>
|
||||||
<div className="row align-items-center">
|
<div className="card-body">
|
||||||
<div className="col-auto">
|
<div className="row align-items-center">
|
||||||
<span className="bg-blue text-white avatar">
|
<div className="col-auto">
|
||||||
<IconDisc />
|
<span className="bg-blue text-white avatar">
|
||||||
</span>
|
<IconDisc />
|
||||||
</div>
|
</span>
|
||||||
<div className="col">
|
</div>
|
||||||
<T id="streams.count" data={{ count: hostReport?.stream }} />
|
<div className="col">
|
||||||
|
<T id="streams.count" data={{ count: hostReport?.stream }} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</HasPermission>
|
||||||
<div className="col-sm-6 col-lg-3">
|
<HasPermission permission="deadHosts" type="view" hideError>
|
||||||
<a
|
<div className="col-sm-6 col-lg-3">
|
||||||
href="/nginx/404"
|
<a
|
||||||
className="card card-sm card-link card-link-pop"
|
href="/nginx/404"
|
||||||
onClick={(e) => {
|
className="card card-sm card-link card-link-pop"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
navigate("/nginx/404");
|
e.preventDefault();
|
||||||
}}
|
navigate("/nginx/404");
|
||||||
>
|
}}
|
||||||
<div className="card-body">
|
>
|
||||||
<div className="row align-items-center">
|
<div className="card-body">
|
||||||
<div className="col-auto">
|
<div className="row align-items-center">
|
||||||
<span className="bg-red text-white avatar">
|
<div className="col-auto">
|
||||||
<IconBoltOff />
|
<span className="bg-red text-white avatar">
|
||||||
</span>
|
<IconBoltOff />
|
||||||
</div>
|
</span>
|
||||||
<div className="col">
|
</div>
|
||||||
<T id="dead-hosts.count" data={{ count: hostReport?.dead }} />
|
<div className="col">
|
||||||
|
<T id="dead-hosts.count" data={{ count: hostReport?.dead }} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</HasPermission>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { IconDotsVertical, IconEdit, IconLock, IconPower, IconShield, IconTrash } from "@tabler/icons-react";
|
import {
|
||||||
|
IconDotsVertical,
|
||||||
|
IconEdit,
|
||||||
|
IconLock,
|
||||||
|
IconLogin2,
|
||||||
|
IconPower,
|
||||||
|
IconShield,
|
||||||
|
IconTrash,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import type { User } from "src/api/backend";
|
import type { User } from "src/api/backend";
|
||||||
@@ -24,6 +32,7 @@ interface Props {
|
|||||||
onDeleteUser?: (id: number) => void;
|
onDeleteUser?: (id: number) => void;
|
||||||
onDisableToggle?: (id: number, enabled: boolean) => void;
|
onDisableToggle?: (id: number, enabled: boolean) => void;
|
||||||
onNewUser?: () => void;
|
onNewUser?: () => void;
|
||||||
|
onLoginAs?: (id: number) => void;
|
||||||
}
|
}
|
||||||
export default function Table({
|
export default function Table({
|
||||||
data,
|
data,
|
||||||
@@ -36,6 +45,7 @@ export default function Table({
|
|||||||
onDeleteUser,
|
onDeleteUser,
|
||||||
onDisableToggle,
|
onDisableToggle,
|
||||||
onNewUser,
|
onNewUser,
|
||||||
|
onLoginAs,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const columnHelper = createColumnHelper<User>();
|
const columnHelper = createColumnHelper<User>();
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
@@ -153,6 +163,24 @@ export default function Table({
|
|||||||
<IconPower size={16} />
|
<IconPower size={16} />
|
||||||
<T id={info.row.original.isDisabled ? "action.enable" : "action.disable"} />
|
<T id={info.row.original.isDisabled ? "action.enable" : "action.disable"} />
|
||||||
</a>
|
</a>
|
||||||
|
{info.row.original.isDisabled ? (
|
||||||
|
<div className="dropdown-item text-muted">
|
||||||
|
<IconLogin2 size={16} />
|
||||||
|
<T id="user.login-as" data={{ name: info.row.original.name }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
className="dropdown-item"
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onLoginAs?.(info.row.original.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconLogin2 size={16} />
|
||||||
|
<T id="user.login-as" data={{ name: info.row.original.name }} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<div className="dropdown-divider" />
|
<div className="dropdown-divider" />
|
||||||
<a
|
<a
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
@@ -176,7 +204,16 @@ export default function Table({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[columnHelper, currentUserId, onEditUser, onDisableToggle, onDeleteUser, onEditPermissions, onSetPassword],
|
[
|
||||||
|
columnHelper,
|
||||||
|
currentUserId,
|
||||||
|
onEditUser,
|
||||||
|
onDisableToggle,
|
||||||
|
onDeleteUser,
|
||||||
|
onEditPermissions,
|
||||||
|
onSetPassword,
|
||||||
|
onLoginAs,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableInstance = useReactTable<User>({
|
const tableInstance = useReactTable<User>({
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import { useState } from "react";
|
|||||||
import Alert from "react-bootstrap/Alert";
|
import Alert from "react-bootstrap/Alert";
|
||||||
import { deleteUser, toggleUser } from "src/api/backend";
|
import { deleteUser, toggleUser } from "src/api/backend";
|
||||||
import { Button, LoadingPage } from "src/components";
|
import { Button, LoadingPage } from "src/components";
|
||||||
|
import { useAuthState } from "src/context";
|
||||||
import { useUser, useUsers } from "src/hooks";
|
import { useUser, useUsers } from "src/hooks";
|
||||||
import { T } from "src/locale";
|
import { T } from "src/locale";
|
||||||
import { showDeleteConfirmModal, showPermissionsModal, showSetPasswordModal, showUserModal } from "src/modals";
|
import { showDeleteConfirmModal, showPermissionsModal, showSetPasswordModal, showUserModal } from "src/modals";
|
||||||
import { showObjectSuccess } from "src/notifications";
|
import { showError, showObjectSuccess } from "src/notifications";
|
||||||
import Table from "./Table";
|
import Table from "./Table";
|
||||||
|
|
||||||
export default function TableWrapper() {
|
export default function TableWrapper() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { loginAs } = useAuthState();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
|
const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
|
||||||
const { data: currentUser } = useUser("me");
|
const { data: currentUser } = useUser("me");
|
||||||
@@ -24,6 +26,16 @@ export default function TableWrapper() {
|
|||||||
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
|
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLoginAs = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await loginAs(id);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
showError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
await deleteUser(id);
|
await deleteUser(id);
|
||||||
showObjectSuccess("user", "deleted");
|
showObjectSuccess("user", "deleted");
|
||||||
@@ -103,6 +115,7 @@ export default function TableWrapper() {
|
|||||||
}
|
}
|
||||||
onDisableToggle={handleDisableToggle}
|
onDisableToggle={handleDisableToggle}
|
||||||
onNewUser={() => showUserModal("new")}
|
onNewUser={() => showUserModal("new")}
|
||||||
|
onLoginAs={handleLoginAs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user