mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-27 19:20:04 +00:00
Version 3 starter
This commit is contained in:
11
frontend/src/App.test.tsx
Normal file
11
frontend/src/App.test.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as React from "react";
|
||||
|
||||
import * as ReactDOM from "react-dom";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
const div = document.createElement("div");
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
16
frontend/src/App.tsx
Normal file
16
frontend/src/App.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
import Router from "components/Router";
|
||||
import { AuthProvider, HealthProvider } from "context";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<HealthProvider>
|
||||
<AuthProvider>
|
||||
<Router />
|
||||
</AuthProvider>
|
||||
</HealthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
89
frontend/src/api/npm/base.ts
Normal file
89
frontend/src/api/npm/base.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { camelizeKeys, decamelizeKeys } from "humps";
|
||||
import AuthStore from "modules/AuthStore";
|
||||
import * as queryString from "query-string";
|
||||
|
||||
interface BuildUrlArgs {
|
||||
url: string;
|
||||
params?: queryString.StringifiableRecord;
|
||||
}
|
||||
|
||||
function buildUrl({ url, params }: BuildUrlArgs) {
|
||||
const endpoint = url.replace(/^\/|\/$/g, "");
|
||||
const apiParams = params ? `?${queryString.stringify(params)}` : "";
|
||||
const apiUrl = `/api/${endpoint}${apiParams}`;
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
function buildAuthHeader(): Record<string, string> | undefined {
|
||||
if (AuthStore.token) {
|
||||
return { Authorization: `Bearer ${AuthStore.token.token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function buildBody(data?: Record<string, any>) {
|
||||
if (data) {
|
||||
return JSON.stringify(decamelizeKeys(data));
|
||||
}
|
||||
}
|
||||
|
||||
async function processResponse(response: Response) {
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error.message);
|
||||
}
|
||||
return camelizeKeys(payload) as any;
|
||||
}
|
||||
|
||||
interface GetArgs {
|
||||
url: string;
|
||||
params?: queryString.StringifiableRecord;
|
||||
}
|
||||
|
||||
export async function get(
|
||||
{ url, params }: GetArgs,
|
||||
abortController?: AbortController,
|
||||
) {
|
||||
const apiUrl = buildUrl({ url, params });
|
||||
const method = "GET";
|
||||
const signal = abortController?.signal;
|
||||
const headers = buildAuthHeader();
|
||||
const response = await fetch(apiUrl, { method, headers, signal });
|
||||
return processResponse(response);
|
||||
}
|
||||
|
||||
interface PostArgs {
|
||||
url: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export async function post(
|
||||
{ url, data }: PostArgs,
|
||||
abortController?: AbortController,
|
||||
) {
|
||||
const apiUrl = buildUrl({ url });
|
||||
const method = "POST";
|
||||
const headers = { ...buildAuthHeader(), "Content-Type": "application/json" };
|
||||
const signal = abortController?.signal;
|
||||
const body = buildBody(data);
|
||||
const response = await fetch(apiUrl, { method, headers, body, signal });
|
||||
return processResponse(response);
|
||||
}
|
||||
|
||||
interface PutArgs {
|
||||
url: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export async function put(
|
||||
{ url, data }: PutArgs,
|
||||
abortController?: AbortController,
|
||||
) {
|
||||
const apiUrl = buildUrl({ url });
|
||||
const method = "PUT";
|
||||
const headers = { ...buildAuthHeader(), "Content-Type": "application/json" };
|
||||
const signal = abortController?.signal;
|
||||
const body = buildBody(data);
|
||||
const response = await fetch(apiUrl, { method, headers, body, signal });
|
||||
return processResponse(response);
|
||||
}
|
32
frontend/src/api/npm/createUser.ts
Normal file
32
frontend/src/api/npm/createUser.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as api from "./base";
|
||||
import { UserResponse } from "./responseTypes";
|
||||
|
||||
interface AuthOptions {
|
||||
type: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
payload: {
|
||||
name: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
isDisabled: boolean;
|
||||
auth: AuthOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
{ payload }: Options,
|
||||
abortController?: AbortController,
|
||||
): Promise<UserResponse> {
|
||||
const { result } = await api.post(
|
||||
{
|
||||
url: "/users",
|
||||
data: payload,
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
24
frontend/src/api/npm/getToken.ts
Normal file
24
frontend/src/api/npm/getToken.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as api from "./base";
|
||||
import { TokenResponse } from "./responseTypes";
|
||||
|
||||
interface Options {
|
||||
payload: {
|
||||
type: string;
|
||||
identity: string;
|
||||
secret: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getToken(
|
||||
{ payload }: Options,
|
||||
abortController?: AbortController,
|
||||
): Promise<TokenResponse> {
|
||||
const { result } = await api.post(
|
||||
{
|
||||
url: "/tokens",
|
||||
data: payload,
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
12
frontend/src/api/npm/getUser.ts
Normal file
12
frontend/src/api/npm/getUser.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as api from "./base";
|
||||
import { UserResponse } from "./responseTypes";
|
||||
|
||||
export async function getUser(
|
||||
id: number | string = "me",
|
||||
): Promise<UserResponse> {
|
||||
const userId = id ? id : "me";
|
||||
const { result } = await api.get({
|
||||
url: `/users/${userId}`,
|
||||
});
|
||||
return result;
|
||||
}
|
6
frontend/src/api/npm/index.ts
Normal file
6
frontend/src/api/npm/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./createUser";
|
||||
export * from "./getToken";
|
||||
export * from "./getUser";
|
||||
export * from "./refreshToken";
|
||||
export * from "./requestHealth";
|
||||
export * from "./responseTypes";
|
14
frontend/src/api/npm/refreshToken.ts
Normal file
14
frontend/src/api/npm/refreshToken.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as api from "./base";
|
||||
import { TokenResponse } from "./responseTypes";
|
||||
|
||||
export async function refreshToken(
|
||||
abortController?: AbortController,
|
||||
): Promise<TokenResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "/tokens",
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
15
frontend/src/api/npm/requestHealth.ts
Normal file
15
frontend/src/api/npm/requestHealth.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as api from "./base";
|
||||
import { HealthResponse } from "./responseTypes";
|
||||
|
||||
// Request function.
|
||||
export async function requestHealth(
|
||||
abortController?: AbortController,
|
||||
): Promise<HealthResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "",
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
33
frontend/src/api/npm/responseTypes.ts
Normal file
33
frontend/src/api/npm/responseTypes.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export interface HealthResponse {
|
||||
commit: string;
|
||||
errorReporting: boolean;
|
||||
healthy: boolean;
|
||||
setup: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface UserAuthResponse {
|
||||
id: number;
|
||||
userId: number;
|
||||
type: string;
|
||||
createdOn: number;
|
||||
updatedOn: number;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
expires: number;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
createdOn: number;
|
||||
updatedOn: number;
|
||||
roles: string[];
|
||||
gravatarUrl: string;
|
||||
isDisabled: boolean;
|
||||
auth?: UserAuthResponse;
|
||||
}
|
56
frontend/src/components/Footer.tsx
Normal file
56
frontend/src/components/Footer.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
|
||||
import { useHealthState } from "context";
|
||||
import styled from "styled-components";
|
||||
import { Site } from "tabler-react";
|
||||
|
||||
const FixedFooterWrapper = styled.div`
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
fixed?: boolean;
|
||||
}
|
||||
function Footer({ fixed }: Props) {
|
||||
const { health } = useHealthState();
|
||||
|
||||
const footerNav = (
|
||||
<div>
|
||||
<a
|
||||
href="https://nginxproxymanager.com?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
User Guide
|
||||
</a>{" "}
|
||||
{String.fromCharCode(183)}{" "}
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Changelog
|
||||
</a>{" "}
|
||||
{String.fromCharCode(183)}{" "}
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Github
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const note =
|
||||
"v" + health.version + " " + String.fromCharCode(183) + " " + health.commit;
|
||||
|
||||
return fixed ? (
|
||||
<FixedFooterWrapper>
|
||||
<Site.Footer copyright={note} nav={footerNav} />
|
||||
</FixedFooterWrapper>
|
||||
) : (
|
||||
<Site.Footer copyright={note} nav={footerNav} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Footer };
|
18
frontend/src/components/Loading.tsx
Normal file
18
frontend/src/components/Loading.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
import styled from "styled-components";
|
||||
import { Loader } from "tabler-react";
|
||||
|
||||
const Root = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<Root>
|
||||
<Loader />
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Loading };
|
57
frontend/src/components/Router.tsx
Normal file
57
frontend/src/components/Router.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { lazy, Suspense } from "react";
|
||||
|
||||
import { Loading, SiteWrapper, SinglePage } from "components";
|
||||
import { useAuthState, useHealthState, UserProvider } from "context";
|
||||
import { BrowserRouter, Switch, Route } from "react-router-dom";
|
||||
|
||||
const Setup = lazy(() => import("pages/Setup"));
|
||||
const Dashboard = lazy(() => import("pages/Dashboard"));
|
||||
const Login = lazy(() => import("pages/Login"));
|
||||
|
||||
function Router() {
|
||||
const { health } = useHealthState();
|
||||
const { authenticated } = useAuthState();
|
||||
const Spinner = (
|
||||
<SinglePage>
|
||||
<Loading />
|
||||
</SinglePage>
|
||||
);
|
||||
|
||||
if (health.loading) {
|
||||
return Spinner;
|
||||
}
|
||||
|
||||
if (health.healthy && !health.setup) {
|
||||
return (
|
||||
<Suspense fallback={Spinner}>
|
||||
<Setup />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<Suspense fallback={Spinner}>
|
||||
<Login />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<SiteWrapper>
|
||||
<Suspense fallback={Spinner}>
|
||||
<Switch>
|
||||
<Route path="/">
|
||||
<Dashboard />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</SiteWrapper>
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Router;
|
32
frontend/src/components/SinglePage.tsx
Normal file
32
frontend/src/components/SinglePage.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import { Footer } from "components";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Root = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
function SinglePage({ children }: Props) {
|
||||
return (
|
||||
<Root>
|
||||
<Wrapper>{children}</Wrapper>
|
||||
<Footer />
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { SinglePage };
|
193
frontend/src/components/SiteWrapper.tsx
Normal file
193
frontend/src/components/SiteWrapper.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import { Footer } from "components";
|
||||
import { useAuthState, useUserState } from "context";
|
||||
import styled from "styled-components";
|
||||
import { Site, Container, Button, Grid, List } from "tabler-react";
|
||||
|
||||
const StyledContainer = styled(Container)`
|
||||
padding-bottom: 30px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
function SiteWrapper({ children }: Props) {
|
||||
const user = useUserState();
|
||||
const { logout } = useAuthState();
|
||||
|
||||
const accountDropdownProps = {
|
||||
avatarURL: user.gravatarUrl,
|
||||
name: user.nickname,
|
||||
description: user.roles.includes("admin")
|
||||
? "Administrator"
|
||||
: "Standard User",
|
||||
options: [
|
||||
{ icon: "user", value: "Profile" },
|
||||
{ icon: "settings", value: "Settings" },
|
||||
{ isDivider: true },
|
||||
{
|
||||
icon: "help-circle",
|
||||
value: "Need help?",
|
||||
href: "https://nginxproxymanager.com",
|
||||
target: "_blank",
|
||||
},
|
||||
{ icon: "log-out", value: "Log out", onClick: logout },
|
||||
],
|
||||
};
|
||||
|
||||
const navBarItems = [
|
||||
{
|
||||
value: "Home",
|
||||
to: "/",
|
||||
icon: "home",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
useExact: true,
|
||||
},
|
||||
{
|
||||
value: "Interface",
|
||||
icon: "box",
|
||||
subItems: [
|
||||
{
|
||||
value: "Cards Design",
|
||||
to: "/cards",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
//{ value: "Charts", to: "/charts", LinkComponent: withRouter(NavLink) },
|
||||
{
|
||||
value: "Pricing Cards",
|
||||
to: "/pricing-cards",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "Components",
|
||||
icon: "calendar",
|
||||
/*
|
||||
subItems: [
|
||||
{ value: "Maps", to: "/maps", LinkComponent: withRouter(NavLink) },
|
||||
{ value: "Icons", to: "/icons", LinkComponent: withRouter(NavLink) },
|
||||
{ value: "Store", to: "/store", LinkComponent: withRouter(NavLink) },
|
||||
{ value: "Blog", to: "/blog", LinkComponent: withRouter(NavLink) },
|
||||
],
|
||||
*/
|
||||
},
|
||||
{
|
||||
value: "Pages",
|
||||
icon: "file",
|
||||
subItems: [
|
||||
{
|
||||
value: "Profile",
|
||||
to: "/profile",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
//{ value: "Login", to: "/login", LinkComponent: withRouter(NavLink) },
|
||||
{
|
||||
value: "Register",
|
||||
to: "/register",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
{
|
||||
value: "Forgot password",
|
||||
to: "/forgot-password",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
{
|
||||
value: "Empty page",
|
||||
to: "/empty-page",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
//{ value: "RTL", to: "/rtl", LinkComponent: withRouter(NavLink) },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "Forms",
|
||||
to: "/form-elements",
|
||||
icon: "check-square",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
{
|
||||
value: "Gallery",
|
||||
to: "/gallery",
|
||||
icon: "image",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
{
|
||||
icon: "file-text",
|
||||
value: "Documentation",
|
||||
to:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "https://tabler.github.io/tabler-react/documentation"
|
||||
: "/documentation",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Site.Wrapper
|
||||
headerProps={{
|
||||
href: "/",
|
||||
alt: "Nginx Proxy Manager",
|
||||
imageURL: "/images/logo-bold-horizontal-grey.svg",
|
||||
accountDropdown: accountDropdownProps,
|
||||
}}
|
||||
navProps={{ itemsObjects: navBarItems }}
|
||||
//routerContextComponentType={withRouter(RouterContextProvider)}
|
||||
footerProps={{
|
||||
links: [
|
||||
<a href="#asd">First Link</a>,
|
||||
<a href="#fg">Second Link</a>,
|
||||
<a href="#dsg">Third Link</a>,
|
||||
<a href="#egf">Fourth Link</a>,
|
||||
<a href="#dsf">Five Link</a>,
|
||||
<a href="#sdfg">Sixth Link</a>,
|
||||
<a href="#sdf">Seventh Link</a>,
|
||||
<a href="#sdf">Eigth Link</a>,
|
||||
],
|
||||
note: "Premium and Open Source dashboard template with responsive and high quality UI. For Free!",
|
||||
copyright: (
|
||||
<React.Fragment>
|
||||
Copyright © 2019
|
||||
<a href="."> Tabler-react</a>. Theme by
|
||||
<a
|
||||
href="https://codecalm.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{" "}
|
||||
codecalm.net
|
||||
</a>{" "}
|
||||
All rights reserved.
|
||||
</React.Fragment>
|
||||
),
|
||||
nav: (
|
||||
<React.Fragment>
|
||||
<Grid.Col auto={true}>
|
||||
<List className="list-inline list-inline-dots mb-0">
|
||||
<List.Item className="list-inline-item">
|
||||
<a href="./docs/index.html">Documentation</a>
|
||||
</List.Item>
|
||||
<List.Item className="list-inline-item">
|
||||
<a href="./faq.html">FAQ</a>
|
||||
</List.Item>
|
||||
</List>
|
||||
</Grid.Col>
|
||||
<Grid.Col auto={true}>
|
||||
<Button
|
||||
href="https://github.com/tabler/tabler-react"
|
||||
size="sm"
|
||||
outline
|
||||
color="primary"
|
||||
RootComponent="a">
|
||||
Source code
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
</React.Fragment>
|
||||
),
|
||||
}}>
|
||||
<StyledContainer>{children}</StyledContainer>
|
||||
<Footer fixed />
|
||||
</Site.Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { SiteWrapper };
|
45
frontend/src/components/Unhealthy.tsx
Normal file
45
frontend/src/components/Unhealthy.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
|
||||
import styled from "styled-components";
|
||||
import { Alert } from "tabler-react";
|
||||
|
||||
const Root = styled.div`
|
||||
padding: 20vh 10vw 0 10vw;
|
||||
|
||||
&& .ant-alert-warning {
|
||||
background-color: #2a2a2a;
|
||||
border: 2px solid #2ab1a4;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
&& .ant-alert-message {
|
||||
color: #fff;
|
||||
font-size: 6vh;
|
||||
}
|
||||
|
||||
&& .ant-alert-description {
|
||||
font-size: 4vh;
|
||||
line-height: 5vh;
|
||||
}
|
||||
|
||||
&& .ant-alert-with-description {
|
||||
padding-left: 23vh;
|
||||
}
|
||||
|
||||
&& .ant-alert-with-description .ant-alert-icon {
|
||||
font-size: 15vh;
|
||||
}
|
||||
`;
|
||||
|
||||
function Unhealthy() {
|
||||
return (
|
||||
<Root>
|
||||
<Alert type="warning" icon="alert-triangle">
|
||||
Nginx Proxy Manager is <strong>unhealthy</strong>. We'll continue to
|
||||
check the health and hope to be back up and running soon!
|
||||
</Alert>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Unhealthy };
|
6
frontend/src/components/index.ts
Normal file
6
frontend/src/components/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./Footer";
|
||||
export * from "./Loading";
|
||||
export * from "./Router";
|
||||
export * from "./SinglePage";
|
||||
export * from "./SiteWrapper";
|
||||
export * from "./Unhealthy";
|
56
frontend/src/components/page/Footer.tsx
Normal file
56
frontend/src/components/page/Footer.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
|
||||
import { useHealthState } from "context";
|
||||
import styled from "styled-components";
|
||||
import { Site } from "tabler-react";
|
||||
|
||||
const FixedFooterWrapper = styled.div`
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
fixed?: boolean;
|
||||
}
|
||||
function Footer({ fixed }: Props) {
|
||||
const { health } = useHealthState();
|
||||
|
||||
const footerNav = (
|
||||
<div>
|
||||
<a
|
||||
href="https://nginxproxymanager.com?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
User Guide
|
||||
</a>{" "}
|
||||
{String.fromCharCode(183)}{" "}
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Changelog
|
||||
</a>{" "}
|
||||
{String.fromCharCode(183)}{" "}
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Github
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const note =
|
||||
"v" + health.version + " " + String.fromCharCode(183) + " " + health.commit;
|
||||
|
||||
return fixed ? (
|
||||
<FixedFooterWrapper>
|
||||
<Site.Footer copyright={note} nav={footerNav} />
|
||||
</FixedFooterWrapper>
|
||||
) : (
|
||||
<Site.Footer copyright={note} nav={footerNav} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Footer };
|
130
frontend/src/components/page/Header.tsx
Normal file
130
frontend/src/components/page/Header.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from "react";
|
||||
|
||||
import { useAuthState, useUserState } from "context";
|
||||
import { Site } from "tabler-react";
|
||||
|
||||
function Header() {
|
||||
const user = useUserState();
|
||||
const { logout } = useAuthState();
|
||||
|
||||
const accountDropdownProps = {
|
||||
avatarURL: user.gravatarUrl,
|
||||
name: user.nickname,
|
||||
description: user.roles.includes("admin")
|
||||
? "Administrator"
|
||||
: "Standard User",
|
||||
options: [
|
||||
{ icon: "user", value: "Profile" },
|
||||
{ icon: "settings", value: "Settings" },
|
||||
{ isDivider: true },
|
||||
{
|
||||
icon: "help-circle",
|
||||
value: "Need help?",
|
||||
href: "https://nginxproxymanager.com",
|
||||
target: "_blank",
|
||||
},
|
||||
{ icon: "log-out", value: "Log out", onClick: logout },
|
||||
],
|
||||
};
|
||||
|
||||
const navBarItems = [
|
||||
{
|
||||
value: "Home",
|
||||
to: "/",
|
||||
icon: "home",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
useExact: true,
|
||||
},
|
||||
{
|
||||
value: "Interface",
|
||||
icon: "box",
|
||||
subItems: [
|
||||
{
|
||||
value: "Cards Design",
|
||||
to: "/cards",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
//{ value: "Charts", to: "/charts", LinkComponent: withRouter(NavLink) },
|
||||
{
|
||||
value: "Pricing Cards",
|
||||
to: "/pricing-cards",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "Components",
|
||||
icon: "calendar",
|
||||
/*
|
||||
subItems: [
|
||||
{ value: "Maps", to: "/maps", LinkComponent: withRouter(NavLink) },
|
||||
{ value: "Icons", to: "/icons", LinkComponent: withRouter(NavLink) },
|
||||
{ value: "Store", to: "/store", LinkComponent: withRouter(NavLink) },
|
||||
{ value: "Blog", to: "/blog", LinkComponent: withRouter(NavLink) },
|
||||
],
|
||||
*/
|
||||
},
|
||||
{
|
||||
value: "Pages",
|
||||
icon: "file",
|
||||
subItems: [
|
||||
{
|
||||
value: "Profile",
|
||||
to: "/profile",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
//{ value: "Login", to: "/login", LinkComponent: withRouter(NavLink) },
|
||||
{
|
||||
value: "Register",
|
||||
to: "/register",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
{
|
||||
value: "Forgot password",
|
||||
to: "/forgot-password",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
{
|
||||
value: "Empty page",
|
||||
to: "/empty-page",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
//{ value: "RTL", to: "/rtl", LinkComponent: withRouter(NavLink) },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "Forms",
|
||||
to: "/form-elements",
|
||||
icon: "check-square",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
{
|
||||
value: "Gallery",
|
||||
to: "/gallery",
|
||||
icon: "image",
|
||||
//LinkComponent: withRouter(NavLink),
|
||||
},
|
||||
{
|
||||
icon: "file-text",
|
||||
value: "Documentation",
|
||||
to:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "https://tabler.github.io/tabler-react/documentation"
|
||||
: "/documentation",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Site.Header
|
||||
href="/"
|
||||
alt="Nginx Proxy Manager"
|
||||
imageURL="/images/logo-bold-horizontal-grey.svg"
|
||||
accountDropdown={accountDropdownProps}
|
||||
/>
|
||||
<Site.Nav itemsObjects={navBarItems} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { Header };
|
2
frontend/src/components/page/index.ts
Normal file
2
frontend/src/components/page/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./Footer";
|
||||
export * from "./Header";
|
76
frontend/src/context/AuthContext.tsx
Normal file
76
frontend/src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
import { getToken, refreshToken, TokenResponse } from "api/npm";
|
||||
import AuthStore from "modules/AuthStore";
|
||||
import { useInterval } from "rooks";
|
||||
|
||||
// Context
|
||||
export interface AuthContextType {
|
||||
authenticated: boolean;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const initalValue = null;
|
||||
const AuthContext = React.createContext<AuthContextType | null>(initalValue);
|
||||
|
||||
// Provider
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
tokenRefreshInterval?: number;
|
||||
}
|
||||
function AuthProvider({
|
||||
children,
|
||||
tokenRefreshInterval = 5 * 60 * 1000,
|
||||
}: Props) {
|
||||
const [authenticated, setAuthenticated] = useState(
|
||||
AuthStore.hasActiveToken(),
|
||||
);
|
||||
|
||||
const handleTokenUpdate = (response: TokenResponse) => {
|
||||
AuthStore.set(response);
|
||||
setAuthenticated(true);
|
||||
};
|
||||
|
||||
const login = async (identity: string, secret: string) => {
|
||||
const type = "password";
|
||||
const response = await getToken({ payload: { type, identity, secret } });
|
||||
handleTokenUpdate(response);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
AuthStore.clear();
|
||||
setAuthenticated(false);
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
const response = await refreshToken();
|
||||
handleTokenUpdate(response);
|
||||
};
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
if (authenticated) {
|
||||
refresh();
|
||||
}
|
||||
},
|
||||
tokenRefreshInterval,
|
||||
true,
|
||||
);
|
||||
|
||||
const value = { authenticated, login, logout };
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
function useAuthState() {
|
||||
const context = React.useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error(`useAuthState must be used within a AuthProvider`);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { AuthProvider, useAuthState };
|
||||
export default AuthContext;
|
82
frontend/src/context/HealthContext.tsx
Normal file
82
frontend/src/context/HealthContext.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { ReactNode, useState, useEffect, useCallback } from "react";
|
||||
|
||||
import { HealthResponse, requestHealth } from "api/npm";
|
||||
import { Unhealthy } from "components";
|
||||
import { useInterval } from "rooks";
|
||||
|
||||
interface HealthResponseLoaded extends HealthResponse {
|
||||
loading: boolean;
|
||||
}
|
||||
export interface HealthContextType {
|
||||
health: HealthResponseLoaded;
|
||||
refreshHealth: () => void;
|
||||
}
|
||||
|
||||
const initalValue = null;
|
||||
const HealthContext =
|
||||
React.createContext<HealthContextType | null>(initalValue);
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
function HealthProvider({ children }: Props) {
|
||||
const [health, setHealth] = useState({
|
||||
loading: true,
|
||||
commit: "",
|
||||
healthy: false,
|
||||
setup: false,
|
||||
errorReporting: true,
|
||||
version: "",
|
||||
});
|
||||
|
||||
const handleHealthUpdate = (response: HealthResponse) => {
|
||||
setHealth({ ...response, loading: false });
|
||||
};
|
||||
|
||||
const refreshHealth = async () => {
|
||||
const response = await requestHealth();
|
||||
handleHealthUpdate(response);
|
||||
};
|
||||
|
||||
const asyncFetch = useCallback(async () => {
|
||||
try {
|
||||
const response = await requestHealth();
|
||||
handleHealthUpdate(response);
|
||||
if (response.healthy) {
|
||||
if (!health.loading && health.commit !== response.commit) {
|
||||
// New backend version detected, let's reload the entire window
|
||||
window.location.assign(`?hash=${response.commit}`);
|
||||
}
|
||||
}
|
||||
} catch ({ message }) {
|
||||
console.error("Health failed:", message);
|
||||
}
|
||||
}, [health.commit, health.loading]);
|
||||
|
||||
useEffect(() => {
|
||||
asyncFetch();
|
||||
}, [asyncFetch]);
|
||||
|
||||
useInterval(asyncFetch, 30 * 1000, true);
|
||||
|
||||
if (!health.loading && !health.healthy) {
|
||||
return <Unhealthy />;
|
||||
}
|
||||
|
||||
const value = { health, refreshHealth };
|
||||
|
||||
return (
|
||||
<HealthContext.Provider value={value}>{children}</HealthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useHealthState() {
|
||||
const context = React.useContext(HealthContext);
|
||||
if (!context) {
|
||||
throw new Error(`useHealthState must be used within a HealthProvider`);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { HealthProvider, useHealthState };
|
||||
export default HealthContext;
|
54
frontend/src/context/UserContext.tsx
Normal file
54
frontend/src/context/UserContext.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { getUser, UserResponse } from "api/npm";
|
||||
import { useAuthState } from "context";
|
||||
|
||||
// Context
|
||||
const initalValue = null;
|
||||
const UserContext = React.createContext<UserResponse | null>(initalValue);
|
||||
|
||||
// Provider
|
||||
interface Props {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
function UserProvider({ children }: Props) {
|
||||
const [userData, setUserData] = useState<UserResponse>({
|
||||
id: 0,
|
||||
name: "",
|
||||
nickname: "",
|
||||
email: "",
|
||||
createdOn: 0,
|
||||
updatedOn: 0,
|
||||
roles: [],
|
||||
gravatarUrl: "",
|
||||
isDisabled: false,
|
||||
});
|
||||
const { authenticated } = useAuthState();
|
||||
|
||||
const fetchUserData = async () => {
|
||||
const response = await getUser();
|
||||
setUserData({ ...userData, ...response });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (authenticated) {
|
||||
fetchUserData();
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, [authenticated]);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={userData}>{children}</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useUserState() {
|
||||
const context = React.useContext(UserContext);
|
||||
if (!context) {
|
||||
throw new Error(`useUserState must be used within a UserProvider`);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { UserProvider, useUserState };
|
||||
export default UserContext;
|
3
frontend/src/context/index.ts
Normal file
3
frontend/src/context/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./AuthContext";
|
||||
export * from "./HealthContext";
|
||||
export * from "./UserContext";
|
BIN
frontend/src/fonts/feather/feather-webfont.eot
Executable file
BIN
frontend/src/fonts/feather/feather-webfont.eot
Executable file
Binary file not shown.
1038
frontend/src/fonts/feather/feather-webfont.svg
Executable file
1038
frontend/src/fonts/feather/feather-webfont.svg
Executable file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 163 KiB |
BIN
frontend/src/fonts/feather/feather-webfont.ttf
Executable file
BIN
frontend/src/fonts/feather/feather-webfont.ttf
Executable file
Binary file not shown.
BIN
frontend/src/fonts/feather/feather-webfont.woff
Executable file
BIN
frontend/src/fonts/feather/feather-webfont.woff
Executable file
Binary file not shown.
BIN
frontend/src/fonts/tabler-webfont/tabler-webfont.eot
Executable file
BIN
frontend/src/fonts/tabler-webfont/tabler-webfont.eot
Executable file
Binary file not shown.
1044
frontend/src/fonts/tabler-webfont/tabler-webfont.svg
Executable file
1044
frontend/src/fonts/tabler-webfont/tabler-webfont.svg
Executable file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 342 KiB |
BIN
frontend/src/fonts/tabler-webfont/tabler-webfont.ttf
Executable file
BIN
frontend/src/fonts/tabler-webfont/tabler-webfont.ttf
Executable file
Binary file not shown.
BIN
frontend/src/fonts/tabler-webfont/tabler-webfont.woff
Executable file
BIN
frontend/src/fonts/tabler-webfont/tabler-webfont.woff
Executable file
Binary file not shown.
BIN
frontend/src/fonts/tabler-webfont/tabler-webfont.woff2
Executable file
BIN
frontend/src/fonts/tabler-webfont/tabler-webfont.woff2
Executable file
Binary file not shown.
BIN
frontend/src/img/logo-text-vertical-grey.png
Normal file
BIN
frontend/src/img/logo-text-vertical-grey.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
62
frontend/src/index.scss
Normal file
62
frontend/src/index.scss
Normal file
@@ -0,0 +1,62 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f7fb;
|
||||
color: rgb(73, 80, 87);
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.btn-loading {
|
||||
color: transparent !important;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
-webkit-animation: loader 500ms infinite linear;
|
||||
animation: loader 500ms infinite linear;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
border-right-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
display: block;
|
||||
height: 1.4em;
|
||||
width: 1.4em;
|
||||
position: absolute;
|
||||
left: calc(50% - (1.4em / 2));
|
||||
top: calc(50% - (1.4em / 2));
|
||||
-webkit-transform-origin: center;
|
||||
transform-origin: center;
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #fff;
|
||||
border-top: 1px solid rgba(0, 40, 100, 0.12);
|
||||
font-size: 0.875rem;
|
||||
padding: 1.25rem 0;
|
||||
color: #9aa0ac;
|
||||
|
||||
a:not(.btn) {
|
||||
color: #6e7687;
|
||||
text-decoration: none;
|
||||
background-color: initial;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
10
frontend/src/index.tsx
Normal file
10
frontend/src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as React from "react";
|
||||
|
||||
import * as ReactDOM from "react-dom";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
import "./tabler.css";
|
||||
import "./index.scss";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
84
frontend/src/modules/AuthStore.ts
Normal file
84
frontend/src/modules/AuthStore.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { TokenResponse } from "api/npm";
|
||||
|
||||
export const TOKEN_KEY = "authentications";
|
||||
|
||||
export class AuthStore {
|
||||
// Get all tokens from stack
|
||||
get tokens() {
|
||||
const t = localStorage.getItem(TOKEN_KEY);
|
||||
let tokens = [];
|
||||
if (t !== null) {
|
||||
try {
|
||||
tokens = JSON.parse(t);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// Get last token from stack
|
||||
get token() {
|
||||
const t = this.tokens;
|
||||
if (t.length) {
|
||||
return t[t.length - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get expires from last token
|
||||
get expires() {
|
||||
const t = this.token;
|
||||
if (t && typeof t.expires !== "undefined") {
|
||||
const expires = Number(t.expires);
|
||||
if (expires && !isNaN(expires)) {
|
||||
return expires;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter out invalid tokens and return true if we find one that is valid
|
||||
hasActiveToken() {
|
||||
const t = this.tokens;
|
||||
if (!t.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Math.round(new Date().getTime() / 1000);
|
||||
const oneMinuteBuffer = 60;
|
||||
for (let i = t.length - 1; i >= 0; i--) {
|
||||
const valid = t[i].expires - oneMinuteBuffer > now;
|
||||
if (valid) {
|
||||
return true;
|
||||
} else {
|
||||
this.drop();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set a single token on the stack
|
||||
set({ token, expires }: TokenResponse) {
|
||||
localStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }]));
|
||||
}
|
||||
|
||||
// Add a token to the stack
|
||||
add({ token, expires }: TokenResponse) {
|
||||
const t = this.tokens;
|
||||
t.push({ token, expires });
|
||||
localStorage.setItem(TOKEN_KEY, t);
|
||||
}
|
||||
|
||||
// Drop a token from the stack
|
||||
drop() {
|
||||
const t = this.tokens;
|
||||
localStorage.setItem(TOKEN_KEY, t.splice(-1, 1));
|
||||
}
|
||||
|
||||
clear() {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AuthStore();
|
24
frontend/src/pages/Dashboard/index.tsx
Normal file
24
frontend/src/pages/Dashboard/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
|
||||
import { useAuthState } from "context";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Root = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
function Dashboard() {
|
||||
const { logout } = useAuthState();
|
||||
|
||||
return (
|
||||
<Root>
|
||||
Dashboard
|
||||
<div>
|
||||
<button onClick={logout}>Logout</button>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
94
frontend/src/pages/Login/index.tsx
Normal file
94
frontend/src/pages/Login/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState, ChangeEvent } from "react";
|
||||
|
||||
import { SinglePage } from "components";
|
||||
import { useAuthState } from "context";
|
||||
import styled from "styled-components";
|
||||
import { Alert, Button, Container, Form, Card } from "tabler-react";
|
||||
|
||||
import logo from "../../img/logo-text-vertical-grey.png";
|
||||
|
||||
const Wrapper = styled(Container)`
|
||||
margin: 15px auto;
|
||||
max-width: 400px;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const LogoWrapper = styled.div`
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
`;
|
||||
|
||||
function Login() {
|
||||
const { login } = useAuthState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const onSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
await login(formData.email, formData.password);
|
||||
} catch ({ message }) {
|
||||
setErrorMessage(message);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, [target.name]: target.value });
|
||||
};
|
||||
|
||||
const formBody = (
|
||||
<>
|
||||
<Card.Title>Login</Card.Title>
|
||||
<Form method="post" type="card" onSubmit={onSubmit}>
|
||||
{errorMessage ? <Alert type="danger">{errorMessage}</Alert> : null}
|
||||
<Form.Group label="Email Address">
|
||||
<Form.Input
|
||||
onChange={onChange}
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
maxLength={150}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group label="Password">
|
||||
<Form.Input
|
||||
onChange={onChange}
|
||||
name="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
minLength={8}
|
||||
maxLength={100}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button color="cyan" loading={loading} block>
|
||||
Login
|
||||
</Button>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<SinglePage>
|
||||
<Wrapper>
|
||||
<LogoWrapper>
|
||||
<img src={logo} alt="Logo" />
|
||||
</LogoWrapper>
|
||||
<Card body={formBody} />
|
||||
</Wrapper>
|
||||
</SinglePage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
143
frontend/src/pages/Setup/index.tsx
Normal file
143
frontend/src/pages/Setup/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { useState, ChangeEvent } from "react";
|
||||
|
||||
import { createUser } from "api/npm";
|
||||
import { SinglePage } from "components";
|
||||
import { useAuthState, useHealthState } from "context";
|
||||
import styled from "styled-components";
|
||||
import { Alert, Button, Container, Form, Card } from "tabler-react";
|
||||
|
||||
import logo from "../../img/logo-text-vertical-grey.png";
|
||||
|
||||
const Wrapper = styled(Container)`
|
||||
margin: 15px auto;
|
||||
max-width: 400px;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const LogoWrapper = styled.div`
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
`;
|
||||
|
||||
function Setup() {
|
||||
const { refreshHealth } = useHealthState();
|
||||
const { login } = useAuthState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
nickname: "",
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const onSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setErrorMessage("");
|
||||
|
||||
const { password, ...payload } = {
|
||||
...formData,
|
||||
...{
|
||||
isDisabled: false,
|
||||
roles: ["admin"],
|
||||
auth: {
|
||||
type: "password",
|
||||
secret: formData.password,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await createUser({ payload });
|
||||
if (response && typeof response.id !== "undefined" && response.id) {
|
||||
// Success, Login using creds
|
||||
try {
|
||||
await login(response.email, password);
|
||||
// Trigger a Health change
|
||||
refreshHealth();
|
||||
|
||||
// window.location.reload();
|
||||
} catch ({ message }) {
|
||||
setErrorMessage(message);
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
setErrorMessage("Unable to create user!");
|
||||
}
|
||||
} catch ({ message }) {
|
||||
setErrorMessage(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, [target.name]: target.value });
|
||||
};
|
||||
|
||||
const formBody = (
|
||||
<>
|
||||
<Card.Title>Initial Setup</Card.Title>
|
||||
<Form method="post" type="card" onSubmit={onSubmit}>
|
||||
{errorMessage ? <Alert type="danger">{errorMessage}</Alert> : null}
|
||||
<Form.Group label="Full Name">
|
||||
<Form.Input
|
||||
onChange={onChange}
|
||||
name="name"
|
||||
value={formData.name}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group label="Nickname">
|
||||
<Form.Input
|
||||
onChange={onChange}
|
||||
name="nickname"
|
||||
value={formData.nickname}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group label="Email Address">
|
||||
<Form.Input
|
||||
onChange={onChange}
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
maxLength={150}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group label="Password">
|
||||
<Form.Input
|
||||
onChange={onChange}
|
||||
name="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
minLength={8}
|
||||
maxLength={100}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button color="cyan" loading={loading} block>
|
||||
Create Account
|
||||
</Button>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<SinglePage>
|
||||
<Wrapper>
|
||||
<LogoWrapper>
|
||||
<img src={logo} alt="Logo" />
|
||||
</LogoWrapper>
|
||||
<Card body={formBody} />
|
||||
</Wrapper>
|
||||
</SinglePage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Setup;
|
1
frontend/src/react-app-env.d.ts
vendored
Normal file
1
frontend/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
2
frontend/src/tabler.css
Executable file
2
frontend/src/tabler.css
Executable file
@@ -0,0 +1,2 @@
|
||||
@charset "UTF-8";
|
||||
@import url("https://unpkg.com/@tabler/core@1.0.0-beta3/dist/css/tabler.min.css");
|
Reference in New Issue
Block a user