mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-09 19:32:24 +00:00
refactor(cloudcp): add Pulse Account portal store
This commit is contained in:
parent
8c86c497da
commit
e50048f0bd
7 changed files with 236 additions and 235 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"source_hash": "cee75c53fdd6a5b26d949440b84ab98240517e04cd1e40aaefacc0cc9d20f20a",
|
||||
"source_hash": "633edb73d1996633e9ccd36ec746a2ea4eb295989063af03743102265c9d3cab",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
|
|
|||
167
internal/cloudcp/portal/dist/portal_app.js
vendored
167
internal/cloudcp/portal/dist/portal_app.js
vendored
|
|
@ -1,4 +1,43 @@
|
|||
(() => {
|
||||
// src/store.ts
|
||||
function createAnonymousBootstrap(bootstrapDefaults2, overrides = {}) {
|
||||
return {
|
||||
authenticated: false,
|
||||
email: "",
|
||||
...bootstrapDefaults2,
|
||||
...overrides,
|
||||
accounts: normalizeAccounts(overrides.accounts)
|
||||
};
|
||||
}
|
||||
function normalizeBootstrap(bootstrapDefaults2, raw) {
|
||||
return createAnonymousBootstrap(bootstrapDefaults2, raw || {});
|
||||
}
|
||||
function createPortalStore(bootstrapDefaults2, initialBootstrap) {
|
||||
var bootstrapState = normalizeBootstrap(bootstrapDefaults2, initialBootstrap);
|
||||
var subscribers = /* @__PURE__ */ new Set();
|
||||
return {
|
||||
getBootstrap: function() {
|
||||
return bootstrapState;
|
||||
},
|
||||
setBootstrap: function(nextBootstrap) {
|
||||
bootstrapState = normalizeBootstrap(bootstrapDefaults2, nextBootstrap);
|
||||
subscribers.forEach(function(listener) {
|
||||
listener();
|
||||
});
|
||||
return bootstrapState;
|
||||
},
|
||||
subscribe: function(listener) {
|
||||
subscribers.add(listener);
|
||||
return function() {
|
||||
subscribers.delete(listener);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
function normalizeAccounts(accounts) {
|
||||
return Array.isArray(accounts) ? accounts : [];
|
||||
}
|
||||
|
||||
// src/runtime.ts
|
||||
function readEmbeddedBootstrap() {
|
||||
const bootstrapEl = document.getElementById("pulse-account-bootstrap");
|
||||
|
|
@ -24,65 +63,7 @@
|
|||
account_api_base_path: embeddedBootstrap.account_api_base_path || "/api/accounts",
|
||||
portal_api_base_path: embeddedBootstrap.portal_api_base_path || "/api/portal"
|
||||
};
|
||||
function normalizeAccounts(accounts) {
|
||||
return Array.isArray(accounts) ? accounts : [];
|
||||
}
|
||||
function createAnonymousBootstrap(overrides = {}) {
|
||||
return {
|
||||
authenticated: false,
|
||||
email: "",
|
||||
...bootstrapDefaults,
|
||||
...overrides,
|
||||
accounts: normalizeAccounts(overrides.accounts)
|
||||
};
|
||||
}
|
||||
function normalizeBootstrap(raw) {
|
||||
return createAnonymousBootstrap(raw || {});
|
||||
}
|
||||
var bootstrapState = normalizeBootstrap(embeddedBootstrap);
|
||||
var renderSubscribers = /* @__PURE__ */ new Set();
|
||||
function getBootstrap() {
|
||||
return bootstrapState;
|
||||
}
|
||||
function setBootstrap(nextBootstrap) {
|
||||
bootstrapState = normalizeBootstrap(nextBootstrap);
|
||||
return bootstrapState;
|
||||
}
|
||||
function getCommercialAPIBaseURL() {
|
||||
return bootstrapState.commercial_api_base_url;
|
||||
}
|
||||
function getPortalPath() {
|
||||
return bootstrapState.portal_path;
|
||||
}
|
||||
function getBootstrapPath() {
|
||||
return bootstrapState.bootstrap_path;
|
||||
}
|
||||
function getMagicLinkRequestPath() {
|
||||
return bootstrapState.magic_link_request_path;
|
||||
}
|
||||
function getSignupPath() {
|
||||
return bootstrapState.signup_path;
|
||||
}
|
||||
function getLogoutPath() {
|
||||
return bootstrapState.logout_path;
|
||||
}
|
||||
function getAccountAPIBasePath() {
|
||||
return bootstrapState.account_api_base_path;
|
||||
}
|
||||
function getPortalAPIBasePath() {
|
||||
return bootstrapState.portal_api_base_path;
|
||||
}
|
||||
function notifyPortalRender() {
|
||||
renderSubscribers.forEach((listener) => {
|
||||
listener();
|
||||
});
|
||||
}
|
||||
function subscribePortalRender(listener) {
|
||||
renderSubscribers.add(listener);
|
||||
return () => {
|
||||
renderSubscribers.delete(listener);
|
||||
};
|
||||
}
|
||||
var portalStore = createPortalStore(bootstrapDefaults, embeddedBootstrap);
|
||||
|
||||
// src/account_controller.ts
|
||||
function getElement(id) {
|
||||
|
|
@ -724,61 +705,45 @@
|
|||
}
|
||||
|
||||
// src/shell.ts
|
||||
var portalBootstrap = getBootstrap();
|
||||
var LICENSE_API_BASE = getCommercialAPIBaseURL();
|
||||
var PORTAL_PATH = getPortalPath();
|
||||
var BOOTSTRAP_PATH = getBootstrapPath();
|
||||
var MAGIC_LINK_REQUEST_PATH = getMagicLinkRequestPath();
|
||||
var SIGNUP_PATH = getSignupPath();
|
||||
var LOGOUT_PATH = getLogoutPath();
|
||||
var ACCOUNT_API_BASE_PATH = getAccountAPIBasePath();
|
||||
var PORTAL_API_BASE_PATH = getPortalAPIBasePath();
|
||||
function renderHeader() {
|
||||
var userInfo = document.getElementById("portal-user-info");
|
||||
if (!userInfo) return;
|
||||
var portalBootstrap = portalStore.getBootstrap();
|
||||
userInfo.innerHTML = renderHeaderHTML({
|
||||
bootstrap: portalBootstrap,
|
||||
loginState: authController.getLoginState(),
|
||||
signupPath: SIGNUP_PATH,
|
||||
accountAPIBasePath: ACCOUNT_API_BASE_PATH
|
||||
signupPath: portalBootstrap.signup_path,
|
||||
accountAPIBasePath: portalBootstrap.account_api_base_path
|
||||
});
|
||||
}
|
||||
function renderPortalApp() {
|
||||
renderHeader();
|
||||
var root = document.getElementById("portal-app-root");
|
||||
if (!root) return;
|
||||
var portalBootstrap = portalStore.getBootstrap();
|
||||
var context = {
|
||||
bootstrap: portalBootstrap,
|
||||
loginState: authController.getLoginState(),
|
||||
signupPath: SIGNUP_PATH,
|
||||
accountAPIBasePath: ACCOUNT_API_BASE_PATH
|
||||
signupPath: portalBootstrap.signup_path,
|
||||
accountAPIBasePath: portalBootstrap.account_api_base_path
|
||||
};
|
||||
root.innerHTML = portalBootstrap.authenticated ? renderAuthenticatedPortalHTML(context) : renderSignedOutPortalHTML(context);
|
||||
notifyPortalRender();
|
||||
}
|
||||
function applyBootstrap(data) {
|
||||
portalBootstrap = setBootstrap(data || createAnonymousBootstrap());
|
||||
LICENSE_API_BASE = getCommercialAPIBaseURL();
|
||||
PORTAL_PATH = getPortalPath();
|
||||
BOOTSTRAP_PATH = getBootstrapPath();
|
||||
MAGIC_LINK_REQUEST_PATH = getMagicLinkRequestPath();
|
||||
SIGNUP_PATH = getSignupPath();
|
||||
LOGOUT_PATH = getLogoutPath();
|
||||
ACCOUNT_API_BASE_PATH = getAccountAPIBasePath();
|
||||
PORTAL_API_BASE_PATH = getPortalAPIBasePath();
|
||||
var portalBootstrap = portalStore.setBootstrap(data || createAnonymousBootstrap(bootstrapDefaults));
|
||||
if (!portalBootstrap.authenticated) {
|
||||
authController.syncBootstrapEmail(portalBootstrap.email || "");
|
||||
}
|
||||
renderPortalApp();
|
||||
}
|
||||
async function refreshBootstrap() {
|
||||
if (!BOOTSTRAP_PATH) return false;
|
||||
var bootstrap = portalStore.getBootstrap();
|
||||
if (!bootstrap.bootstrap_path) return false;
|
||||
try {
|
||||
var response = await fetch(BOOTSTRAP_PATH, {
|
||||
var response = await fetch(bootstrap.bootstrap_path, {
|
||||
headers: { "Accept": "application/json" }
|
||||
});
|
||||
if (response.status === 401) {
|
||||
applyBootstrap(createAnonymousBootstrap());
|
||||
applyBootstrap(createAnonymousBootstrap(bootstrapDefaults));
|
||||
return true;
|
||||
}
|
||||
if (!response.ok) return false;
|
||||
|
|
@ -801,31 +766,34 @@
|
|||
}
|
||||
var authController = installAuthController({
|
||||
getMagicLinkRequestPath: function() {
|
||||
return MAGIC_LINK_REQUEST_PATH;
|
||||
return portalStore.getBootstrap().magic_link_request_path;
|
||||
},
|
||||
getLogoutPath: function() {
|
||||
return LOGOUT_PATH;
|
||||
return portalStore.getBootstrap().logout_path;
|
||||
},
|
||||
getPortalPath: function() {
|
||||
return PORTAL_PATH;
|
||||
return portalStore.getBootstrap().portal_path;
|
||||
},
|
||||
renderPortal: renderPortalApp
|
||||
});
|
||||
installAccountController({
|
||||
getAccountAPIBasePath: function() {
|
||||
return ACCOUNT_API_BASE_PATH;
|
||||
return portalStore.getBootstrap().account_api_base_path;
|
||||
},
|
||||
getPortalAPIBasePath: function() {
|
||||
return PORTAL_API_BASE_PATH;
|
||||
return portalStore.getBootstrap().portal_api_base_path;
|
||||
},
|
||||
getPortalPath: function() {
|
||||
return PORTAL_PATH;
|
||||
return portalStore.getBootstrap().portal_path;
|
||||
},
|
||||
refreshBootstrap,
|
||||
showToast
|
||||
});
|
||||
applyBootstrap(portalBootstrap);
|
||||
if (portalBootstrap.authenticated) {
|
||||
portalStore.subscribe(function() {
|
||||
renderPortalApp();
|
||||
});
|
||||
applyBootstrap(portalStore.getBootstrap());
|
||||
if (portalStore.getBootstrap().authenticated) {
|
||||
refreshBootstrap();
|
||||
}
|
||||
|
||||
|
|
@ -1008,11 +976,8 @@
|
|||
|
||||
// src/services.ts
|
||||
var serviceState = createPortalServiceState();
|
||||
function getCommercialAPIBaseURL2() {
|
||||
return getCommercialAPIBaseURL();
|
||||
}
|
||||
function serviceFetch(path, body) {
|
||||
return fetch(getCommercialAPIBaseURL2() + path, {
|
||||
return fetch(portalStore.getBootstrap().commercial_api_base_url + path, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
|
|
@ -1047,7 +1012,7 @@
|
|||
renderRefund();
|
||||
}
|
||||
function renderRefund() {
|
||||
renderRefundPanel(serviceState.refund, getBootstrap());
|
||||
renderRefundPanel(serviceState.refund, portalStore.getBootstrap());
|
||||
renderButton("refund-inline-submit", serviceState.refund.submitting, serviceState.refund.submitting ? "Processing..." : "Process Refund");
|
||||
renderStatus("refund-inline-status", serviceState.refund.status);
|
||||
}
|
||||
|
|
@ -1306,7 +1271,7 @@
|
|||
}
|
||||
}
|
||||
function syncServiceStateFromBootstrap() {
|
||||
var bootstrap = getBootstrap();
|
||||
var bootstrap = portalStore.getBootstrap();
|
||||
if (!bootstrap.authenticated) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1318,7 +1283,7 @@
|
|||
renderAllFlows();
|
||||
}
|
||||
renderServiceRuntime();
|
||||
subscribePortalRender(renderServiceRuntime);
|
||||
portalStore.subscribe(renderServiceRuntime);
|
||||
installServicesController({
|
||||
toggleServicePanel,
|
||||
focusElement,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { createPortalStore } from './store';
|
||||
import type { PortalBootstrapData } from './types';
|
||||
|
||||
function readEmbeddedBootstrap(): Partial<PortalBootstrapData> {
|
||||
|
|
@ -14,7 +15,7 @@ function readEmbeddedBootstrap(): Partial<PortalBootstrapData> {
|
|||
|
||||
const embeddedBootstrap = readEmbeddedBootstrap();
|
||||
|
||||
const bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'> = {
|
||||
export const bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'> = {
|
||||
public_site_url: embeddedBootstrap.public_site_url || 'https://pulserelay.pro',
|
||||
support_email: embeddedBootstrap.support_email || 'support@pulserelay.pro',
|
||||
commercial_api_base_url: embeddedBootstrap.commercial_api_base_url || '',
|
||||
|
|
@ -27,77 +28,4 @@ const bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | '
|
|||
portal_api_base_path: embeddedBootstrap.portal_api_base_path || '/api/portal',
|
||||
};
|
||||
|
||||
function normalizeAccounts(accounts: Partial<PortalBootstrapData>['accounts']): PortalBootstrapData['accounts'] {
|
||||
return Array.isArray(accounts) ? accounts : [];
|
||||
}
|
||||
|
||||
export function createAnonymousBootstrap(overrides: Partial<PortalBootstrapData> = {}): PortalBootstrapData {
|
||||
return {
|
||||
authenticated: false,
|
||||
email: '',
|
||||
...bootstrapDefaults,
|
||||
...overrides,
|
||||
accounts: normalizeAccounts(overrides.accounts),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeBootstrap(raw: Partial<PortalBootstrapData> | null | undefined): PortalBootstrapData {
|
||||
return createAnonymousBootstrap(raw || {});
|
||||
}
|
||||
|
||||
let bootstrapState: PortalBootstrapData = normalizeBootstrap(embeddedBootstrap);
|
||||
const renderSubscribers = new Set<() => void>();
|
||||
|
||||
export function getBootstrap(): PortalBootstrapData {
|
||||
return bootstrapState;
|
||||
}
|
||||
|
||||
export function setBootstrap(nextBootstrap: Partial<PortalBootstrapData> | PortalBootstrapData): PortalBootstrapData {
|
||||
bootstrapState = normalizeBootstrap(nextBootstrap);
|
||||
return bootstrapState;
|
||||
}
|
||||
|
||||
export function getCommercialAPIBaseURL(): string {
|
||||
return bootstrapState.commercial_api_base_url;
|
||||
}
|
||||
|
||||
export function getPortalPath(): string {
|
||||
return bootstrapState.portal_path;
|
||||
}
|
||||
|
||||
export function getBootstrapPath(): string {
|
||||
return bootstrapState.bootstrap_path;
|
||||
}
|
||||
|
||||
export function getMagicLinkRequestPath(): string {
|
||||
return bootstrapState.magic_link_request_path;
|
||||
}
|
||||
|
||||
export function getSignupPath(): string {
|
||||
return bootstrapState.signup_path;
|
||||
}
|
||||
|
||||
export function getLogoutPath(): string {
|
||||
return bootstrapState.logout_path;
|
||||
}
|
||||
|
||||
export function getAccountAPIBasePath(): string {
|
||||
return bootstrapState.account_api_base_path;
|
||||
}
|
||||
|
||||
export function getPortalAPIBasePath(): string {
|
||||
return bootstrapState.portal_api_base_path;
|
||||
}
|
||||
|
||||
export function notifyPortalRender(): void {
|
||||
renderSubscribers.forEach((listener) => {
|
||||
listener();
|
||||
});
|
||||
}
|
||||
|
||||
export function subscribePortalRender(listener: () => void): () => void {
|
||||
renderSubscribers.add(listener);
|
||||
return () => {
|
||||
renderSubscribers.delete(listener);
|
||||
};
|
||||
}
|
||||
export const portalStore = createPortalStore(bootstrapDefaults, embeddedBootstrap);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getBootstrap, getCommercialAPIBaseURL as readCommercialAPIBaseURL, subscribePortalRender } from './runtime';
|
||||
import { portalStore } from './runtime';
|
||||
import { installServicesController } from './services_controller';
|
||||
import {
|
||||
clearFlowStatus,
|
||||
|
|
@ -59,12 +59,8 @@ interface VerificationFlowDefinition {
|
|||
|
||||
var serviceState = createPortalServiceState();
|
||||
|
||||
function getCommercialAPIBaseURL() {
|
||||
return readCommercialAPIBaseURL();
|
||||
}
|
||||
|
||||
function serviceFetch(path, body) {
|
||||
return fetch(getCommercialAPIBaseURL() + path, {
|
||||
return fetch(portalStore.getBootstrap().commercial_api_base_url + path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
|
|
@ -103,7 +99,7 @@ interface VerificationFlowDefinition {
|
|||
}
|
||||
|
||||
function renderRefund() {
|
||||
renderRefundPanel(serviceState.refund, getBootstrap());
|
||||
renderRefundPanel(serviceState.refund, portalStore.getBootstrap());
|
||||
renderButton('refund-inline-submit', serviceState.refund.submitting, serviceState.refund.submitting ? 'Processing...' : 'Process Refund');
|
||||
renderStatus('refund-inline-status', serviceState.refund.status);
|
||||
}
|
||||
|
|
@ -369,7 +365,7 @@ interface VerificationFlowDefinition {
|
|||
}
|
||||
|
||||
function syncServiceStateFromBootstrap() {
|
||||
var bootstrap = getBootstrap();
|
||||
var bootstrap = portalStore.getBootstrap();
|
||||
if (!bootstrap.authenticated) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -383,7 +379,7 @@ interface VerificationFlowDefinition {
|
|||
}
|
||||
|
||||
renderServiceRuntime();
|
||||
subscribePortalRender(renderServiceRuntime);
|
||||
portalStore.subscribe(renderServiceRuntime);
|
||||
|
||||
installServicesController({
|
||||
toggleServicePanel,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
import {
|
||||
createAnonymousBootstrap,
|
||||
getAccountAPIBasePath,
|
||||
getBootstrap,
|
||||
getBootstrapPath,
|
||||
getCommercialAPIBaseURL,
|
||||
getLogoutPath,
|
||||
getMagicLinkRequestPath,
|
||||
getPortalAPIBasePath,
|
||||
getPortalPath,
|
||||
getSignupPath,
|
||||
notifyPortalRender,
|
||||
setBootstrap,
|
||||
bootstrapDefaults,
|
||||
portalStore,
|
||||
} from './runtime';
|
||||
import { createAnonymousBootstrap } from './store';
|
||||
import { installAccountController } from './account_controller';
|
||||
import { installAuthController } from './auth_controller';
|
||||
import {
|
||||
|
|
@ -19,28 +10,18 @@ import {
|
|||
renderHeaderHTML,
|
||||
renderSignedOutPortalHTML,
|
||||
} from './shell_view';
|
||||
import type { PortalBootstrapData } from './types';
|
||||
|
||||
var portalBootstrap: PortalBootstrapData = getBootstrap();
|
||||
var LICENSE_API_BASE = getCommercialAPIBaseURL();
|
||||
var PORTAL_PATH = getPortalPath();
|
||||
var BOOTSTRAP_PATH = getBootstrapPath();
|
||||
var MAGIC_LINK_REQUEST_PATH = getMagicLinkRequestPath();
|
||||
var SIGNUP_PATH = getSignupPath();
|
||||
var LOGOUT_PATH = getLogoutPath();
|
||||
var ACCOUNT_API_BASE_PATH = getAccountAPIBasePath();
|
||||
var PORTAL_API_BASE_PATH = getPortalAPIBasePath();
|
||||
|
||||
type ToastElement = HTMLElement & { _timer?: ReturnType<typeof setTimeout> };
|
||||
|
||||
function renderHeader() {
|
||||
var userInfo = document.getElementById('portal-user-info');
|
||||
if (!userInfo) return;
|
||||
var portalBootstrap = portalStore.getBootstrap();
|
||||
userInfo.innerHTML = renderHeaderHTML({
|
||||
bootstrap: portalBootstrap,
|
||||
loginState: authController.getLoginState(),
|
||||
signupPath: SIGNUP_PATH,
|
||||
accountAPIBasePath: ACCOUNT_API_BASE_PATH,
|
||||
signupPath: portalBootstrap.signup_path,
|
||||
accountAPIBasePath: portalBootstrap.account_api_base_path,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -48,42 +29,34 @@ function renderPortalApp() {
|
|||
renderHeader();
|
||||
var root = document.getElementById('portal-app-root');
|
||||
if (!root) return;
|
||||
var portalBootstrap = portalStore.getBootstrap();
|
||||
var context = {
|
||||
bootstrap: portalBootstrap,
|
||||
loginState: authController.getLoginState(),
|
||||
signupPath: SIGNUP_PATH,
|
||||
accountAPIBasePath: ACCOUNT_API_BASE_PATH,
|
||||
signupPath: portalBootstrap.signup_path,
|
||||
accountAPIBasePath: portalBootstrap.account_api_base_path,
|
||||
};
|
||||
root.innerHTML = portalBootstrap.authenticated
|
||||
? renderAuthenticatedPortalHTML(context)
|
||||
: renderSignedOutPortalHTML(context);
|
||||
notifyPortalRender();
|
||||
}
|
||||
|
||||
function applyBootstrap(data) {
|
||||
portalBootstrap = setBootstrap(data || createAnonymousBootstrap());
|
||||
LICENSE_API_BASE = getCommercialAPIBaseURL();
|
||||
PORTAL_PATH = getPortalPath();
|
||||
BOOTSTRAP_PATH = getBootstrapPath();
|
||||
MAGIC_LINK_REQUEST_PATH = getMagicLinkRequestPath();
|
||||
SIGNUP_PATH = getSignupPath();
|
||||
LOGOUT_PATH = getLogoutPath();
|
||||
ACCOUNT_API_BASE_PATH = getAccountAPIBasePath();
|
||||
PORTAL_API_BASE_PATH = getPortalAPIBasePath();
|
||||
var portalBootstrap = portalStore.setBootstrap(data || createAnonymousBootstrap(bootstrapDefaults));
|
||||
if (!portalBootstrap.authenticated) {
|
||||
authController.syncBootstrapEmail(portalBootstrap.email || '');
|
||||
}
|
||||
renderPortalApp();
|
||||
}
|
||||
|
||||
async function refreshBootstrap() {
|
||||
if (!BOOTSTRAP_PATH) return false;
|
||||
var bootstrap = portalStore.getBootstrap();
|
||||
if (!bootstrap.bootstrap_path) return false;
|
||||
try {
|
||||
var response = await fetch(BOOTSTRAP_PATH, {
|
||||
var response = await fetch(bootstrap.bootstrap_path, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
if (response.status === 401) {
|
||||
applyBootstrap(createAnonymousBootstrap());
|
||||
applyBootstrap(createAnonymousBootstrap(bootstrapDefaults));
|
||||
return true;
|
||||
}
|
||||
if (!response.ok) return false;
|
||||
|
|
@ -105,32 +78,36 @@ function showToast(msg, isError = false) {
|
|||
|
||||
var authController = installAuthController({
|
||||
getMagicLinkRequestPath: function() {
|
||||
return MAGIC_LINK_REQUEST_PATH;
|
||||
return portalStore.getBootstrap().magic_link_request_path;
|
||||
},
|
||||
getLogoutPath: function() {
|
||||
return LOGOUT_PATH;
|
||||
return portalStore.getBootstrap().logout_path;
|
||||
},
|
||||
getPortalPath: function() {
|
||||
return PORTAL_PATH;
|
||||
return portalStore.getBootstrap().portal_path;
|
||||
},
|
||||
renderPortal: renderPortalApp,
|
||||
});
|
||||
|
||||
installAccountController({
|
||||
getAccountAPIBasePath: function() {
|
||||
return ACCOUNT_API_BASE_PATH;
|
||||
return portalStore.getBootstrap().account_api_base_path;
|
||||
},
|
||||
getPortalAPIBasePath: function() {
|
||||
return PORTAL_API_BASE_PATH;
|
||||
return portalStore.getBootstrap().portal_api_base_path;
|
||||
},
|
||||
getPortalPath: function() {
|
||||
return PORTAL_PATH;
|
||||
return portalStore.getBootstrap().portal_path;
|
||||
},
|
||||
refreshBootstrap: refreshBootstrap,
|
||||
showToast: showToast
|
||||
});
|
||||
|
||||
applyBootstrap(portalBootstrap);
|
||||
if (portalBootstrap.authenticated) {
|
||||
portalStore.subscribe(function() {
|
||||
renderPortalApp();
|
||||
});
|
||||
|
||||
applyBootstrap(portalStore.getBootstrap());
|
||||
if (portalStore.getBootstrap().authenticated) {
|
||||
refreshBootstrap();
|
||||
}
|
||||
|
|
|
|||
77
internal/cloudcp/portal/frontend/src/store.test.ts
Normal file
77
internal/cloudcp/portal/frontend/src/store.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createAnonymousBootstrap, createPortalStore, normalizeBootstrap } from './store';
|
||||
import type { PortalBootstrapData } from './types';
|
||||
|
||||
const bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'> = {
|
||||
public_site_url: 'https://pulserelay.pro',
|
||||
support_email: 'support@pulserelay.pro',
|
||||
commercial_api_base_url: 'https://license.pulserelay.pro',
|
||||
portal_path: '/portal',
|
||||
bootstrap_path: '/api/portal/bootstrap',
|
||||
magic_link_request_path: '/api/public/magic-link/request',
|
||||
signup_path: '/signup',
|
||||
logout_path: '/auth/logout',
|
||||
account_api_base_path: '/api/accounts',
|
||||
portal_api_base_path: '/api/portal',
|
||||
};
|
||||
|
||||
describe('portal store', function() {
|
||||
it('normalizes anonymous bootstrap state from defaults', function() {
|
||||
var bootstrap = createAnonymousBootstrap(bootstrapDefaults, {
|
||||
signup_path: '/join',
|
||||
});
|
||||
|
||||
expect(bootstrap.authenticated).toBe(false);
|
||||
expect(bootstrap.email).toBe('');
|
||||
expect(bootstrap.signup_path).toBe('/join');
|
||||
expect(bootstrap.accounts).toEqual([]);
|
||||
});
|
||||
|
||||
it('normalizes partial bootstrap payloads into tracked account state', function() {
|
||||
var bootstrap = normalizeBootstrap(bootstrapDefaults, {
|
||||
authenticated: true,
|
||||
email: 'owner@example.com',
|
||||
accounts: [
|
||||
{
|
||||
id: 'acct_1',
|
||||
name: 'Acme MSP',
|
||||
kind: 'msp',
|
||||
kind_label: 'MSP',
|
||||
role: 'owner',
|
||||
can_manage: true,
|
||||
has_billing: true,
|
||||
workspaces: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(bootstrap.authenticated).toBe(true);
|
||||
expect(bootstrap.email).toBe('owner@example.com');
|
||||
expect(bootstrap.accounts).toHaveLength(1);
|
||||
expect(bootstrap.portal_path).toBe('/portal');
|
||||
});
|
||||
|
||||
it('publishes bootstrap changes through a subscription boundary', function() {
|
||||
var store = createPortalStore(bootstrapDefaults, null);
|
||||
var listener = vi.fn();
|
||||
var unsubscribe = store.subscribe(listener);
|
||||
|
||||
store.setBootstrap({
|
||||
authenticated: true,
|
||||
email: 'owner@example.com',
|
||||
});
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(store.getBootstrap().authenticated).toBe(true);
|
||||
expect(store.getBootstrap().email).toBe('owner@example.com');
|
||||
|
||||
unsubscribe();
|
||||
store.setBootstrap({
|
||||
authenticated: false,
|
||||
email: '',
|
||||
});
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
58
internal/cloudcp/portal/frontend/src/store.ts
Normal file
58
internal/cloudcp/portal/frontend/src/store.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { PortalBootstrapData } from './types';
|
||||
|
||||
export interface PortalStore {
|
||||
getBootstrap(): PortalBootstrapData;
|
||||
setBootstrap(nextBootstrap: Partial<PortalBootstrapData> | PortalBootstrapData): PortalBootstrapData;
|
||||
subscribe(listener: () => void): () => void;
|
||||
}
|
||||
|
||||
export function createAnonymousBootstrap(
|
||||
bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'>,
|
||||
overrides: Partial<PortalBootstrapData> = {}
|
||||
): PortalBootstrapData {
|
||||
return {
|
||||
authenticated: false,
|
||||
email: '',
|
||||
...bootstrapDefaults,
|
||||
...overrides,
|
||||
accounts: normalizeAccounts(overrides.accounts),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeBootstrap(
|
||||
bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'>,
|
||||
raw: Partial<PortalBootstrapData> | null | undefined
|
||||
): PortalBootstrapData {
|
||||
return createAnonymousBootstrap(bootstrapDefaults, raw || {});
|
||||
}
|
||||
|
||||
export function createPortalStore(
|
||||
bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'>,
|
||||
initialBootstrap: Partial<PortalBootstrapData> | null | undefined
|
||||
): PortalStore {
|
||||
var bootstrapState = normalizeBootstrap(bootstrapDefaults, initialBootstrap);
|
||||
var subscribers = new Set<() => void>();
|
||||
|
||||
return {
|
||||
getBootstrap: function() {
|
||||
return bootstrapState;
|
||||
},
|
||||
setBootstrap: function(nextBootstrap) {
|
||||
bootstrapState = normalizeBootstrap(bootstrapDefaults, nextBootstrap);
|
||||
subscribers.forEach(function(listener) {
|
||||
listener();
|
||||
});
|
||||
return bootstrapState;
|
||||
},
|
||||
subscribe: function(listener) {
|
||||
subscribers.add(listener);
|
||||
return function() {
|
||||
subscribers.delete(listener);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAccounts(accounts: Partial<PortalBootstrapData>['accounts']): PortalBootstrapData['accounts'] {
|
||||
return Array.isArray(accounts) ? accounts : [];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue