Skip to content

Commit 82a1a86

Browse files
committed
Log in as user support
1 parent 95957a1 commit 82a1a86

File tree

11 files changed

+206
-107
lines changed

11 files changed

+206
-107
lines changed

frontend/src/api/backend/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export * from "./getToken";
3737
export * from "./getUser";
3838
export * from "./getUsers";
3939
export * from "./helpers";
40+
export * from "./loginAsUser";
4041
export * from "./models";
4142
export * from "./refreshToken";
4243
export * from "./renewCertificate";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as api from "./base";
2+
import type { LoginAsTokenResponse } from "./responseTypes";
3+
4+
export async function loginAsUser(id: number): Promise<LoginAsTokenResponse> {
5+
return await api.post({
6+
url: `/users/${id}/login`,
7+
});
8+
}

frontend/src/api/backend/responseTypes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AppVersion } from "./models";
1+
import type { AppVersion, User } from "./models";
22

33
export interface HealthResponse {
44
status: string;
@@ -15,3 +15,7 @@ export interface ValidatedCertificateResponse {
1515
certificate: Record<string, any>;
1616
certificateKey: boolean;
1717
}
18+
19+
export interface LoginAsTokenResponse extends TokenResponse {
20+
user: User;
21+
}

frontend/src/components/SiteHeader.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IconLock, IconLogout, IconUser } from "@tabler/icons-react";
2-
import { LocalePicker, ThemeSwitcher, NavLink } from "src/components";
2+
import { LocalePicker, NavLink, ThemeSwitcher } from "src/components";
33
import { useAuthState } from "src/context";
44
import { useUser } from "src/hooks";
55
import { T } from "src/locale";
@@ -26,18 +26,18 @@ export function SiteHeader() {
2626
<span className="navbar-toggler-icon" />
2727
</button>
2828
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
29-
<NavLink to="/">
30-
<div className={styles.logo}>
31-
<img
32-
src="/images/logo-no-text.svg"
33-
width={40}
34-
height={40}
35-
className="navbar-brand-image"
36-
alt="Logo"
37-
/>
38-
</div>
39-
Nginx Proxy Manager
40-
</NavLink>
29+
<NavLink to="/">
30+
<div className={styles.logo}>
31+
<img
32+
src="/images/logo-no-text.svg"
33+
width={40}
34+
height={40}
35+
className="navbar-brand-image"
36+
alt="Logo"
37+
/>
38+
</div>
39+
Nginx Proxy Manager
40+
</NavLink>
4141
</div>
4242
<div className="navbar-nav flex-row order-md-last">
4343
<div className="d-none d-md-flex">

frontend/src/context/AuthContext.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { useQueryClient } from "@tanstack/react-query";
22
import { createContext, type ReactNode, useContext, useState } from "react";
33
import { useIntervalWhen } from "rooks";
4-
import { getToken, refreshToken, type TokenResponse } from "src/api/backend";
4+
import { getToken, loginAsUser, refreshToken, type TokenResponse } from "src/api/backend";
55
import AuthStore from "src/modules/AuthStore";
66

77
// Context
88
export interface AuthContextType {
99
authenticated: boolean;
1010
login: (username: string, password: string) => Promise<void>;
11+
loginAs: (id: number) => Promise<void>;
1112
logout: () => void;
1213
token?: string;
1314
}
@@ -34,7 +35,20 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props)
3435
handleTokenUpdate(response);
3536
};
3637

38+
const loginAs = async (id: number) => {
39+
const response = await loginAsUser(id);
40+
AuthStore.add(response);
41+
queryClient.clear();
42+
window.location.reload();
43+
};
44+
3745
const logout = () => {
46+
if (AuthStore.count() >= 2) {
47+
AuthStore.drop();
48+
queryClient.clear();
49+
window.location.reload();
50+
return;
51+
}
3852
AuthStore.clear();
3953
setAuthenticated(false);
4054
queryClient.clear();
@@ -55,7 +69,7 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props)
5569
true,
5670
);
5771

58-
const value = { authenticated, login, logout };
72+
const value = { authenticated, login, logout, loginAs };
5973

6074
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
6175
}

frontend/src/locale/lang/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@
201201
"user.current-password": "Current Password",
202202
"user.edit-profile": "Edit Profile",
203203
"user.full-name": "Full Name",
204+
"user.login-as": "Sign in as {name}",
204205
"user.logout": "Logout",
205206
"user.new-password": "New Password",
206207
"user.nickname": "Nickname",

frontend/src/locale/src/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,9 @@
605605
"user.full-name": {
606606
"defaultMessage": "Full Name"
607607
},
608+
"user.login-as": {
609+
"defaultMessage": "Sign in as {name}"
610+
},
608611
"user.logout": {
609612
"defaultMessage": "Logout"
610613
},

frontend/src/modules/AuthStore.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export class AuthStore {
4444
// const t = this.tokens;
4545
// return t.length > 0;
4646
// }
47+
// Start from the END of the stack and work backwards
4748
hasActiveToken() {
4849
const t = this.tokens;
4950
if (!t.length) {
@@ -68,22 +69,27 @@ export class AuthStore {
6869
localStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }]));
6970
}
7071

71-
// Add a token to the stack
72+
// Add a token to the END of the stack
7273
add({ token, expires }: TokenResponse) {
7374
const t = this.tokens;
7475
t.push({ token, expires });
7576
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
7677
}
7778

78-
// Drop a token from the stack
79+
// Drop a token from the END of the stack
7980
drop() {
8081
const t = this.tokens;
81-
localStorage.setItem(TOKEN_KEY, JSON.stringify(t.splice(-1, 1)));
82+
t.splice(-1, 1);
83+
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
8284
}
8385

8486
clear() {
8587
localStorage.removeItem(TOKEN_KEY);
8688
}
89+
90+
count() {
91+
return this.tokens.length;
92+
}
8793
}
8894

8995
export default new AuthStore();

frontend/src/pages/Dashboard/index.tsx

Lines changed: 97 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from "@tabler/icons-react";
22
import { useNavigate } from "react-router-dom";
3+
import { HasPermission } from "src/components";
34
import { useHostReport } from "src/hooks";
45
import { T } from "src/locale";
56

@@ -15,100 +16,111 @@ const Dashboard = () => {
1516
<div className="row row-deck row-cards">
1617
<div className="col-12 my-4">
1718
<div className="row row-cards">
18-
<div className="col-sm-6 col-lg-3">
19-
<a
20-
href="/nginx/proxy"
21-
className="card card-sm card-link card-link-pop"
22-
onClick={(e) => {
23-
e.preventDefault();
24-
navigate("/nginx/proxy");
25-
}}
26-
>
27-
<div className="card-body">
28-
<div className="row align-items-center">
29-
<div className="col-auto">
30-
<span className="bg-green text-white avatar">
31-
<IconBolt />
32-
</span>
33-
</div>
34-
<div className="col">
35-
<div className="font-weight-medium">
36-
<T id="proxy-hosts.count" data={{ count: hostReport?.proxy }} />
19+
<HasPermission permission="proxyHosts" type="view" hideError>
20+
<div className="col-sm-6 col-lg-3">
21+
<a
22+
href="/nginx/proxy"
23+
className="card card-sm card-link card-link-pop"
24+
onClick={(e) => {
25+
e.preventDefault();
26+
navigate("/nginx/proxy");
27+
}}
28+
>
29+
<div className="card-body">
30+
<div className="row align-items-center">
31+
<div className="col-auto">
32+
<span className="bg-green text-white avatar">
33+
<IconBolt />
34+
</span>
35+
</div>
36+
<div className="col">
37+
<div className="font-weight-medium">
38+
<T id="proxy-hosts.count" data={{ count: hostReport?.proxy }} />
39+
</div>
3740
</div>
3841
</div>
3942
</div>
40-
</div>
41-
</a>
42-
</div>
43-
<div className="col-sm-6 col-lg-3">
44-
<a
45-
href="/nginx/redirection"
46-
className="card card-sm card-link card-link-pop"
47-
onClick={(e) => {
48-
e.preventDefault();
49-
navigate("/nginx/redirection");
50-
}}
51-
>
52-
<div className="card-body">
53-
<div className="row align-items-center">
54-
<div className="col-auto">
55-
<span className="bg-yellow text-white avatar">
56-
<IconArrowsCross />
57-
</span>
58-
</div>
59-
<div className="col">
60-
<T id="redirection-hosts.count" data={{ count: hostReport?.redirection }} />
43+
</a>
44+
</div>
45+
</HasPermission>
46+
<HasPermission permission="redirectionHosts" type="view" hideError>
47+
<div className="col-sm-6 col-lg-3">
48+
<a
49+
href="/nginx/redirection"
50+
className="card card-sm card-link card-link-pop"
51+
onClick={(e) => {
52+
e.preventDefault();
53+
navigate("/nginx/redirection");
54+
}}
55+
>
56+
<div className="card-body">
57+
<div className="row align-items-center">
58+
<div className="col-auto">
59+
<span className="bg-yellow text-white avatar">
60+
<IconArrowsCross />
61+
</span>
62+
</div>
63+
<div className="col">
64+
<T
65+
id="redirection-hosts.count"
66+
data={{ count: hostReport?.redirection }}
67+
/>
68+
</div>
6169
</div>
6270
</div>
63-
</div>
64-
</a>
65-
</div>
66-
<div className="col-sm-6 col-lg-3">
67-
<a
68-
href="/nginx/stream"
69-
className="card card-sm card-link card-link-pop"
70-
onClick={(e) => {
71-
e.preventDefault();
72-
navigate("/nginx/stream");
73-
}}
74-
>
75-
<div className="card-body">
76-
<div className="row align-items-center">
77-
<div className="col-auto">
78-
<span className="bg-blue text-white avatar">
79-
<IconDisc />
80-
</span>
81-
</div>
82-
<div className="col">
83-
<T id="streams.count" data={{ count: hostReport?.stream }} />
71+
</a>
72+
</div>
73+
</HasPermission>
74+
<HasPermission permission="streams" type="view" hideError>
75+
<div className="col-sm-6 col-lg-3">
76+
<a
77+
href="/nginx/stream"
78+
className="card card-sm card-link card-link-pop"
79+
onClick={(e) => {
80+
e.preventDefault();
81+
navigate("/nginx/stream");
82+
}}
83+
>
84+
<div className="card-body">
85+
<div className="row align-items-center">
86+
<div className="col-auto">
87+
<span className="bg-blue text-white avatar">
88+
<IconDisc />
89+
</span>
90+
</div>
91+
<div className="col">
92+
<T id="streams.count" data={{ count: hostReport?.stream }} />
93+
</div>
8494
</div>
8595
</div>
86-
</div>
87-
</a>
88-
</div>
89-
<div className="col-sm-6 col-lg-3">
90-
<a
91-
href="/nginx/404"
92-
className="card card-sm card-link card-link-pop"
93-
onClick={(e) => {
94-
e.preventDefault();
95-
navigate("/nginx/404");
96-
}}
97-
>
98-
<div className="card-body">
99-
<div className="row align-items-center">
100-
<div className="col-auto">
101-
<span className="bg-red text-white avatar">
102-
<IconBoltOff />
103-
</span>
104-
</div>
105-
<div className="col">
106-
<T id="dead-hosts.count" data={{ count: hostReport?.dead }} />
96+
</a>
97+
</div>
98+
</HasPermission>
99+
<HasPermission permission="deadHosts" type="view" hideError>
100+
<div className="col-sm-6 col-lg-3">
101+
<a
102+
href="/nginx/404"
103+
className="card card-sm card-link card-link-pop"
104+
onClick={(e) => {
105+
e.preventDefault();
106+
navigate("/nginx/404");
107+
}}
108+
>
109+
<div className="card-body">
110+
<div className="row align-items-center">
111+
<div className="col-auto">
112+
<span className="bg-red text-white avatar">
113+
<IconBoltOff />
114+
</span>
115+
</div>
116+
<div className="col">
117+
<T id="dead-hosts.count" data={{ count: hostReport?.dead }} />
118+
</div>
107119
</div>
108120
</div>
109-
</div>
110-
</a>
111-
</div>
121+
</a>
122+
</div>
123+
</HasPermission>
112124
</div>
113125
</div>
114126
</div>

0 commit comments

Comments
 (0)