From 24fdf3487bfaed2a36925786f3a734fff6051476 Mon Sep 17 00:00:00 2001
From: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
Date: Mon, 6 Feb 2023 13:51:39 -0800
Subject: [PATCH] Move Register Component to Redux (#2630)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
---
.../Console/Support/ApiKeyRegister.tsx | 16 +-
.../Support/ClusterRegistrationForm.tsx | 116 +++
.../Console/Support/OfflineRegistration.tsx | 159 +++++
.../Console/Support/OnlineRegistration.tsx | 190 +++++
.../src/screens/Console/Support/Register.tsx | 665 ++----------------
.../Console/Support/RegisterOperator.tsx | 5 +-
.../Console/Support/RegisterStatus.tsx | 5 +-
.../Console/Support/SubnetMFAToken.tsx | 110 +++
.../screens/Console/Support/registerSlice.ts | 132 ++++
.../screens/Console/Support/registerThunks.ts | 211 ++++++
.../src/screens/Console/Support/utils.tsx | 7 +-
portal-ui/src/store.ts | 2 +
restapi/admin_subnet.go | 39 +-
13 files changed, 1042 insertions(+), 615 deletions(-)
create mode 100644 portal-ui/src/screens/Console/Support/ClusterRegistrationForm.tsx
create mode 100644 portal-ui/src/screens/Console/Support/OfflineRegistration.tsx
create mode 100644 portal-ui/src/screens/Console/Support/OnlineRegistration.tsx
create mode 100644 portal-ui/src/screens/Console/Support/SubnetMFAToken.tsx
create mode 100644 portal-ui/src/screens/Console/Support/registerSlice.ts
create mode 100644 portal-ui/src/screens/Console/Support/registerThunks.ts
diff --git a/portal-ui/src/screens/Console/Support/ApiKeyRegister.tsx b/portal-ui/src/screens/Console/Support/ApiKeyRegister.tsx
index 043d2558f..fbbb0d313 100644
--- a/portal-ui/src/screens/Console/Support/ApiKeyRegister.tsx
+++ b/portal-ui/src/screens/Console/Support/ApiKeyRegister.tsx
@@ -30,11 +30,12 @@ import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
+import { useNavigate } from "react-router-dom";
+import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
interface IApiKeyRegister {
classes: any;
registerEndpoint: string;
- afterRegister: () => void;
}
const styles = (theme: Theme) =>
@@ -45,11 +46,9 @@ const styles = (theme: Theme) =>
...spacingUtils,
});
-const ApiKeyRegister = ({
- classes,
- registerEndpoint,
- afterRegister,
-}: IApiKeyRegister) => {
+const ApiKeyRegister = ({ classes, registerEndpoint }: IApiKeyRegister) => {
+ const navigate = useNavigate();
+
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
const [apiKey, setApiKey] = useState("");
const [loading, setLoading] = useState(false);
@@ -67,8 +66,7 @@ const ApiKeyRegister = ({
.then((resp: SubnetLoginResponse) => {
setLoading(false);
if (resp && resp.registered) {
- reset();
- afterRegister();
+ navigate(IAM_PAGES.LICENSE);
}
})
.catch((err: ErrorResponseHandler) => {
@@ -76,7 +74,7 @@ const ApiKeyRegister = ({
setLoading(false);
reset();
});
- }, [afterRegister, apiKey, dispatch, loading, registerEndpoint]);
+ }, [apiKey, dispatch, loading, registerEndpoint, navigate]);
useEffect(() => {
if (fromModal) {
diff --git a/portal-ui/src/screens/Console/Support/ClusterRegistrationForm.tsx b/portal-ui/src/screens/Console/Support/ClusterRegistrationForm.tsx
new file mode 100644
index 000000000..d9f420738
--- /dev/null
+++ b/portal-ui/src/screens/Console/Support/ClusterRegistrationForm.tsx
@@ -0,0 +1,116 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2023 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import React from "react";
+import { Box } from "@mui/material";
+import { FormTitle } from "./utils";
+import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
+import { setLoading, setSelectedSubnetOrganization } from "./registerSlice";
+import { Button } from "mds";
+import RegisterHelpBox from "./RegisterHelpBox";
+import { useSelector } from "react-redux";
+import { AppState, useAppDispatch } from "../../../store";
+import { callRegister } from "./registerThunks";
+
+const ClusterRegistrationForm = () => {
+ const dispatch = useAppDispatch();
+
+ const subnetAccessToken = useSelector(
+ (state: AppState) => state.register.subnetAccessToken
+ );
+ const selectedSubnetOrganization = useSelector(
+ (state: AppState) => state.register.selectedSubnetOrganization
+ );
+ const subnetOrganizations = useSelector(
+ (state: AppState) => state.register.subnetOrganizations
+ );
+ const loading = useSelector((state: AppState) => state.register.loading);
+
+ return (
+
+
+
+
+
+
+
+ dispatch(setSelectedSubnetOrganization(e.target.value as string))
+ }
+ label="Select an organization"
+ value={selectedSubnetOrganization}
+ options={subnetOrganizations.map((organization) => ({
+ label: organization.company,
+ value: organization.accountId.toString(),
+ }))}
+ />
+
+
+
+
+
+
+ );
+};
+
+export default ClusterRegistrationForm;
diff --git a/portal-ui/src/screens/Console/Support/OfflineRegistration.tsx b/portal-ui/src/screens/Console/Support/OfflineRegistration.tsx
new file mode 100644
index 000000000..a37a155b0
--- /dev/null
+++ b/portal-ui/src/screens/Console/Support/OfflineRegistration.tsx
@@ -0,0 +1,159 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2023 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import React, { Fragment } from "react";
+import { Box, Link } from "@mui/material";
+import { ClusterRegistered, FormTitle } from "./utils";
+import { Button, CopyIcon, OfflineRegistrationIcon } from "mds";
+import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
+import CopyToClipboard from "react-copy-to-clipboard";
+import RegisterHelpBox from "./RegisterHelpBox";
+import { AppState } from "../../../store";
+import { useSelector } from "react-redux";
+
+const OfflineRegistration = () => {
+ const subnetRegToken = useSelector(
+ (state: AppState) => state.register.subnetRegToken
+ );
+ const clusterRegistered = useSelector(
+ (state: AppState) => state.register.clusterRegistered
+ );
+ const licenseInfo = useSelector(
+ (state: AppState) => state.register.licenseInfo
+ );
+
+ const offlineRegUrl = `https://subnet.min.io/cluster/register?token=${subnetRegToken}`;
+
+ return (
+
+
+ {clusterRegistered && licenseInfo ? (
+
+ ) : null}
+
+ }
+ title={`Register cluster in an Air-gap environment`}
+ />
+
+
+
+
+
+
+
+ Click on the link to register this cluster in SUBNET
+
+ If this machine does not have internet connection, Copy paste
+ the following URL in a browser where you access SUBNET and
+ follow the instructions to complete the registration
+
+
+
+
+
+
+
+ );
+};
+
+export default OfflineRegistration;
diff --git a/portal-ui/src/screens/Console/Support/OnlineRegistration.tsx b/portal-ui/src/screens/Console/Support/OnlineRegistration.tsx
new file mode 100644
index 000000000..13da56a83
--- /dev/null
+++ b/portal-ui/src/screens/Console/Support/OnlineRegistration.tsx
@@ -0,0 +1,190 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2023 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import React, { Fragment } from "react";
+import { Box } from "@mui/material";
+import { FormTitle } from "./utils";
+import { Button, OnlineRegistrationIcon, UsersIcon } from "mds";
+import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
+import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
+import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
+import RegisterHelpBox from "./RegisterHelpBox";
+import { Theme } from "@mui/material/styles";
+import createStyles from "@mui/styles/createStyles";
+import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
+import makeStyles from "@mui/styles/makeStyles";
+import { useSelector } from "react-redux";
+import { selOpMode } from "../../../systemSlice";
+import { AppState, useAppDispatch } from "../../../store";
+import {
+ setShowPassword,
+ setSubnetEmail,
+ setSubnetPassword,
+} from "./registerSlice";
+import { subnetLogin } from "./registerThunks";
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ sizedLabel: {
+ minWidth: "75px",
+ },
+ ...spacingUtils,
+ })
+);
+
+const OnlineRegistration = () => {
+ const classes = useStyles();
+ const dispatch = useAppDispatch();
+
+ const operatorMode = useSelector(selOpMode);
+ const subnetPassword = useSelector(
+ (state: AppState) => state.register.subnetPassword
+ );
+ const subnetEmail = useSelector(
+ (state: AppState) => state.register.subnetEmail
+ );
+ const showPassword = useSelector(
+ (state: AppState) => state.register.showPassword
+ );
+ const loading = useSelector((state: AppState) => state.register.loading);
+
+ return (
+
+
+ }
+ title={`Online activation of MinIO Subscription Network License`}
+ />
+
+
+
+
+ Use your MinIO Subscription Network login credentials to register
+ this cluster.
+
+
+ ) =>
+ dispatch(setSubnetEmail(event.target.value))
+ }
+ label="Email"
+ value={subnetEmail}
+ overlayIcon={}
+ />
+ ) =>
+ dispatch(setSubnetPassword(event.target.value))
+ }
+ label="Password"
+ type={showPassword ? "text" : "password"}
+ value={subnetPassword}
+ overlayIcon={
+ showPassword ? :
+ }
+ overlayAction={() => dispatch(setShowPassword(!showPassword))}
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default OnlineRegistration;
diff --git a/portal-ui/src/screens/Console/Support/Register.tsx b/portal-ui/src/screens/Console/Support/Register.tsx
index eb306d79b..1e4285e62 100644
--- a/portal-ui/src/screens/Console/Support/Register.tsx
+++ b/portal-ui/src/screens/Console/Support/Register.tsx
@@ -14,61 +14,37 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-import React, { Fragment, useCallback, useEffect, useState } from "react";
+import React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
-import {
- Button,
- CopyIcon,
- OfflineRegistrationIcon,
- OnlineRegistrationIcon,
- PageHeader,
- UsersIcon,
-} from "mds";
+import { PageHeader } from "mds";
import createStyles from "@mui/styles/createStyles";
-import {
- actionsTray,
- containerForHeader,
- searchField,
- spacingUtils,
-} from "../Common/FormComponents/common/styleLibrary";
+import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
import withStyles from "@mui/styles/withStyles";
-import { Box, Link } from "@mui/material";
+import { Box } from "@mui/material";
import PageLayout from "../Common/Layout/PageLayout";
-import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
-import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
-import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import api from "../../../common/api";
-import {
- SubnetInfo,
- SubnetLoginRequest,
- SubnetLoginResponse,
- SubnetLoginWithMFARequest,
- SubnetOrganization,
- SubnetRegisterRequest,
- SubnetRegTokenResponse,
-} from "../License/types";
+import { SubnetRegTokenResponse } from "../License/types";
import { ErrorResponseHandler } from "../../../common/types";
-import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
-import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
-import { hasPermission } from "../../../common/SecureComponent";
-import {
- CONSOLE_UI_RESOURCE,
- IAM_PAGES,
- IAM_PAGES_PERMISSIONS,
-} from "../../../common/SecureComponent/permissions";
import { useSelector } from "react-redux";
-
-import RegisterHelpBox from "./RegisterHelpBox";
-import { selOpMode, setErrorSnackMessage } from "../../../systemSlice";
-import { useAppDispatch } from "../../../store";
+import { setErrorSnackMessage } from "../../../systemSlice";
+import { AppState, useAppDispatch } from "../../../store";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import { TabPanel } from "../../shared/tabs";
-import { ClusterRegistered, FormTitle, ProxyConfiguration } from "./utils";
+import { ClusterRegistered, ProxyConfiguration } from "./utils";
import ApiKeyRegister from "./ApiKeyRegister";
-import CopyToClipboard from "react-copy-to-clipboard";
-import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
+import { fetchLicenseInfo } from "./registerThunks";
+import {
+ resetRegisterForm,
+ setCurTab,
+ setLoading,
+ setSubnetRegToken,
+} from "./registerSlice";
+import OfflineRegistration from "./OfflineRegistration";
+import SubnetMFAToken from "./SubnetMFAToken";
+import ClusterRegistrationForm from "./ClusterRegistrationForm";
+import OnlineRegistration from "./OnlineRegistration";
interface IRegister {
classes: any;
@@ -79,469 +55,83 @@ const styles = (theme: Theme) =>
sizedLabel: {
minWidth: "75px",
},
- ...actionsTray,
- ...searchField,
...spacingUtils,
- ...containerForHeader,
});
const Register = ({ classes }: IRegister) => {
const dispatch = useAppDispatch();
- const operatorMode = useSelector(selOpMode);
- const [license, setLicense] = useState("");
- const [subnetPassword, setSubnetPassword] = useState("");
- const [subnetEmail, setSubnetEmail] = useState("");
- const [subnetMFAToken, setSubnetMFAToken] = useState("");
- const [subnetOTP, setSubnetOTP] = useState("");
- const [subnetAccessToken, setSubnetAccessToken] = useState("");
- const [selectedSubnetOrganization, setSelectedSubnetOrganization] =
- useState("");
- const [subnetRegToken, setSubnetRegToken] = useState("");
- const [subnetOrganizations, setSubnetOrganizations] = useState<
- SubnetOrganization[]
- >([]);
- const [showPassword, setShowPassword] = useState(false);
- const [loading, setLoading] = useState(false);
- const [loadingLicenseInfo, setLoadingLicenseInfo] = useState(false);
- const [clusterRegistered, setClusterRegistered] = useState(false);
- const [licenseInfo, setLicenseInfo] = useState();
- const [curTab, setCurTab] = useState(0);
+
+ const subnetMFAToken = useSelector(
+ (state: AppState) => state.register.subnetMFAToken
+ );
+ const subnetAccessToken = useSelector(
+ (state: AppState) => state.register.subnetAccessToken
+ );
+
+ const subnetRegToken = useSelector(
+ (state: AppState) => state.register.subnetRegToken
+ );
+ const subnetOrganizations = useSelector(
+ (state: AppState) => state.register.subnetOrganizations
+ );
+
+ const loading = useSelector((state: AppState) => state.register.loading);
+ const loadingLicenseInfo = useSelector(
+ (state: AppState) => state.register.loadingLicenseInfo
+ );
+ const clusterRegistered = useSelector(
+ (state: AppState) => state.register.clusterRegistered
+ );
+ const licenseInfo = useSelector(
+ (state: AppState) => state.register.licenseInfo
+ );
+ const curTab = useSelector((state: AppState) => state.register.curTab);
const [initialLicenseLoading, setInitialLicenseLoading] =
useState(true);
- const clearForm = () => {
- setSubnetAccessToken("");
- setSelectedSubnetOrganization("");
- setSubnetRegToken("");
- setShowPassword(false);
- setSubnetOrganizations([]);
- setLicense("");
- setSubnetPassword("");
- setSubnetEmail("");
- setSubnetMFAToken("");
- setSubnetOTP("");
- };
- const getSubnetInfo = hasPermission(
- CONSOLE_UI_RESOURCE,
- IAM_PAGES_PERMISSIONS[IAM_PAGES.LICENSE],
- true
- );
-
- const fetchLicenseInfo = useCallback(() => {
- if (loadingLicenseInfo) {
- return;
- }
- if (getSubnetInfo) {
- setLoadingLicenseInfo(true);
- api
- .invoke("GET", `/api/v1/subnet/info`)
- .then((res: SubnetInfo) => {
- setLicenseInfo(res);
- setClusterRegistered(true);
- setLoadingLicenseInfo(false);
- })
- .catch((err: ErrorResponseHandler) => {
- if (
- err.detailedError.toLowerCase() !==
- "License is not present".toLowerCase()
- ) {
- dispatch(setErrorSnackMessage(err));
- }
- setClusterRegistered(false);
- setLoadingLicenseInfo(false);
- });
- } else {
- setLoadingLicenseInfo(false);
- }
- }, [loadingLicenseInfo, getSubnetInfo, dispatch]);
+ useEffect(() => {
+ // when unmounted, reset
+ return () => {
+ dispatch(resetRegisterForm());
+ };
+ }, [dispatch]);
const fetchSubnetRegToken = () => {
if (loading || subnetRegToken) {
return;
}
- setLoading(true);
+ dispatch(setLoading(true));
api
.invoke("GET", "/api/v1/subnet/registration-token")
.then((resp: SubnetRegTokenResponse) => {
- setLoading(false);
+ dispatch(setLoading(false));
if (resp && resp.regToken) {
- setSubnetRegToken(resp.regToken);
+ dispatch(setSubnetRegToken(resp.regToken));
}
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
dispatch(setErrorSnackMessage(err));
- setLoading(false);
- });
- };
-
- const callRegister = (token: string, account_id: string) => {
- const request: SubnetRegisterRequest = {
- token: token,
- account_id: account_id,
- };
- api
- .invoke("POST", "/api/v1/subnet/register", request)
- .then(() => {
- setLoading(false);
- clearForm();
- fetchLicenseInfo();
- })
- .catch((err: ErrorResponseHandler) => {
- dispatch(setErrorSnackMessage(err));
- setLoading(false);
- });
- };
- const subnetRegister = () => {
- if (loading) {
- return;
- }
- setLoading(true);
- if (subnetAccessToken && selectedSubnetOrganization) {
- callRegister(subnetAccessToken, selectedSubnetOrganization);
- }
- };
-
- const subnetLoginWithMFA = () => {
- if (loading) {
- return;
- }
- setLoading(true);
- const request: SubnetLoginWithMFARequest = {
- username: subnetEmail,
- otp: subnetOTP,
- mfa_token: subnetMFAToken,
- };
- api
- .invoke("POST", "/api/v1/subnet/login/mfa", request)
- .then((resp: SubnetLoginResponse) => {
- setLoading(false);
- if (resp && resp.access_token && resp.organizations.length > 0) {
- if (resp.organizations.length === 1) {
- callRegister(
- resp.access_token,
- resp.organizations[0].accountId.toString()
- );
- } else {
- setSubnetAccessToken(resp.access_token);
- setSubnetOrganizations(resp.organizations);
- setSelectedSubnetOrganization(
- resp.organizations[0].accountId.toString()
- );
- }
- }
- })
- .catch((err: ErrorResponseHandler) => {
- dispatch(setErrorSnackMessage(err));
- setLoading(false);
- setSubnetOTP("");
- });
- };
-
- const subnetLogin = () => {
- if (loading) {
- return;
- }
- setLoading(true);
- let request: SubnetLoginRequest = {
- username: subnetEmail,
- password: subnetPassword,
- apiKey: license,
- };
- api
- .invoke("POST", "/api/v1/subnet/login", request)
- .then((resp: SubnetLoginResponse) => {
- setLoading(false);
- if (resp && resp.registered) {
- clearForm();
- fetchLicenseInfo();
- } else if (resp && resp.mfa_token) {
- setSubnetMFAToken(resp.mfa_token);
- } else if (resp && resp.access_token && resp.organizations.length > 0) {
- setSubnetAccessToken(resp.access_token);
- setSubnetOrganizations(resp.organizations);
- setSelectedSubnetOrganization(
- resp.organizations[0].accountId.toString()
- );
- }
- })
- .catch((err: ErrorResponseHandler) => {
- dispatch(setErrorSnackMessage(err));
- setLoading(false);
- clearForm();
+ dispatch(setLoading(false));
});
};
useEffect(() => {
if (initialLicenseLoading) {
- fetchLicenseInfo();
+ dispatch(fetchLicenseInfo());
setInitialLicenseLoading(false);
}
- }, [fetchLicenseInfo, initialLicenseLoading, setInitialLicenseLoading]);
+ }, [initialLicenseLoading, setInitialLicenseLoading, dispatch]);
let clusterRegistrationForm: JSX.Element = ;
if (subnetAccessToken && subnetOrganizations.length > 0) {
- clusterRegistrationForm = (
-
-
-
-
-
-
-
- setSelectedSubnetOrganization(e.target.value as string)
- }
- label="Select an organization"
- value={selectedSubnetOrganization}
- options={subnetOrganizations.map((organization) => ({
- label: organization.company,
- value: organization.accountId.toString(),
- }))}
- />
-
- subnetRegister()}
- disabled={loading || subnetAccessToken.trim().length === 0}
- variant="callAction"
- label={"Register"}
- />
-
-
-
-
-
- );
+ clusterRegistrationForm = ;
} else if (subnetMFAToken) {
- clusterRegistrationForm = (
-
-
-
- Two-Factor Authentication
-
-
-
- Please enter the 6-digit verification code that was sent to your
- email address. This code will be valid for 5 minutes.
-
-
-
- }
- id="subnet-otp"
- name="subnet-otp"
- onChange={(event: React.ChangeEvent) =>
- setSubnetOTP(event.target.value)
- }
- placeholder=""
- label=""
- value={subnetOTP}
- />
-
-
- subnetLoginWithMFA()}
- disabled={
- loading ||
- subnetOTP.trim().length === 0 ||
- subnetMFAToken.trim().length === 0
- }
- variant="callAction"
- label={"Verify"}
- />
-
-
-
-
-
- );
+ clusterRegistrationForm = ;
} else {
- clusterRegistrationForm = (
-
-
- }
- title={`Online activation of MinIO Subscription Network License`}
- />
-
-
-
-
- Use your MinIO Subscription Network login credentials to register
- this cluster.
-
-
- ) =>
- setSubnetEmail(event.target.value)
- }
- label="Email"
- value={subnetEmail}
- overlayIcon={}
- />
- ) =>
- setSubnetPassword(event.target.value)
- }
- label="Password"
- type={showPassword ? "text" : "password"}
- value={subnetPassword}
- overlayIcon={
- showPassword ? :
- }
- overlayAction={() => setShowPassword(!showPassword)}
- />
-
-
- {
- e.preventDefault();
- window.open(
- `https://min.io/signup?ref=${
- operatorMode ? "op" : "con"
- }`,
- "_blank"
- );
- }}
- label={"Sign up"}
- />
- subnetLogin()}
- label={"Register"}
- />
-
-
-
-
-
-
- );
+ clusterRegistrationForm = ;
}
const apiKeyRegistration = (
@@ -558,131 +148,14 @@ const Register = ({ classes }: IRegister) => {
{clusterRegistered && licenseInfo ? (
) : (
-
+
)}
-
+
);
- const offlineRegUrl = `https://subnet.min.io/cluster/register?token=${subnetRegToken}`;
- const offlineRegistration = (
-
-
- {clusterRegistered && licenseInfo ? (
-
- ) : null}
-
- }
- title={`Register cluster in an Airgap environment`}
- />
-
-
-
-
-
-
-
- Click on the link to register this cluster in SUBNET
-
- If this machine does not have internet connection, Copy paste
- the following URL in a browser where you access SUBNET and
- follow the instructions to complete the registration
-
-
-
-
-
-
-
- );
+ const offlineRegistration = ;
const regUi = (
@@ -702,7 +175,7 @@ const Register = ({ classes }: IRegister) => {
)}
- {!clusterRegistered && }
+ {!clusterRegistered && }
);
@@ -720,7 +193,7 @@ const Register = ({ classes }: IRegister) => {
, newValue: number) => {
- setCurTab(newValue);
+ dispatch(setCurTab(newValue));
}}
indicatorColor="primary"
textColor="primary"
@@ -731,17 +204,17 @@ const Register = ({ classes }: IRegister) => {
fetchSubnetRegToken()}
/>
diff --git a/portal-ui/src/screens/Console/Support/RegisterOperator.tsx b/portal-ui/src/screens/Console/Support/RegisterOperator.tsx
index 6047a8722..3ab0ecfec 100644
--- a/portal-ui/src/screens/Console/Support/RegisterOperator.tsx
+++ b/portal-ui/src/screens/Console/Support/RegisterOperator.tsx
@@ -82,10 +82,7 @@ const RegisterOperator = ({ classes }: IRegister) => {
{apiKeyRegistered ? (
) : (
-
+
)}
diff --git a/portal-ui/src/screens/Console/Support/RegisterStatus.tsx b/portal-ui/src/screens/Console/Support/RegisterStatus.tsx
index d51b08a56..d590adb4e 100644
--- a/portal-ui/src/screens/Console/Support/RegisterStatus.tsx
+++ b/portal-ui/src/screens/Console/Support/RegisterStatus.tsx
@@ -78,7 +78,10 @@ function RegisterStatus({ classes, showHelp }: IRegisterStatus) {
SUBNET
{" "}
diff --git a/portal-ui/src/screens/Console/Support/SubnetMFAToken.tsx b/portal-ui/src/screens/Console/Support/SubnetMFAToken.tsx
new file mode 100644
index 000000000..764152b74
--- /dev/null
+++ b/portal-ui/src/screens/Console/Support/SubnetMFAToken.tsx
@@ -0,0 +1,110 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2023 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import React from "react";
+import { Box } from "@mui/material";
+import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
+import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
+import { setSubnetOTP } from "./registerSlice";
+import { Button } from "mds";
+import RegisterHelpBox from "./RegisterHelpBox";
+import { AppState, useAppDispatch } from "../../../store";
+import { useSelector } from "react-redux";
+import { subnetLoginWithMFA } from "./registerThunks";
+
+const SubnetMFAToken = () => {
+ const dispatch = useAppDispatch();
+
+ const subnetMFAToken = useSelector(
+ (state: AppState) => state.register.subnetMFAToken
+ );
+ const subnetOTP = useSelector((state: AppState) => state.register.subnetOTP);
+ const loading = useSelector((state: AppState) => state.register.loading);
+
+ return (
+
+
+
+ Two-Factor Authentication
+
+
+
+ Please enter the 6-digit verification code that was sent to your email
+ address. This code will be valid for 5 minutes.
+
+
+
+ }
+ id="subnet-otp"
+ name="subnet-otp"
+ onChange={(event: React.ChangeEvent) =>
+ dispatch(setSubnetOTP(event.target.value))
+ }
+ placeholder=""
+ label=""
+ value={subnetOTP}
+ />
+
+
+ dispatch(subnetLoginWithMFA())}
+ disabled={
+ loading ||
+ subnetOTP.trim().length === 0 ||
+ subnetMFAToken.trim().length === 0
+ }
+ variant="callAction"
+ label={"Verify"}
+ />
+
+
+
+
+
+ );
+};
+export default SubnetMFAToken;
diff --git a/portal-ui/src/screens/Console/Support/registerSlice.ts b/portal-ui/src/screens/Console/Support/registerSlice.ts
new file mode 100644
index 000000000..02b8be054
--- /dev/null
+++ b/portal-ui/src/screens/Console/Support/registerSlice.ts
@@ -0,0 +1,132 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2023 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { SubnetInfo, SubnetOrganization } from "../License/types";
+
+export interface RegisterState {
+ license: string;
+ subnetPassword: string;
+ subnetEmail: string;
+ subnetMFAToken: string;
+ subnetOTP: string;
+ subnetAccessToken: string;
+ selectedSubnetOrganization: string;
+ subnetRegToken: string;
+ subnetOrganizations: SubnetOrganization[];
+ showPassword: boolean;
+ loading: boolean;
+ loadingLicenseInfo: boolean;
+ clusterRegistered: boolean;
+ licenseInfo: SubnetInfo | undefined;
+ curTab: number;
+}
+
+const initialState: RegisterState = {
+ license: "",
+ subnetPassword: "",
+ subnetEmail: "",
+ subnetMFAToken: "",
+ subnetOTP: "",
+ subnetAccessToken: "",
+ selectedSubnetOrganization: "",
+ subnetRegToken: "",
+ subnetOrganizations: [],
+ showPassword: false,
+ loading: false,
+ loadingLicenseInfo: false,
+ clusterRegistered: false,
+ licenseInfo: undefined,
+ curTab: 0,
+};
+
+export const registerSlice = createSlice({
+ name: "register",
+ initialState,
+ reducers: {
+ setLicense: (state, action: PayloadAction) => {
+ state.license = action.payload;
+ },
+ setSubnetPassword: (state, action: PayloadAction) => {
+ state.subnetPassword = action.payload;
+ },
+ setSubnetEmail: (state, action: PayloadAction) => {
+ state.subnetEmail = action.payload;
+ },
+ setSubnetMFAToken: (state, action: PayloadAction) => {
+ state.subnetMFAToken = action.payload;
+ },
+ setSubnetOTP: (state, action: PayloadAction) => {
+ state.subnetOTP = action.payload;
+ },
+ setSubnetAccessToken: (state, action: PayloadAction) => {
+ state.subnetAccessToken = action.payload;
+ },
+ setSelectedSubnetOrganization: (state, action: PayloadAction) => {
+ state.selectedSubnetOrganization = action.payload;
+ },
+ setSubnetRegToken: (state, action: PayloadAction) => {
+ state.subnetRegToken = action.payload;
+ },
+ setSubnetOrganizations: (
+ state,
+ action: PayloadAction
+ ) => {
+ state.subnetOrganizations = action.payload;
+ },
+ setShowPassword: (state, action: PayloadAction) => {
+ state.showPassword = action.payload;
+ },
+ setLoading: (state, action: PayloadAction) => {
+ state.loading = action.payload;
+ },
+ setLoadingLicenseInfo: (state, action: PayloadAction) => {
+ state.loadingLicenseInfo = action.payload;
+ },
+ setClusterRegistered: (state, action: PayloadAction) => {
+ state.clusterRegistered = action.payload;
+ },
+ setLicenseInfo: (state, action: PayloadAction) => {
+ state.licenseInfo = action.payload;
+ },
+ setCurTab: (state, action: PayloadAction) => {
+ state.curTab = action.payload;
+ },
+ resetRegisterForm: () => initialState,
+ },
+});
+
+// Action creators are generated for each case reducer function
+export const {
+ setLicense,
+ setSubnetPassword,
+ setSubnetEmail,
+ setSubnetMFAToken,
+ setSubnetOTP,
+ setSubnetAccessToken,
+ setSelectedSubnetOrganization,
+ setSubnetRegToken,
+ setSubnetOrganizations,
+ setShowPassword,
+ setLoading,
+ setLoadingLicenseInfo,
+ setClusterRegistered,
+ setLicenseInfo,
+ setCurTab,
+ resetRegisterForm,
+} = registerSlice.actions;
+
+export default registerSlice.reducer;
diff --git a/portal-ui/src/screens/Console/Support/registerThunks.ts b/portal-ui/src/screens/Console/Support/registerThunks.ts
new file mode 100644
index 000000000..a18e258cf
--- /dev/null
+++ b/portal-ui/src/screens/Console/Support/registerThunks.ts
@@ -0,0 +1,211 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2023 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import {
+ resetRegisterForm,
+ setClusterRegistered,
+ setLicenseInfo,
+ setLoading,
+ setLoadingLicenseInfo,
+ setSelectedSubnetOrganization,
+ setSubnetAccessToken,
+ setSubnetMFAToken,
+ setSubnetOrganizations,
+ setSubnetOTP,
+} from "./registerSlice";
+import api from "../../../common/api";
+import {
+ SubnetInfo,
+ SubnetLoginRequest,
+ SubnetLoginResponse,
+ SubnetLoginWithMFARequest,
+ SubnetRegisterRequest,
+} from "../License/types";
+import { ErrorResponseHandler } from "../../../common/types";
+import { setErrorSnackMessage } from "../../../systemSlice";
+import { createAsyncThunk } from "@reduxjs/toolkit";
+import { AppState } from "../../../store";
+import { hasPermission } from "../../../common/SecureComponent";
+import {
+ CONSOLE_UI_RESOURCE,
+ IAM_PAGES,
+ IAM_PAGES_PERMISSIONS,
+} from "../../../common/SecureComponent/permissions";
+
+export const fetchLicenseInfo = createAsyncThunk(
+ "register/fetchLicenseInfo",
+ async (_, { getState, dispatch }) => {
+ const state = getState() as AppState;
+
+ const getSubnetInfo = hasPermission(
+ CONSOLE_UI_RESOURCE,
+ IAM_PAGES_PERMISSIONS[IAM_PAGES.LICENSE],
+ true
+ );
+
+ const loadingLicenseInfo = state.register.loadingLicenseInfo;
+
+ if (loadingLicenseInfo) {
+ return;
+ }
+ if (getSubnetInfo) {
+ dispatch(setLoadingLicenseInfo(true));
+ api
+ .invoke("GET", `/api/v1/subnet/info`)
+ .then((res: SubnetInfo) => {
+ dispatch(setLicenseInfo(res));
+ dispatch(setClusterRegistered(true));
+ dispatch(setLoadingLicenseInfo(false));
+ })
+ .catch((err: ErrorResponseHandler) => {
+ if (
+ err.detailedError.toLowerCase() !==
+ "License is not present".toLowerCase() &&
+ err.detailedError.toLowerCase() !==
+ "license not found".toLowerCase()
+ ) {
+ dispatch(setErrorSnackMessage(err));
+ }
+ dispatch(setClusterRegistered(false));
+ dispatch(setLoadingLicenseInfo(false));
+ });
+ } else {
+ dispatch(setLoadingLicenseInfo(false));
+ }
+ }
+);
+
+export interface ClassRegisterArgs {
+ token: string;
+ account_id: string;
+}
+
+export const callRegister = createAsyncThunk(
+ "register/callRegister",
+ async (args: ClassRegisterArgs, { dispatch }) => {
+ const request: SubnetRegisterRequest = {
+ token: args.token,
+ account_id: args.account_id,
+ };
+ api
+ .invoke("POST", "/api/v1/subnet/register", request)
+ .then(() => {
+ dispatch(setLoading(false));
+ dispatch(resetRegisterForm());
+ dispatch(fetchLicenseInfo());
+ })
+ .catch((err: ErrorResponseHandler) => {
+ dispatch(setErrorSnackMessage(err));
+ dispatch(setLoading(false));
+ });
+ }
+);
+
+export const subnetLoginWithMFA = createAsyncThunk(
+ "register/subnetLoginWithMFA",
+ async (_, { getState, rejectWithValue, dispatch }) => {
+ const state = getState() as AppState;
+
+ const subnetEmail = state.register.subnetEmail;
+ const subnetMFAToken = state.register.subnetMFAToken;
+ const subnetOTP = state.register.subnetOTP;
+ const loading = state.register.loading;
+
+ if (loading) {
+ return;
+ }
+ dispatch(setLoading(true));
+ const request: SubnetLoginWithMFARequest = {
+ username: subnetEmail,
+ otp: subnetOTP,
+ mfa_token: subnetMFAToken,
+ };
+ api
+ .invoke("POST", "/api/v1/subnet/login/mfa", request)
+ .then((resp: SubnetLoginResponse) => {
+ dispatch(setLoading(false));
+ if (resp && resp.access_token && resp.organizations.length > 0) {
+ if (resp.organizations.length === 1) {
+ dispatch(
+ callRegister({
+ token: resp.access_token,
+ account_id: resp.organizations[0].accountId.toString(),
+ })
+ );
+ } else {
+ dispatch(setSubnetAccessToken(resp.access_token));
+ dispatch(setSubnetOrganizations(resp.organizations));
+ dispatch(
+ setSelectedSubnetOrganization(
+ resp.organizations[0].accountId.toString()
+ )
+ );
+ }
+ }
+ })
+ .catch((err: ErrorResponseHandler) => {
+ dispatch(setErrorSnackMessage(err));
+ dispatch(setLoading(false));
+ dispatch(setSubnetOTP(""));
+ });
+ }
+);
+
+export const subnetLogin = createAsyncThunk(
+ "register/subnetLogin",
+ async (_, { getState, rejectWithValue, dispatch }) => {
+ const state = getState() as AppState;
+
+ const license = state.register.license;
+ const subnetPassword = state.register.subnetPassword;
+ const subnetEmail = state.register.subnetEmail;
+ const loading = state.register.loading;
+
+ if (loading) {
+ return;
+ }
+ dispatch(setLoading(true));
+ let request: SubnetLoginRequest = {
+ username: subnetEmail,
+ password: subnetPassword,
+ apiKey: license,
+ };
+ api
+ .invoke("POST", "/api/v1/subnet/login", request)
+ .then((resp: SubnetLoginResponse) => {
+ dispatch(setLoading(false));
+ if (resp && resp.registered) {
+ dispatch(resetRegisterForm());
+ dispatch(fetchLicenseInfo());
+ } else if (resp && resp.mfa_token) {
+ dispatch(setSubnetMFAToken(resp.mfa_token));
+ } else if (resp && resp.access_token && resp.organizations.length > 0) {
+ dispatch(setSubnetAccessToken(resp.access_token));
+ dispatch(setSubnetOrganizations(resp.organizations));
+ dispatch(
+ setSelectedSubnetOrganization(
+ resp.organizations[0].accountId.toString()
+ )
+ );
+ }
+ })
+ .catch((err: ErrorResponseHandler) => {
+ dispatch(setErrorSnackMessage(err));
+ dispatch(setLoading(false));
+ dispatch(resetRegisterForm());
+ });
+ }
+);
diff --git a/portal-ui/src/screens/Console/Support/utils.tsx b/portal-ui/src/screens/Console/Support/utils.tsx
index 202a97b18..060cf6111 100644
--- a/portal-ui/src/screens/Console/Support/utils.tsx
+++ b/portal-ui/src/screens/Console/Support/utils.tsx
@@ -58,7 +58,7 @@ export const ClusterRegistered = ({ email }: { email: string }) => {
);
};
-export const ProxyConfiguration = ({ linkClass }: { linkClass: string }) => {
+export const ProxyConfiguration = () => {
const proxyConfigurationCommand =
"mc admin config set {alias} subnet proxy={proxy}";
const [displaySubnetProxy, setDisplaySubnetProxy] = useState(false);
@@ -104,7 +104,10 @@ export const ProxyConfiguration = ({ linkClass }: { linkClass: string }) => {
>
For airgap/firewalled environments it is possible to{" "}
diff --git a/portal-ui/src/store.ts b/portal-ui/src/store.ts
index d222d731c..c77570523 100644
--- a/portal-ui/src/store.ts
+++ b/portal-ui/src/store.ts
@@ -37,6 +37,7 @@ import editTenantAuditLoggingReducer from "./screens/Console/Tenants/TenantDetai
import editTenantSecurityContextReducer from "./screens/Console/Tenants/tenantSecurityContextSlice";
import directPVReducer from "./screens/Console/DirectPV/directPVSlice";
import licenseReducer from "./screens/Console/License/licenseSlice";
+import registerReducer from "./screens/Console/Support/registerSlice";
const rootReducer = combineReducers({
system: systemReducer,
@@ -50,6 +51,7 @@ const rootReducer = combineReducers({
objectBrowser: objectBrowserReducer,
healthInfo: healthInfoReducer,
dashboard: dashboardReducer,
+ register: registerReducer,
// Operator Reducers
tenants: tenantsReducer,
createTenant: createTenantReducer,
diff --git a/restapi/admin_subnet.go b/restapi/admin_subnet.go
index a8c33a684..7a92f346f 100644
--- a/restapi/admin_subnet.go
+++ b/restapi/admin_subnet.go
@@ -62,7 +62,7 @@ func registerSubnetHandlers(api *operations.ConsoleAPI) {
})
// Get subnet info
api.SubnetSubnetInfoHandler = subnetApi.SubnetInfoHandlerFunc(func(params subnetApi.SubnetInfoParams, session *models.Principal) middleware.Responder {
- resp, err := GetSubnetInfoResponse(params)
+ resp, err := GetSubnetInfoResponse(session, params)
if err != nil {
return subnetApi.NewSubnetInfoDefault(int(err.Code)).WithPayload(err)
}
@@ -318,14 +318,47 @@ func subnetRegisterResponse(ctx context.Context, minioClient MinioAdmin, params
return nil
}
-func GetSubnetInfoResponse(params subnetApi.SubnetInfoParams) (*models.License, *models.Error) {
+var ErrSubnetLicenseNotFound = errors.New("license not found")
+
+func GetSubnetInfoResponse(session *models.Principal, params subnetApi.SubnetInfoParams) (*models.License, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
client := &xhttp.Client{
Client: GetConsoleHTTPClient(""),
}
+ // license gets seeded to us by MinIO
+ seededLicense := os.Getenv(EnvSubnetLicense)
+ // if it's missing, we will gracefully fallback to attempt to fetch it from MinIO
+ if seededLicense == "" {
+ mAdmin, err := NewMinioAdminClient(session)
+ if err != nil {
+ return nil, ErrorWithContext(ctx, err)
+ }
+ adminClient := AdminClient{Client: mAdmin}
- licenseInfo, err := subnet.ParseLicense(client, os.Getenv(EnvSubnetLicense))
+ configBytes, err := adminClient.getConfigKV(params.HTTPRequest.Context(), "subnet")
+ if err != nil {
+ return nil, ErrorWithContext(ctx, err)
+ }
+ subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
+ if err != nil {
+ return nil, ErrorWithContext(ctx, err)
+ }
+ // search for licese
+ for _, v := range subSysConfigs {
+ for _, sv := range v.KV {
+ if sv.Key == "license" {
+ seededLicense = sv.Value
+ }
+ }
+ }
+ }
+ // still empty means not found
+ if seededLicense == "" {
+ return nil, ErrorWithContext(ctx, ErrSubnetLicenseNotFound)
+ }
+
+ licenseInfo, err := subnet.ParseLicense(client, seededLicense)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}