You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 18:05:34 +03:00 
			
		
		
		
	Introducing the Setup Wizard for creating the first user
- no longer setup a default - still able to do that with env vars however
This commit is contained in:
		| @@ -13,8 +13,9 @@ import { | ||||
| import { useAuthState } from "src/context"; | ||||
| import { useHealth } from "src/hooks"; | ||||
|  | ||||
| const Dashboard = lazy(() => import("src/pages/Dashboard")); | ||||
| const Setup = lazy(() => import("src/pages/Setup")); | ||||
| const Login = lazy(() => import("src/pages/Login")); | ||||
| const Dashboard = lazy(() => import("src/pages/Dashboard")); | ||||
| const Settings = lazy(() => import("src/pages/Settings")); | ||||
| const Certificates = lazy(() => import("src/pages/Certificates")); | ||||
| const Access = lazy(() => import("src/pages/Access")); | ||||
| @@ -37,6 +38,10 @@ function Router() { | ||||
| 		return <Unhealthy />; | ||||
| 	} | ||||
|  | ||||
| 	if (!health.data?.setup) { | ||||
| 		return <Setup />; | ||||
| 	} | ||||
|  | ||||
| 	if (!authenticated) { | ||||
| 		return ( | ||||
| 			<Suspense fallback={<LoadingPage />}> | ||||
|   | ||||
| @@ -88,15 +88,19 @@ interface PostArgs { | ||||
| 	url: string; | ||||
| 	params?: queryString.StringifiableRecord; | ||||
| 	data?: any; | ||||
| 	noAuth?: boolean; | ||||
| } | ||||
|  | ||||
| export async function post({ url, params, data }: PostArgs, abortController?: AbortController) { | ||||
| export async function post({ url, params, data, noAuth }: PostArgs, abortController?: AbortController) { | ||||
| 	const apiUrl = buildUrl({ url, params }); | ||||
| 	const method = "POST"; | ||||
|  | ||||
| 	let headers = { | ||||
| 		...buildAuthHeader(), | ||||
| 	}; | ||||
| 	let headers: Record<string, string> = {}; | ||||
| 	if (!noAuth) { | ||||
| 		headers = { | ||||
| 			...buildAuthHeader(), | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	let body: string | FormData | undefined; | ||||
| 	// Check if the data is an instance of FormData | ||||
|   | ||||
| @@ -1,12 +1,27 @@ | ||||
| import * as api from "./base"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export async function createUser(item: User, abortController?: AbortController): Promise<User> { | ||||
| export interface AuthOptions { | ||||
| 	type: string; | ||||
| 	secret: string; | ||||
| } | ||||
|  | ||||
| export interface NewUser { | ||||
| 	name: string; | ||||
| 	nickname: string; | ||||
| 	email: string; | ||||
| 	isDisabled?: boolean; | ||||
| 	auth?: AuthOptions; | ||||
| 	roles?: string[]; | ||||
| } | ||||
|  | ||||
| export async function createUser(item: NewUser, noAuth?: boolean, abortController?: AbortController): Promise<User> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/users", | ||||
| 			// todo: only use whitelist of fields for this data | ||||
| 			data: item, | ||||
| 			noAuth, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import type { AppVersion } from "./models"; | ||||
| export interface HealthResponse { | ||||
| 	status: string; | ||||
| 	version: AppVersion; | ||||
| 	setup: boolean; | ||||
| } | ||||
|  | ||||
| export interface TokenResponse { | ||||
|   | ||||
| @@ -72,6 +72,8 @@ | ||||
|   "role.standard-user": "Standard User", | ||||
|   "save": "Save", | ||||
|   "settings.title": "Settings", | ||||
|   "setup.preamble": "Get started by creating your admin account.", | ||||
|   "setup.title": "Welcome!", | ||||
|   "sign-in": "Sign in", | ||||
|   "streams.actions-title": "Stream #{id}", | ||||
|   "streams.add": "Add Stream", | ||||
|   | ||||
| @@ -218,6 +218,12 @@ | ||||
| 	"settings.title": { | ||||
| 		"defaultMessage": "Settings" | ||||
| 	}, | ||||
| 	"setup.preamble": { | ||||
| 		"defaultMessage": "Get started by creating your admin account." | ||||
| 	}, | ||||
| 	"setup.title": { | ||||
| 		"defaultMessage": "Welcome!" | ||||
| 	}, | ||||
| 	"sign-in": { | ||||
| 		"defaultMessage": "Sign in" | ||||
| 	}, | ||||
|   | ||||
| @@ -122,18 +122,15 @@ const Dashboard = () => { | ||||
| 			<pre> | ||||
| 				<code>{`Todo: | ||||
|  | ||||
| - Users: permissions modal and trigger after adding user | ||||
| - modal dialgs for everything | ||||
| - Tables | ||||
| - check mobile | ||||
| - fix bad jwt not refreshing entire page | ||||
| - add help docs for host types | ||||
| - show user as disabled on user table | ||||
|  | ||||
| More for api, then implement here: | ||||
| - Properly implement refresh tokens | ||||
| - don't create default user, instead use the is_setup from v3 | ||||
|   - also remove the initial user/pass env vars | ||||
|   - update docs for this | ||||
| - Add error message_18n for all backend errors | ||||
| - minor: certificates expand with hosts needs to omit 'is_deleted' | ||||
| `}</code> | ||||
|   | ||||
							
								
								
									
										10
									
								
								frontend/src/pages/Setup/index.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/pages/Setup/index.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| .logo { | ||||
| 	width: 200px; | ||||
| } | ||||
|  | ||||
| .helperBtns { | ||||
| 	position: absolute; | ||||
| 	top: 10px; | ||||
| 	right: 10px; | ||||
| 	z-index: 1000; | ||||
| } | ||||
							
								
								
									
										191
									
								
								frontend/src/pages/Setup/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								frontend/src/pages/Setup/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,191 @@ | ||||
| import { useQueryClient } from "@tanstack/react-query"; | ||||
| import cn from "classnames"; | ||||
| import { Field, Form, Formik } from "formik"; | ||||
| import { useState } from "react"; | ||||
| import { Alert } from "react-bootstrap"; | ||||
| import { createUser } from "src/api/backend"; | ||||
| import { Button, LocalePicker, Page, ThemeSwitcher } from "src/components"; | ||||
| import { useAuthState } from "src/context"; | ||||
| import { intl } from "src/locale"; | ||||
| import { validateEmail, validateString } from "src/modules/Validations"; | ||||
| import styles from "./index.module.css"; | ||||
|  | ||||
| interface Payload { | ||||
| 	name: string; | ||||
| 	email: string; | ||||
| 	password: string; | ||||
| } | ||||
|  | ||||
| export default function Setup() { | ||||
| 	const queryClient = useQueryClient(); | ||||
| 	const { login } = useAuthState(); | ||||
| 	const [errorMsg, setErrorMsg] = useState<string | null>(null); | ||||
|  | ||||
| 	const onSubmit = async (values: Payload, { setSubmitting }: any) => { | ||||
| 		setErrorMsg(null); | ||||
|  | ||||
| 		// Set a nickname, which is the first word of the name | ||||
| 		const nickname = values.name.split(" ")[0]; | ||||
|  | ||||
| 		const { password, ...payload } = { | ||||
| 			...values, | ||||
| 			...{ | ||||
| 				nickname, | ||||
| 				auth: { | ||||
| 					type: "password", | ||||
| 					secret: values.password, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}; | ||||
|  | ||||
| 		try { | ||||
| 			const user = await createUser(payload, true); | ||||
| 			if (user && typeof user.id !== "undefined" && user.id) { | ||||
| 				try { | ||||
| 					await login(user.email, password); | ||||
| 					// Trigger a Health change | ||||
| 					await queryClient.refetchQueries({ queryKey: ["health"] }); | ||||
| 					// window.location.reload(); | ||||
| 				} catch (err: any) { | ||||
| 					setErrorMsg(err.message); | ||||
| 				} | ||||
| 			} else { | ||||
| 				setErrorMsg("cannot_create_user"); | ||||
| 			} | ||||
| 		} catch (err: any) { | ||||
| 			setErrorMsg(err.message); | ||||
| 		} | ||||
| 		setSubmitting(false); | ||||
| 	}; | ||||
|  | ||||
| 	return ( | ||||
| 		<Page className="page page-center"> | ||||
| 			<div className={cn("d-none", "d-md-flex", styles.helperBtns)}> | ||||
| 				<LocalePicker /> | ||||
| 				<ThemeSwitcher /> | ||||
| 			</div> | ||||
| 			<div className="container container-tight py-4"> | ||||
| 				<div className="text-center mb-4"> | ||||
| 					<img | ||||
| 						className={styles.logo} | ||||
| 						src="/images/logo-text-horizontal-grey.png" | ||||
| 						alt="Nginx Proxy Manager" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div className="card card-md"> | ||||
| 					<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible> | ||||
| 						{errorMsg} | ||||
| 					</Alert> | ||||
| 					<Formik | ||||
| 						initialValues={ | ||||
| 							{ | ||||
| 								name: "", | ||||
| 								email: "", | ||||
| 								password: "", | ||||
| 							} as any | ||||
| 						} | ||||
| 						onSubmit={onSubmit} | ||||
| 					> | ||||
| 						{({ isSubmitting }) => ( | ||||
| 							<Form> | ||||
| 								<div className="card-body text-center py-4 p-sm-5"> | ||||
| 									<h1 className="mt-5">{intl.formatMessage({ id: "setup.title" })}</h1> | ||||
| 									<p className="text-secondary">{intl.formatMessage({ id: "setup.preamble" })}</p> | ||||
| 								</div> | ||||
| 								<hr /> | ||||
| 								<div className="card-body"> | ||||
| 									<div className="mb-3"> | ||||
| 										<Field name="name" validate={validateString(1, 50)}> | ||||
| 											{({ field, form }: any) => ( | ||||
| 												<div className="form-floating mb-3"> | ||||
| 													<input | ||||
| 														id="name" | ||||
| 														className={`form-control ${form.errors.name && form.touched.name ? "is-invalid" : ""}`} | ||||
| 														placeholder={intl.formatMessage({ id: "user.full-name" })} | ||||
| 														{...field} | ||||
| 													/> | ||||
| 													<label htmlFor="name"> | ||||
| 														{intl.formatMessage({ id: "user.full-name" })} | ||||
| 													</label> | ||||
| 													{form.errors.name ? ( | ||||
| 														<div className="invalid-feedback"> | ||||
| 															{form.errors.name && form.touched.name | ||||
| 																? form.errors.name | ||||
| 																: null} | ||||
| 														</div> | ||||
| 													) : null} | ||||
| 												</div> | ||||
| 											)} | ||||
| 										</Field> | ||||
| 									</div> | ||||
| 									<div className="mb-3"> | ||||
| 										<Field name="email" validate={validateEmail()}> | ||||
| 											{({ field, form }: any) => ( | ||||
| 												<div className="form-floating mb-3"> | ||||
| 													<input | ||||
| 														id="email" | ||||
| 														type="email" | ||||
| 														className={`form-control ${form.errors.email && form.touched.email ? "is-invalid" : ""}`} | ||||
| 														placeholder={intl.formatMessage({ id: "email-address" })} | ||||
| 														{...field} | ||||
| 													/> | ||||
| 													<label htmlFor="email"> | ||||
| 														{intl.formatMessage({ id: "email-address" })} | ||||
| 													</label> | ||||
| 													{form.errors.email ? ( | ||||
| 														<div className="invalid-feedback"> | ||||
| 															{form.errors.email && form.touched.email | ||||
| 																? form.errors.email | ||||
| 																: null} | ||||
| 														</div> | ||||
| 													) : null} | ||||
| 												</div> | ||||
| 											)} | ||||
| 										</Field> | ||||
| 									</div> | ||||
| 									<div className="mb-3"> | ||||
| 										<Field name="password" validate={validateString(8, 100)}> | ||||
| 											{({ field, form }: any) => ( | ||||
| 												<div className="form-floating mb-3"> | ||||
| 													<input | ||||
| 														id="password" | ||||
| 														type="password" | ||||
| 														className={`form-control ${form.errors.password && form.touched.password ? "is-invalid" : ""}`} | ||||
| 														placeholder={intl.formatMessage({ id: "user.new-password" })} | ||||
| 														{...field} | ||||
| 													/> | ||||
| 													<label htmlFor="password"> | ||||
| 														{intl.formatMessage({ id: "user.new-password" })} | ||||
| 													</label> | ||||
| 													{form.errors.password ? ( | ||||
| 														<div className="invalid-feedback"> | ||||
| 															{form.errors.password && form.touched.password | ||||
| 																? form.errors.password | ||||
| 																: null} | ||||
| 														</div> | ||||
| 													) : null} | ||||
| 												</div> | ||||
| 											)} | ||||
| 										</Field> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 								<div className="text-center my-3 mx-3"> | ||||
| 									<Button | ||||
| 										type="submit" | ||||
| 										actionType="primary" | ||||
| 										data-bs-dismiss="modal" | ||||
| 										isLoading={isSubmitting} | ||||
| 										disabled={isSubmitting} | ||||
| 										className="w-100" | ||||
| 									> | ||||
| 										{intl.formatMessage({ id: "save" })} | ||||
| 									</Button> | ||||
| 								</div> | ||||
| 							</Form> | ||||
| 						)} | ||||
| 					</Formik> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</Page> | ||||
| 	); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user