mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-14 10:52:34 +00: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:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.3/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
@@ -34,7 +34,7 @@
|
||||
"rooks": "^9.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.2",
|
||||
"@biomejs/biome": "^2.2.3",
|
||||
"@formatjs/cli": "^6.7.2",
|
||||
"@tanstack/react-query-devtools": "^5.85.6",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -178,59 +178,59 @@
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@biomejs/biome@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.2.2.tgz#a039a59ce8612ee706c0abbf285eb3ae04a6f1a9"
|
||||
integrity sha512-j1omAiQWCkhuLgwpMKisNKnsM6W8Xtt1l0WZmqY/dFj8QPNkIoTvk4tSsi40FaAAkBE1PU0AFG2RWFBWenAn+w==
|
||||
"@biomejs/biome@^2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.2.3.tgz#9d17991c80e006c5ca3e21bebe84b7afd71559e3"
|
||||
integrity sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg==
|
||||
optionalDependencies:
|
||||
"@biomejs/cli-darwin-arm64" "2.2.2"
|
||||
"@biomejs/cli-darwin-x64" "2.2.2"
|
||||
"@biomejs/cli-linux-arm64" "2.2.2"
|
||||
"@biomejs/cli-linux-arm64-musl" "2.2.2"
|
||||
"@biomejs/cli-linux-x64" "2.2.2"
|
||||
"@biomejs/cli-linux-x64-musl" "2.2.2"
|
||||
"@biomejs/cli-win32-arm64" "2.2.2"
|
||||
"@biomejs/cli-win32-x64" "2.2.2"
|
||||
"@biomejs/cli-darwin-arm64" "2.2.3"
|
||||
"@biomejs/cli-darwin-x64" "2.2.3"
|
||||
"@biomejs/cli-linux-arm64" "2.2.3"
|
||||
"@biomejs/cli-linux-arm64-musl" "2.2.3"
|
||||
"@biomejs/cli-linux-x64" "2.2.3"
|
||||
"@biomejs/cli-linux-x64-musl" "2.2.3"
|
||||
"@biomejs/cli-win32-arm64" "2.2.3"
|
||||
"@biomejs/cli-win32-x64" "2.2.3"
|
||||
|
||||
"@biomejs/cli-darwin-arm64@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.2.tgz#18560240d374d8fa89df7d5af0f2101971a05d04"
|
||||
integrity sha512-6ePfbCeCPryWu0CXlzsWNZgVz/kBEvHiPyNpmViSt6A2eoDf4kXs3YnwQPzGjy8oBgQulrHcLnJL0nkCh80mlQ==
|
||||
"@biomejs/cli-darwin-arm64@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.3.tgz#e18240343fa705dafb08ba72a7b0e88f04a8be3e"
|
||||
integrity sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w==
|
||||
|
||||
"@biomejs/cli-darwin-x64@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.2.tgz#68bf6e2dc4384f96d590b2c342bfa09fbb7be492"
|
||||
integrity sha512-Tn4JmVO+rXsbRslml7FvKaNrlgUeJot++FkvYIhl1OkslVCofAtS35MPlBMhXgKWF9RNr9cwHanrPTUUXcYGag==
|
||||
"@biomejs/cli-darwin-x64@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.3.tgz#964b51c9f649e3a725f6f43e75c4173b9ab8a3ae"
|
||||
integrity sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg==
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.2.tgz#3f091595615739c69ccc300a5eb3acbefca3996c"
|
||||
integrity sha512-/MhYg+Bd6renn6i1ylGFL5snYUn/Ct7zoGVKhxnro3bwekiZYE8Kl39BSb0MeuqM+72sThkQv4TnNubU9njQRw==
|
||||
"@biomejs/cli-linux-arm64-musl@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.3.tgz#1756c37960d5585ca865e184539b113e48719b41"
|
||||
integrity sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ==
|
||||
|
||||
"@biomejs/cli-linux-arm64@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.2.tgz#9ed17fc01681e83a1d52efd366f9edc3efbca0ae"
|
||||
integrity sha512-JfrK3gdmWWTh2J5tq/rcWCOsImVyzUnOS2fkjhiYKCQ+v8PqM+du5cfB7G1kXas+7KQeKSWALv18iQqdtIMvzw==
|
||||
"@biomejs/cli-linux-arm64@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.3.tgz#036c6334d5b09b51233ce5120b18f4c89a15a74c"
|
||||
integrity sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g==
|
||||
|
||||
"@biomejs/cli-linux-x64-musl@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.2.tgz#01bcb119f2f94af5e5610a961b9ffcfa26cf2a3b"
|
||||
integrity sha512-ZCLXcZvjZKSiRY/cFANKg+z6Fhsf9MHOzj+NrDQcM+LbqYRT97LyCLWy2AS+W2vP+i89RyRM+kbGpUzbRTYWig==
|
||||
"@biomejs/cli-linux-x64-musl@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.3.tgz#e6cce01910b9f56c1645c5518595d0b1eb38c245"
|
||||
integrity sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w==
|
||||
|
||||
"@biomejs/cli-linux-x64@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.2.tgz#c5d0c6ce58b90e30f123e2cfdb29d2add65e2384"
|
||||
integrity sha512-Ogb+77edO5LEP/xbNicACOWVLt8mgC+E1wmpUakr+O4nKwLt9vXe74YNuT3T1dUBxC/SnrVmlzZFC7kQJEfquQ==
|
||||
"@biomejs/cli-linux-x64@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.3.tgz#f328e7cfde92fad6c7ad215df1f51b146b4ed007"
|
||||
integrity sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw==
|
||||
|
||||
"@biomejs/cli-win32-arm64@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.2.tgz#26e0fe782de6d83f3ecb4f247322a483104d749a"
|
||||
integrity sha512-wBe2wItayw1zvtXysmHJQoQqXlTzHSpQRyPpJKiNIR21HzH/CrZRDFic1C1jDdp+zAPtqhNExa0owKMbNwW9cQ==
|
||||
"@biomejs/cli-win32-arm64@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.3.tgz#b8d64ca6dc1c50b8f3d42475afd31b7b44460935"
|
||||
integrity sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA==
|
||||
|
||||
"@biomejs/cli-win32-x64@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.2.tgz#8c08d82e50b06ad50e4bc54b4bb41428d4261b5c"
|
||||
integrity sha512-DAuHhHekGfiGb6lCcsT4UyxQmVwQiBCBUMwVra/dcOSs9q8OhfaZgey51MlekT3p8UwRqtXQfFuEJBhJNdLZwg==
|
||||
"@biomejs/cli-win32-x64@2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.3.tgz#ecafffddf0c0675c825735c7cc917cbc8c538433"
|
||||
integrity sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ==
|
||||
|
||||
"@esbuild/aix-ppc64@0.25.9":
|
||||
version "0.25.9"
|
||||
|
Reference in New Issue
Block a user