Define base for assets and support for sub path (#1247)
* Added correct mime type to files * Define Base for Assets Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> * lint Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> * Make things relative Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> * hop styling Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
@@ -185,7 +185,8 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) {
|
||||
responseWriter.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
targetURL.Path = strings.Replace(req.URL.Path, fmt.Sprintf("/api/proxy/%s/%s", tenant.Namespace, tenant.Name), "", -1)
|
||||
tenantBase := fmt.Sprintf("/api/proxy/%s/%s", tenant.Namespace, tenant.Name)
|
||||
targetURL.Path = strings.Replace(req.URL.Path, tenantBase, "", -1)
|
||||
|
||||
proxiedCookie := &http.Cookie{
|
||||
Name: "token",
|
||||
@@ -207,8 +208,17 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) {
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
|
||||
// are we proxying something with cp=y? (console proxy) then add cpb (console proxy base) so the console
|
||||
// on the other side updates the <base href="" /> to this value overriding sub path or root
|
||||
if v := req.URL.Query().Get("cp"); v == "y" {
|
||||
q := req.URL.Query()
|
||||
q.Add("cpb", tenantBase)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
// copy query params
|
||||
targetURL.RawQuery = req.URL.Query().Encode()
|
||||
|
||||
proxRequest, err := http.NewRequest(req.Method, targetURL.String(), req.Body)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
|
||||
@@ -2,51 +2,52 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<base href="/" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#081C42"
|
||||
media="(prefers-color-scheme: light)"
|
||||
name="theme-color"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#081C42"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
name="theme-color"
|
||||
/>
|
||||
<meta name="description" content="MinIO Console" />
|
||||
<meta content="MinIO Console" name="description" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link href="%PUBLIC_URL%/styles/root-styles.css" rel="stylesheet" />
|
||||
<link
|
||||
href="%PUBLIC_URL%/apple-icon-180x180.png"
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="%PUBLIC_URL%/apple-icon-180x180.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
||||
rel="icon"
|
||||
sizes="32x32"
|
||||
type="image/png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="96x96"
|
||||
href="%PUBLIC_URL%/favicon-96x96.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
sizes="96x96"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="%PUBLIC_URL%/safari-pinned-tab.svg"
|
||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||
rel="icon"
|
||||
sizes="16x16"
|
||||
type="image/png"
|
||||
/>
|
||||
<link href="%PUBLIC_URL%/manifest.json" rel="manifest" />
|
||||
<link
|
||||
color="#3a4e54"
|
||||
href="%PUBLIC_URL%/safari-pinned-tab.svg"
|
||||
rel="mask-icon"
|
||||
/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
@@ -71,22 +72,22 @@
|
||||
"
|
||||
cx="44"
|
||||
cy="44"
|
||||
r="20.2"
|
||||
fill="none"
|
||||
r="20.2"
|
||||
stroke-width="3.6"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,8 +20,8 @@ import { connect } from "react-redux";
|
||||
import { AppState } from "./store";
|
||||
import {
|
||||
consoleOperatorMode,
|
||||
userLoggedIn,
|
||||
setDistributedMode,
|
||||
userLoggedIn,
|
||||
} from "./actions";
|
||||
import api from "./common/api";
|
||||
import { saveSessionResponse } from "./screens/Console/actions";
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
import request from "superagent";
|
||||
import get from "lodash/get";
|
||||
import { clearSession } from "../utils";
|
||||
import { baseUrl } from "../../history";
|
||||
import { ErrorResponseHandler } from "../types";
|
||||
|
||||
export class API {
|
||||
invoke(method: string, url: string, data?: object) {
|
||||
const targetURL = `${baseUrl}${url}`.replace(/\/\//g, "/");
|
||||
let targetURL = url;
|
||||
if (targetURL[0] === "/") {
|
||||
targetURL = targetURL.substr(1);
|
||||
}
|
||||
return request(method, targetURL)
|
||||
.send(data)
|
||||
.then((res) => res.body)
|
||||
|
||||
@@ -3,13 +3,4 @@ import { BrowserHistoryBuildOptions } from "history/createBrowserHistory";
|
||||
|
||||
let browserHistoryOpts: BrowserHistoryBuildOptions = {};
|
||||
|
||||
export let baseUrl = "";
|
||||
|
||||
if (`${window.location.pathname}`.startsWith("/api/proxy/")) {
|
||||
// grab from api to the tenant name (/api/proxy/namespace/tenant)
|
||||
const urlParts = `${window.location.pathname}`.split("/").slice(0, 5);
|
||||
browserHistoryOpts.basename = urlParts.join("/");
|
||||
baseUrl = `${urlParts.join("/")}/`;
|
||||
}
|
||||
|
||||
export default createBrowserHistory(browserHistoryOpts);
|
||||
|
||||
@@ -66,7 +66,6 @@ import RewindEnable from "./RewindEnable";
|
||||
|
||||
import DeleteMultipleObjects from "./DeleteMultipleObjects";
|
||||
import PreviewFileModal from "../Preview/PreviewFileModal";
|
||||
import { baseUrl } from "../../../../../../history";
|
||||
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
|
||||
import AddFolderIcon from "../../../../../../icons/AddFolderIcon";
|
||||
import HistoryIcon from "../../../../../../icons/HistoryIcon";
|
||||
@@ -597,7 +596,7 @@ const ListObjects = ({
|
||||
}
|
||||
e.preventDefault();
|
||||
let files = e.target.files;
|
||||
let uploadUrl = `${baseUrl}/api/v1/buckets/${bucketName}/objects/upload`;
|
||||
let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
|
||||
if (encodedPath !== "") {
|
||||
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const PageHeader = ({
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
className={classes.headerContainer}
|
||||
className={`${classes.headerContainer} page-header`}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
>
|
||||
|
||||
@@ -95,9 +95,7 @@ const Dashboard = React.lazy(() => import("./Dashboard/Dashboard"));
|
||||
const Account = React.lazy(() => import("./Account/Account"));
|
||||
const Users = React.lazy(() => import("./Users/Users"));
|
||||
const Groups = React.lazy(() => import("./Groups/Groups"));
|
||||
const ConfigurationMain = React.lazy(
|
||||
() => import("./Configurations/ConfigurationMain")
|
||||
);
|
||||
|
||||
const TenantDetails = React.lazy(
|
||||
() => import("./Tenants/TenantDetails/TenantDetails")
|
||||
);
|
||||
|
||||
@@ -461,7 +461,7 @@ const Menu = ({
|
||||
component: NavLink,
|
||||
to: "/license",
|
||||
name: "License",
|
||||
icon: <LicenseIcon />,
|
||||
icon: LicenseIcon,
|
||||
},
|
||||
{
|
||||
...documentation,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import { Link } from "react-router-dom";
|
||||
import { CircularProgress, IconButton } from "@mui/material";
|
||||
import { Box, CircularProgress, IconButton } from "@mui/material";
|
||||
import PageHeader from "../../../Common/PageHeader/PageHeader";
|
||||
import { containerForHeader } from "../../../Common/FormComponents/common/styleLibrary";
|
||||
import ExitToAppIcon from "@mui/icons-material/ExitToApp";
|
||||
@@ -46,8 +46,8 @@ const styles = (theme: Theme) =>
|
||||
divContainer: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 77,
|
||||
height: "calc(100vh - 77px)",
|
||||
top: 80,
|
||||
height: "calc(100vh - 81px)",
|
||||
width: "100%",
|
||||
},
|
||||
loader: {
|
||||
@@ -55,6 +55,11 @@ const styles = (theme: Theme) =>
|
||||
margin: "auto",
|
||||
marginTop: 80,
|
||||
},
|
||||
|
||||
pageHeader: {
|
||||
borderBottom: "1px solid #dedede",
|
||||
},
|
||||
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
|
||||
@@ -66,72 +71,76 @@ const Hop = ({ classes, match }: IHopSimple) => {
|
||||
const consoleFrame = React.useRef<HTMLIFrameElement>(null);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader
|
||||
label={
|
||||
<Fragment>
|
||||
<Link to={"/tenants"} className={classes.breadcrumLink}>
|
||||
Tenants
|
||||
</Link>
|
||||
{` > `}
|
||||
<Link
|
||||
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}`}
|
||||
className={classes.breadcrumLink}
|
||||
>
|
||||
{match.params["tenantName"]}
|
||||
</Link>
|
||||
{` > Management`}
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Refresh List"
|
||||
component="span"
|
||||
onClick={() => {
|
||||
if (
|
||||
consoleFrame !== null &&
|
||||
consoleFrame.current !== null &&
|
||||
consoleFrame.current.contentDocument !== null
|
||||
) {
|
||||
const loc =
|
||||
consoleFrame.current.contentDocument.location.toString();
|
||||
<Fragment>
|
||||
<Box className={classes.pageHeader}>
|
||||
<PageHeader
|
||||
label={
|
||||
<Fragment>
|
||||
<Link to={"/tenants"} className={classes.breadcrumLink}>
|
||||
Tenants
|
||||
</Link>
|
||||
{` > `}
|
||||
<Link
|
||||
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}`}
|
||||
className={classes.breadcrumLink}
|
||||
>
|
||||
{match.params["tenantName"]}
|
||||
</Link>
|
||||
{` > Management`}
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Refresh List"
|
||||
component="span"
|
||||
onClick={() => {
|
||||
if (
|
||||
consoleFrame !== null &&
|
||||
consoleFrame.current !== null &&
|
||||
consoleFrame.current.contentDocument !== null
|
||||
) {
|
||||
const loc =
|
||||
consoleFrame.current.contentDocument.location.toString();
|
||||
|
||||
let add = "&";
|
||||
let add = "&";
|
||||
|
||||
if (loc.indexOf("?") < 0) {
|
||||
add = `?`;
|
||||
if (loc.indexOf("?") < 0) {
|
||||
add = `?`;
|
||||
}
|
||||
|
||||
if (loc.indexOf("cp=y") < 0) {
|
||||
const next = `${loc}${add}cp=y`;
|
||||
consoleFrame.current.contentDocument.location.replace(
|
||||
next
|
||||
);
|
||||
} else {
|
||||
consoleFrame.current.contentDocument.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
if (loc.indexOf("cp=y") < 0) {
|
||||
const next = `${loc}${add}cp=y`;
|
||||
consoleFrame.current.contentDocument.location.replace(next);
|
||||
} else {
|
||||
consoleFrame.current.contentDocument.location.reload();
|
||||
}
|
||||
}
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Refresh List"
|
||||
component="span"
|
||||
onClick={() => {
|
||||
history.push(
|
||||
`/namespaces/${tenantNamespace}/tenants/${tenantName}`
|
||||
);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<ExitToAppIcon />
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Refresh List"
|
||||
component="span"
|
||||
onClick={() => {
|
||||
history.push(
|
||||
`/namespaces/${tenantNamespace}/tenants/${tenantName}`
|
||||
);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<ExitToAppIcon />
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<div className={classes.divContainer}>
|
||||
{loading && (
|
||||
<div className={classes.loader}>
|
||||
@@ -148,7 +157,7 @@ const Hop = ({ classes, match }: IHopSimple) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,14 +21,16 @@ package restapi
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
@@ -50,6 +52,9 @@ var additionalServerFlags = struct {
|
||||
CertsDir string `long:"certs-dir" description:"path to certs directory" env:"CONSOLE_CERTS_DIR"`
|
||||
}{}
|
||||
|
||||
var subPath = "/"
|
||||
var subPathOnce sync.Once
|
||||
|
||||
func configureFlags(api *operations.ConsoleAPI) {
|
||||
api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{
|
||||
{
|
||||
@@ -251,8 +256,6 @@ func FileServerMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
var reHrefIndex = regexp.MustCompile(`(?m)((href|src)="(.\/).*?")`)
|
||||
|
||||
type notFoundRedirectRespWr struct {
|
||||
http.ResponseWriter // We embed http.ResponseWriter
|
||||
status int
|
||||
@@ -274,9 +277,15 @@ func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) {
|
||||
|
||||
func handleSPA(w http.ResponseWriter, r *http.Request) {
|
||||
basePath := "/"
|
||||
// For SPA mode we will replace relative paths with absolute unless we receive query param cp=y
|
||||
// For SPA mode we will replace root base with a sub path if configured unless we received cp=y and cpb=/NEW/BASE
|
||||
if v := r.URL.Query().Get("cp"); v == "y" {
|
||||
basePath = "./"
|
||||
if base := r.URL.Query().Get("cpb"); base != "" {
|
||||
// make sure the subpath has a trailing slash
|
||||
if !strings.HasSuffix(base, "/") {
|
||||
base = fmt.Sprintf("%s/", base)
|
||||
}
|
||||
basePath = base
|
||||
}
|
||||
}
|
||||
|
||||
indexPage, err := portal_ui.GetStaticAssets().Open("build/index.html")
|
||||
@@ -291,16 +300,21 @@ func handleSPA(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if basePath != "./" {
|
||||
indexPageStr := string(indexPageBytes)
|
||||
for _, match := range reHrefIndex.FindAllStringSubmatch(indexPageStr, -1) {
|
||||
toReplace := strings.Replace(match[1], match[3], basePath, 1)
|
||||
indexPageStr = strings.Replace(indexPageStr, match[1], toReplace, 1)
|
||||
}
|
||||
indexPageBytes = []byte(indexPageStr)
|
||||
// if we have a seeded basePath. This should override CONSOLE_SUBPATH every time, thus the `if else`
|
||||
if basePath != "/" {
|
||||
indexPageBytes = replaceBaseInIndex(indexPageBytes, basePath)
|
||||
// if we have a custom subpath replace it in
|
||||
} else if getSubPath() != "/" {
|
||||
indexPageBytes = replaceBaseInIndex(indexPageBytes, getSubPath())
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
mimeType := mimedb.TypeByExtension(filepath.Ext(r.URL.Path))
|
||||
|
||||
if mimeType == "application/octet-stream" {
|
||||
mimeType = "text/html"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(indexPageBytes))
|
||||
}
|
||||
|
||||
@@ -335,3 +349,24 @@ func configureServer(s *http.Server, _, _ string) {
|
||||
// Turn-off random logging by Go net/http
|
||||
s.ErrorLog = log.New(&nullWriter{}, "", 0)
|
||||
}
|
||||
|
||||
func getSubPath() string {
|
||||
subPathOnce.Do(func() {
|
||||
if v := os.Getenv("CONSOLE_SUBPATH"); v != "" {
|
||||
// make sure the subpath has a trailing slash
|
||||
if !strings.HasSuffix(v, "/") {
|
||||
v = fmt.Sprintf("%s/", v)
|
||||
}
|
||||
subPath = v
|
||||
}
|
||||
})
|
||||
return subPath
|
||||
}
|
||||
|
||||
func replaceBaseInIndex(indexPageBytes []byte, basePath string) []byte {
|
||||
indexPageStr := string(indexPageBytes)
|
||||
newBase := fmt.Sprintf("<base href=\"%s\"/>", basePath)
|
||||
indexPageStr = strings.Replace(indexPageStr, "<base href=\"/\"/>", newBase, 1)
|
||||
indexPageBytes = []byte(indexPageStr)
|
||||
return indexPageBytes
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user