Merge pull request #1942 from versity/ben/webui-template

feat: remove /api/gateways webserver endpoint
This commit is contained in:
Ben McClelland
2026-03-09 16:37:37 -07:00
committed by GitHub
10 changed files with 300 additions and 391 deletions

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2026 Versity Software
* This file is licensed under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Shared Tailwind CSS configuration for all pages.
// Edit this file to change the global color palette, typography, etc.
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#002A7A',
50: '#E6EBF4',
100: '#B3C2E0',
200: '#809ACC',
300: '#4D71B8',
400: '#264DA3',
500: '#002A7A',
600: '#002468',
700: '#001D56',
},
accent: {
DEFAULT: '#0076CD',
50: '#E6F3FA',
100: '#B3DCF2',
500: '#0076CD',
600: '#0065AF',
},
charcoal: {
DEFAULT: '#191B2A',
300: '#757884',
400: '#565968',
},
surface: {
DEFAULT: '#F3F8FC',
}
},
fontFamily: {
sans: ['Roboto', 'system-ui', 'sans-serif'],
},
}
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2026 Versity Software
* This file is licensed under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* theme.css — Global shared styles for all pages.
* Page-specific styles remain inline in their respective HTML files.
*/
body {
font-family: 'Roboto', system-ui, sans-serif;
}
/* Sidebar Navigation */
.nav-item {
transition: all 0.15s ease;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.nav-item.active {
background: rgba(0, 118, 205, 0.2);
border-left: 4px solid #0076CD;
}
/* Modal */
.modal-backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
/* Custom Dropdown */
.custom-dropdown {
display: none;
position: absolute;
z-index: 10;
width: 100%;
margin-top: 4px;
background: white;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
max-height: 12rem;
overflow: auto;
}
.custom-dropdown.show {
display: block;
}
.custom-dropdown-item {
padding: 0.75rem 1rem;
cursor: pointer;
color: #191B2A;
transition: background-color 0.15s;
}
.custom-dropdown-item:hover {
background-color: #f9fafb;
}
.custom-dropdown-item.selected {
background-color: rgba(0, 118, 205, 0.1);
color: #0076CD;
}
/* Dropup variant — opens upward */
.custom-dropdown.dropup {
bottom: 100%;
top: auto;
margin-top: 0;
margin-bottom: 4px;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 28px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #e5e7eb;
transition: 0.3s;
border-radius: 28px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #0076CD;
}
input:checked + .toggle-slider:before {
transform: translateX(32px);
}
.toggle-label {
font-size: 0.875rem;
color: #565968;
font-weight: 500;
}
/* Login Page */
.input-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #808080;
}
.input-with-icon {
padding-left: 44px;
}
.password-toggle {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
color: #808080;
cursor: pointer;
background: none;
border: none;
padding: 4px;
}
.password-toggle:hover {
color: #002A7A;
}
.advanced-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.75rem 0;
margin: 0.5rem 0;
background: none;
border: none;
font: inherit;
color: inherit;
text-align: left;
width: 100%;
}
.advanced-toggle:hover {
opacity: 0.8;
}
.advanced-toggle-carat {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
transition: transform 0.3s ease;
}
.advanced-toggle.expanded .advanced-toggle-carat {
transform: rotate(90deg);
}
.advanced-toggle-label {
font-weight: 500;
color: #565968;
cursor: pointer;
}
.advanced-options {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.advanced-options.show {
max-height: 500px;
}

View File

@@ -21,67 +21,10 @@ under the License.
<title>VersityGW Admin - Buckets</title>
<script src="assets/js/crypto-js.min.js"></script>
<script src="assets/css/tailwind.js"></script>
<script src="assets/css/tailwind-config.js"></script>
<link rel="stylesheet" href="assets/css/fonts.css">
<link rel="stylesheet" href="assets/css/theme.css">
<link rel="icon" type="image/png" href="assets/images/favicon.png">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#002A7A', 50: '#E6EBF4', 500: '#002A7A', 600: '#002468' },
accent: { DEFAULT: '#0076CD', 50: '#E6F3FA', 500: '#0076CD', 600: '#0065AF' },
charcoal: { DEFAULT: '#191B2A', 300: '#757884', 400: '#565968' },
surface: { DEFAULT: '#F3F8FC' }
},
fontFamily: { sans: ['Roboto', 'system-ui', 'sans-serif'] },
}
}
}
</script>
<style>
body { font-family: 'Roboto', system-ui, sans-serif; }
.nav-item { transition: all 0.15s ease; }
.nav-item:hover { background: rgba(255,255,255,0.1); }
.nav-item.active { background: rgba(0, 118, 205, 0.2); border-left: 4px solid #0076CD; }
.modal-backdrop { background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); }
/* Custom dropdown styles */
.custom-dropdown {
display: none;
position: absolute;
z-index: 10;
width: 100%;
margin-top: 4px;
background: white;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
max-height: 12rem;
overflow: auto;
}
.custom-dropdown.show {
display: block;
}
.custom-dropdown-item {
padding: 0.75rem 1rem;
cursor: pointer;
color: #191B2A;
transition: background-color 0.15s;
}
.custom-dropdown-item:hover {
background-color: #f9fafb;
}
.custom-dropdown-item.selected {
background-color: rgba(0, 118, 205, 0.1);
color: #0076CD;
}
/* Dropup variant - opens upward */
.custom-dropdown.dropup {
bottom: 100%;
top: auto;
margin-top: 0;
margin-bottom: 4px;
}
</style>
</head>
<body class="min-h-screen bg-surface">
<script src="js/api.js"></script>

View File

@@ -21,29 +21,10 @@ under the License.
<title>VersityGW Admin - Dashboard</title>
<script src="assets/js/crypto-js.min.js"></script>
<script src="assets/css/tailwind.js"></script>
<script src="assets/css/tailwind-config.js"></script>
<link rel="stylesheet" href="assets/css/fonts.css">
<link rel="stylesheet" href="assets/css/theme.css">
<link rel="icon" type="image/png" href="assets/images/favicon.png">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#002A7A', 50: '#E6EBF4', 500: '#002A7A', 600: '#002468' },
accent: { DEFAULT: '#0076CD', 50: '#E6F3FA', 500: '#0076CD', 600: '#0065AF' },
charcoal: { DEFAULT: '#191B2A', 300: '#757884', 400: '#565968' },
surface: { DEFAULT: '#F3F8FC' }
},
fontFamily: { sans: ['Roboto', 'system-ui', 'sans-serif'] },
}
}
}
</script>
<style>
body { font-family: 'Roboto', system-ui, sans-serif; }
.nav-item { transition: all 0.15s ease; }
.nav-item:hover { background: rgba(255,255,255,0.1); }
.nav-item.active { background: rgba(0, 118, 205, 0.2); border-left: 4px solid #0076CD; }
</style>
</head>
<body class="min-h-screen bg-surface">
<script src="js/api.js"></script>

View File

@@ -21,28 +21,11 @@ under the License.
<title>VersityGW - Explorer</title>
<script src="assets/js/crypto-js.min.js"></script>
<script src="assets/css/tailwind.js"></script>
<script src="assets/css/tailwind-config.js"></script>
<link rel="stylesheet" href="assets/css/fonts.css">
<link rel="stylesheet" href="assets/css/theme.css">
<link rel="icon" type="image/png" href="assets/images/favicon.png">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#002A7A', 50: '#E6EBF4', 500: '#002A7A', 600: '#002468' },
accent: { DEFAULT: '#0076CD', 50: '#E6F3FA', 500: '#0076CD', 600: '#0065AF' },
charcoal: { DEFAULT: '#191B2A', 300: '#757884', 400: '#565968' },
surface: { DEFAULT: '#F3F8FC' }
},
fontFamily: { sans: ['Roboto', 'system-ui', 'sans-serif'] },
}
}
}
</script>
<style>
body { font-family: 'Roboto', system-ui, sans-serif; }
.nav-item { transition: all 0.15s ease; }
.nav-item:hover { background: rgba(255,255,255,0.1); }
.nav-item.active { background: rgba(0, 118, 205, 0.2); border-left: 4px solid #0076CD; }
.drop-zone-active { background: rgba(0, 118, 205, 0.1); border-color: #0076CD; }
.file-row:hover { background: #f9fafb; }
.file-row.selected { background: #e6f3fa; }

View File

@@ -21,191 +21,10 @@ under the License.
<title>VersityGW Admin - Login</title>
<script src="assets/js/crypto-js.min.js"></script>
<script src="assets/css/tailwind.js"></script>
<script src="assets/css/tailwind-config.js"></script>
<link rel="stylesheet" href="assets/css/fonts.css">
<link rel="stylesheet" href="assets/css/theme.css">
<link rel="icon" type="image/png" href="assets/images/favicon.png">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#002A7A',
50: '#E6EBF4',
100: '#B3C2E0',
200: '#809ACC',
300: '#4D71B8',
400: '#264DA3',
500: '#002A7A',
600: '#002468',
700: '#001D56',
},
accent: {
DEFAULT: '#0076CD',
50: '#E6F3FA',
100: '#B3DCF2',
500: '#0076CD',
600: '#0065AF',
},
charcoal: {
DEFAULT: '#191B2A',
300: '#757884',
400: '#565968',
},
surface: {
DEFAULT: '#F3F8FC',
}
},
fontFamily: {
sans: ['Roboto', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style>
body { font-family: 'Roboto', system-ui, sans-serif; }
.input-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #808080;
}
.input-with-icon {
padding-left: 44px;
}
.password-toggle {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
color: #808080;
cursor: pointer;
background: none;
border: none;
padding: 4px;
}
.password-toggle:hover {
color: #002A7A;
}
.advanced-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.75rem 0;
margin: 0.5rem 0;
background: none;
border: none;
font: inherit;
color: inherit;
text-align: left;
width: 100%;
}
.advanced-toggle:hover {
opacity: 0.8;
}
.advanced-toggle-carat {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
transition: transform 0.3s ease;
}
.advanced-toggle.expanded .advanced-toggle-carat {
transform: rotate(90deg);
}
.advanced-toggle-label {
font-weight: 500;
color: #565968;
cursor: pointer;
}
.advanced-options {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.advanced-options.show {
max-height: 500px;
}
/* Custom dropdown styles */
.custom-dropdown {
display: none;
position: absolute;
z-index: 10;
width: 100%;
margin-top: 4px;
background: white;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
max-height: 12rem;
overflow: auto;
}
.custom-dropdown.show {
display: block;
}
.custom-dropdown-item {
padding: 0.75rem 1rem;
cursor: pointer;
color: #191B2A;
transition: background-color 0.15s;
}
.custom-dropdown-item:hover {
background-color: #f9fafb;
}
.custom-dropdown-item.selected {
background-color: rgba(0, 118, 205, 0.1);
color: #0076CD;
}
/* Toggle Switch Styles */
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 28px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #e5e7eb;
transition: 0.3s;
border-radius: 28px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #0076CD;
}
input:checked + .toggle-slider:before {
transform: translateX(32px);
}
.toggle-label {
font-size: 0.875rem;
color: #565968;
font-weight: 500;
}
</style>
</head>
<body class="min-h-screen bg-gradient-to-br from-surface to-white flex items-center justify-center p-4">
<script src="js/api.js"></script>
@@ -405,7 +224,7 @@ under the License.
}
// ============================================
// Configured Gateways (from vgwmgr CLI)
// Configured Gateways
// ============================================
let configuredGateways = [];
let configuredAdminGateways = [];
@@ -434,24 +253,18 @@ under the License.
return out;
}
async function loadConfiguredGateways() {
try {
const res = await fetch('/api/gateways', { cache: 'no-store' });
if (!res.ok) return { gateways: [], adminGateways: [], defaultRegion: null };
const data = await res.json();
if (!data || !Array.isArray(data.gateways)) return { gateways: [], adminGateways: [], defaultRegion: null };
return {
gateways: data.gateways,
adminGateways: data.adminGateways || data.gateways || [],
defaultRegion: normalizeRegion(typeof data.defaultRegion === 'string' ? data.defaultRegion : null),
};
} catch (e) {
return { gateways: [], adminGateways: [], defaultRegion: null };
}
function loadConfiguredGateways() {
const cfg = window.__VGWCONFIG__ || {};
if (!Array.isArray(cfg.gateways)) return { gateways: [], adminGateways: [], defaultRegion: null };
return {
gateways: cfg.gateways,
adminGateways: cfg.adminGateways || cfg.gateways || [],
defaultRegion: normalizeRegion(typeof cfg.defaultRegion === 'string' ? cfg.defaultRegion : null),
};
}
async function initConfiguredGateways() {
const cfg = await loadConfiguredGateways();
function initConfiguredGateways() {
const cfg = loadConfiguredGateways();
configuredGateways = uniqNonEmpty(cfg.gateways);
configuredAdminGateways = uniqNonEmpty(cfg.adminGateways);
configuredDefaultRegion = cfg.defaultRegion;

View File

@@ -291,15 +291,6 @@ class VersityAPI {
// User Context Methods (ROOT user detection)
// ============================================
/**
* Detect ROOT user and get accessible gateways
* Calls /api/detect-root endpoint to check if credentials match ROOT config
* @param {string} accessKey - Access key to check
* @param {string} secretKey - Secret key to check
* @returns {Object} - { userType: 'root'|'user', matchingGateways: [...] }
*/
// detectRootUser removed - single gateway mode
/**
* Store user type and accessible gateways in session
* @param {string} userType - 'root' | 'admin' | 'user'
@@ -318,26 +309,6 @@ class VersityAPI {
this._userType = sessionStorage.getItem('vgw_user_type') || 'user';
}
/**
* Check if current user is ROOT user (has ROOT credentials matching gateway configs)
* @returns {boolean}
*/
// isRootUser removed
/**
* Get list of gateways accessible to current user
* Only populated for ROOT users
* @returns {Array} - Array of { name, port, endpoint, region, status }
*/
// getAccessibleGateways removed
/**
* Check if authenticated
*/
isAuthenticated() {
return this.credentials !== null && this.s3Endpoint !== null;
}
/**
* Check if user has admin privileges
*/

View File

@@ -0,0 +1 @@
window.__VGWCONFIG__ = {{.ConfigJSON}};

View File

@@ -21,60 +21,10 @@ under the License.
<title>VersityGW Admin - Users</title>
<script src="assets/js/crypto-js.min.js"></script>
<script src="assets/css/tailwind.js"></script>
<script src="assets/css/tailwind-config.js"></script>
<link rel="stylesheet" href="assets/css/fonts.css">
<link rel="stylesheet" href="assets/css/theme.css">
<link rel="icon" type="image/png" href="assets/images/favicon.png">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#002A7A', 50: '#E6EBF4', 500: '#002A7A', 600: '#002468' },
accent: { DEFAULT: '#0076CD', 50: '#E6F3FA', 500: '#0076CD', 600: '#0065AF' },
charcoal: { DEFAULT: '#191B2A', 300: '#757884', 400: '#565968' },
surface: { DEFAULT: '#F3F8FC' }
},
fontFamily: { sans: ['Roboto', 'system-ui', 'sans-serif'] },
}
}
}
</script>
<style>
body { font-family: 'Roboto', system-ui, sans-serif; }
.nav-item { transition: all 0.15s ease; }
.nav-item:hover { background: rgba(255,255,255,0.1); }
.nav-item.active { background: rgba(0, 118, 205, 0.2); border-left: 4px solid #0076CD; }
.modal-backdrop { background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); }
/* Custom dropdown styles */
.custom-dropdown {
display: none;
position: absolute;
z-index: 10;
width: 100%;
margin-top: 4px;
background: white;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
max-height: 12rem;
overflow: auto;
}
.custom-dropdown.show {
display: block;
}
.custom-dropdown-item {
padding: 0.75rem 1rem;
cursor: pointer;
color: #191B2A;
transition: background-color 0.15s;
}
.custom-dropdown-item:hover {
background-color: #f9fafb;
}
.custom-dropdown-item.selected {
background-color: rgba(0, 118, 205, 0.1);
color: #0076CD;
}
</style>
</head>
<body class="min-h-screen bg-surface">
<script src="js/api.js"></script>

View File

@@ -15,9 +15,11 @@
package webui
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/filesystem"
@@ -96,32 +98,48 @@ func (s *Server) setupMiddleware() {
// setupRoutes configures all routes
func (s *Server) setupRoutes() {
// API endpoint to get configured gateways
s.app.Get("/api/gateways", s.handleGetGateways)
// Serve index.html
s.app.Get("/", s.handleIndexHTML)
s.app.Get("/index.html", s.handleIndexHTML)
// Serve embedded static files from web/
s.app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(webFS),
PathPrefix: "web",
Index: "index.html",
NotFoundFile: "index.html", // SPA fallback
Browse: false,
Root: http.FS(webFS),
PathPrefix: "web",
Browse: false,
}))
}
// handleGetGateways returns the configured gateway URLs (both S3 and Admin)
func (s *Server) handleGetGateways(c *fiber.Ctx) error {
// handleIndexHTML serves index.html with server config injected as an inline script.
func (s *Server) handleIndexHTML(c *fiber.Ctx) error {
data, err := webFiles.ReadFile("web/index.html")
if err != nil {
return fiber.ErrInternalServerError
}
adminGateways := s.config.AdminGateways
if len(adminGateways) == 0 {
// Fallback to S3 gateways if admin gateways not configured
adminGateways = s.config.Gateways
}
return c.JSON(fiber.Map{
configJSON, err := json.Marshal(map[string]interface{}{
"gateways": s.config.Gateways,
"adminGateways": adminGateways,
"defaultRegion": s.config.Region,
})
if err != nil {
return fiber.ErrInternalServerError
}
html := strings.Replace(
string(data),
"</head>",
"<script>window.__VGWCONFIG__ = "+string(configJSON)+";</script></head>",
1,
)
c.Set("Content-Type", "text/html; charset=utf-8")
return c.SendString(html)
}
// ServeMultiPort creates listeners for multiple address specifications and serves