mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
refactor(cloudcp): own Pulse Account route config in bootstrap
This commit is contained in:
@@ -8,6 +8,10 @@ if (bootstrapEl) {
|
||||
}
|
||||
}
|
||||
var LICENSE_API_BASE = portalBootstrap.commercial_api_base_url || '';
|
||||
var PORTAL_PATH = portalBootstrap.portal_path || '/portal';
|
||||
var LOGOUT_PATH = portalBootstrap.logout_path || '/auth/logout';
|
||||
var ACCOUNT_API_BASE_PATH = portalBootstrap.account_api_base_path || '/api/accounts';
|
||||
var PORTAL_API_BASE_PATH = portalBootstrap.portal_api_base_path || '/api/portal';
|
||||
|
||||
function showToast(msg, isError) {
|
||||
var t = document.getElementById('toast');
|
||||
@@ -21,9 +25,9 @@ document.getElementById('logout-btn').onclick = async function() {
|
||||
this.disabled = true;
|
||||
this.textContent = 'Signing out…';
|
||||
try {
|
||||
await fetch('/auth/logout', { method: 'POST' });
|
||||
await fetch(LOGOUT_PATH, { method: 'POST' });
|
||||
} catch(_) {}
|
||||
window.location.href = '/portal';
|
||||
window.location.href = PORTAL_PATH;
|
||||
};
|
||||
|
||||
function toggleServicePanel(panelID) {
|
||||
@@ -379,7 +383,7 @@ async function createWorkspace(accountID) {
|
||||
var spinner = document.getElementById('ws-spinner-' + accountID);
|
||||
spinner.style.display = 'block';
|
||||
try {
|
||||
var resp = await fetch('/api/accounts/' + accountID + '/tenants', {
|
||||
var resp = await fetch(ACCOUNT_API_BASE_PATH + '/' + accountID + '/tenants', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ display_name: name })
|
||||
@@ -404,7 +408,7 @@ function suspendOrDelete(evt, accountID, tenantID, state, name) {
|
||||
if (!confirm(action + ' workspace "' + name + '"?')) return;
|
||||
var method = state === 'active' ? 'PATCH' : 'DELETE';
|
||||
var body = state === 'active' ? JSON.stringify({ state: 'suspended' }) : undefined;
|
||||
fetch('/api/accounts/' + accountID + '/tenants/' + tenantID, {
|
||||
fetch(ACCOUNT_API_BASE_PATH + '/' + accountID + '/tenants/' + tenantID, {
|
||||
method: method,
|
||||
headers: body ? { 'Content-Type': 'application/json' } : {},
|
||||
body: body
|
||||
@@ -422,7 +426,7 @@ function suspendOrDelete(evt, accountID, tenantID, state, name) {
|
||||
|
||||
async function openBilling(accountID) {
|
||||
try {
|
||||
var r = await fetch('/api/portal/billing?account_id=' + encodeURIComponent(accountID), { method: 'POST' });
|
||||
var r = await fetch(PORTAL_API_BASE_PATH + '/billing?account_id=' + encodeURIComponent(accountID), { method: 'POST' });
|
||||
if (!r.ok) {
|
||||
var err = await r.json().catch(function() { return {}; });
|
||||
showToast((err && err.error) || 'Failed to open billing portal.', true);
|
||||
@@ -464,7 +468,7 @@ async function loadTeam(accountID) {
|
||||
var isOwner = actorRole === 'owner';
|
||||
setTbodyMessage(tbody, 'Loading\u2026', false);
|
||||
try {
|
||||
var r = await fetch('/api/accounts/' + encodeURIComponent(accountID) + '/members');
|
||||
var r = await fetch(ACCOUNT_API_BASE_PATH + '/' + encodeURIComponent(accountID) + '/members');
|
||||
if (!r.ok) { setTbodyMessage(tbody, 'Failed to load team.', true); return; }
|
||||
var members = await r.json();
|
||||
if (!members || members.length === 0) {
|
||||
@@ -520,7 +524,7 @@ async function inviteMember(accountID) {
|
||||
var email = emailEl.value.trim();
|
||||
if (!email) { emailEl.focus(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/accounts/' + encodeURIComponent(accountID) + '/members', {
|
||||
var r = await fetch(ACCOUNT_API_BASE_PATH + '/' + encodeURIComponent(accountID) + '/members', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email, role: roleEl.value })
|
||||
@@ -541,7 +545,7 @@ async function inviteMember(accountID) {
|
||||
|
||||
async function changeRole(accountID, userID, newRole) {
|
||||
try {
|
||||
var r = await fetch('/api/accounts/' + encodeURIComponent(accountID) + '/members/' + encodeURIComponent(userID), {
|
||||
var r = await fetch(ACCOUNT_API_BASE_PATH + '/' + encodeURIComponent(accountID) + '/members/' + encodeURIComponent(userID), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: newRole })
|
||||
@@ -558,7 +562,7 @@ async function changeRole(accountID, userID, newRole) {
|
||||
async function removeMember(accountID, userID, email) {
|
||||
if (!confirm('Remove ' + email + ' from this account?')) return;
|
||||
try {
|
||||
var r = await fetch('/api/accounts/' + encodeURIComponent(accountID) + '/members/' + encodeURIComponent(userID), {
|
||||
var r = await fetch(ACCOUNT_API_BASE_PATH + '/' + encodeURIComponent(accountID) + '/members/' + encodeURIComponent(userID), {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (r.status === 409) { showToast('Cannot remove last owner.', true); return; }
|
||||
|
||||
@@ -729,6 +729,10 @@ func TestPortalPageTemplate_AccountServicesRendered(t *testing.T) {
|
||||
"Pulse Account",
|
||||
`id="pulse-account-bootstrap"`,
|
||||
`"commercial_api_base_url":"https://license.pulserelay.pro"`,
|
||||
`"portal_path":"/portal"`,
|
||||
`"logout_path":"/auth/logout"`,
|
||||
`"account_api_base_path":"/api/accounts"`,
|
||||
`"portal_api_base_path":"/api/portal"`,
|
||||
`"email":"owner@example.com"`,
|
||||
`"accounts":[{"id":"a_test"`,
|
||||
"Other account services",
|
||||
@@ -758,6 +762,9 @@ func TestPortalPageTemplate_AccountServicesRendered(t *testing.T) {
|
||||
if strings.Contains(html, `const LICENSE_API_BASE = 'https://license.pulserelay.pro';`) {
|
||||
t.Errorf("expected commercial API base URL to be renderer-owned, not hardcoded in the asset")
|
||||
}
|
||||
if strings.Contains(html, `await fetch('/auth/logout'`) {
|
||||
t.Errorf("expected portal paths to be renderer-owned, not hardcoded in the asset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPortalBootstrapJSON_Contract(t *testing.T) {
|
||||
@@ -795,6 +802,18 @@ func TestBuildPortalBootstrapJSON_Contract(t *testing.T) {
|
||||
if got := payload["commercial_api_base_url"]; got != "https://license.pulserelay.pro" {
|
||||
t.Fatalf("commercial_api_base_url = %#v", got)
|
||||
}
|
||||
if got := payload["portal_path"]; got != "/portal" {
|
||||
t.Fatalf("portal_path = %#v", got)
|
||||
}
|
||||
if got := payload["logout_path"]; got != "/auth/logout" {
|
||||
t.Fatalf("logout_path = %#v", got)
|
||||
}
|
||||
if got := payload["account_api_base_path"]; got != "/api/accounts" {
|
||||
t.Fatalf("account_api_base_path = %#v", got)
|
||||
}
|
||||
if got := payload["portal_api_base_path"]; got != "/api/portal" {
|
||||
t.Fatalf("portal_api_base_path = %#v", got)
|
||||
}
|
||||
|
||||
accounts, ok := payload["accounts"].([]any)
|
||||
if !ok || len(accounts) != 1 {
|
||||
|
||||
@@ -40,6 +40,10 @@ type portalPageData struct {
|
||||
PublicSiteURL string
|
||||
SupportEmail string
|
||||
CommercialAPIBaseURL string
|
||||
PortalPath string
|
||||
LogoutPath string
|
||||
AccountAPIBasePath string
|
||||
PortalAPIBasePath string
|
||||
Accounts []portalPageAccount
|
||||
Styles template.CSS
|
||||
Script template.JS
|
||||
@@ -75,6 +79,10 @@ type portalBootstrapData struct {
|
||||
PublicSiteURL string `json:"public_site_url"`
|
||||
SupportEmail string `json:"support_email"`
|
||||
CommercialAPIBaseURL string `json:"commercial_api_base_url"`
|
||||
PortalPath string `json:"portal_path"`
|
||||
LogoutPath string `json:"logout_path"`
|
||||
AccountAPIBasePath string `json:"account_api_base_path"`
|
||||
PortalAPIBasePath string `json:"portal_api_base_path"`
|
||||
Accounts []portalBootstrapAccount `json:"accounts"`
|
||||
}
|
||||
|
||||
@@ -82,6 +90,10 @@ const (
|
||||
defaultPublicSiteURL = "https://pulserelay.pro"
|
||||
defaultSupportEmail = "support@pulserelay.pro"
|
||||
defaultCommercialAPIBaseURL = "https://license.pulserelay.pro"
|
||||
defaultPortalPath = "/portal"
|
||||
defaultLogoutPath = "/auth/logout"
|
||||
defaultAccountAPIBasePath = "/api/accounts"
|
||||
defaultPortalAPIBasePath = "/api/portal"
|
||||
)
|
||||
|
||||
// HandlePortalPage serves the MSP/Cloud portal dashboard (browser-facing HTML).
|
||||
@@ -212,6 +224,10 @@ func renderPortalPage(w http.ResponseWriter, nonce, email string, accounts []por
|
||||
PublicSiteURL: defaultPublicSiteURL,
|
||||
SupportEmail: defaultSupportEmail,
|
||||
CommercialAPIBaseURL: defaultCommercialAPIBaseURL,
|
||||
PortalPath: defaultPortalPath,
|
||||
LogoutPath: defaultLogoutPath,
|
||||
AccountAPIBasePath: defaultAccountAPIBasePath,
|
||||
PortalAPIBasePath: defaultPortalAPIBasePath,
|
||||
Accounts: accounts,
|
||||
Styles: portalStyles,
|
||||
Script: portalScript,
|
||||
@@ -262,6 +278,10 @@ func buildPortalBootstrapJSON(email string, accounts []portalPageAccount) (templ
|
||||
PublicSiteURL: defaultPublicSiteURL,
|
||||
SupportEmail: defaultSupportEmail,
|
||||
CommercialAPIBaseURL: defaultCommercialAPIBaseURL,
|
||||
PortalPath: defaultPortalPath,
|
||||
LogoutPath: defaultLogoutPath,
|
||||
AccountAPIBasePath: defaultAccountAPIBasePath,
|
||||
PortalAPIBasePath: defaultPortalAPIBasePath,
|
||||
Accounts: bootstrapAccounts,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div class="ws-actions">
|
||||
{{if eq $ws.State "active"}}
|
||||
<form method="POST" action="/api/accounts/{{$account.ID}}/tenants/{{$ws.ID}}/handoff">
|
||||
<form method="POST" action="{{$.AccountAPIBasePath}}/{{$account.ID}}/tenants/{{$ws.ID}}/handoff">
|
||||
<button type="submit" class="btn-primary">Open →</button>
|
||||
</form>
|
||||
{{else}}
|
||||
|
||||
Reference in New Issue
Block a user