Files
OTSSignsTheme/ots-signs/layoutauth.html

203 lines
6.8 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth to CMS — OTS Signs</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: #0f172a;
color: #e2e8f0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
padding: 2rem;
}
.logo {
margin-bottom: 1.5rem;
}
.logo svg {
width: 48px;
height: 48px;
}
.spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 3px solid #1e293b;
border-top-color: #e87800;
border-radius: 50%;
animation: spin 0.7s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.checkmark {
display: none;
margin-bottom: 1rem;
animation: pop 0.3s ease-out;
}
.checkmark.visible {
display: inline-block;
}
@keyframes pop {
0% { transform: scale(0.6); opacity: 0; }
70% { transform: scale(1.15); }
100% { transform: scale(1); opacity: 1; }
}
.message {
font-size: 1rem;
color: #94a3b8;
}
.link {
display: inline-block;
margin-top: 1rem;
color: #e87800;
text-decoration: none;
font-size: 0.875rem;
}
.link:hover { text-decoration: underline; }
#fallback-link { display: none; }
noscript .message {
color: #e2e8f0;
margin-bottom: 0.5rem;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
<!-- OTS Signs wordmark placeholder -->
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect width="48" height="48" rx="10" fill="#1e293b"/>
<path d="M10 24c0-7.732 6.268-14 14-14s14 6.268 14 14-6.268 14-14 14S10 31.732 10 24z"
stroke="#e87800" stroke-width="3" fill="none"/>
<path d="M19 24l3.5 3.5L30 20" stroke="#e87800" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div id="spinner" class="spinner" role="status" aria-label="Checking authentication"></div>
<div id="checkmark" class="checkmark" aria-label="Authenticated">
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle cx="28" cy="28" r="26" fill="#14532d" stroke="#22c55e" stroke-width="2"/>
<path d="M17 28l8 8 14-16" stroke="#22c55e" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<p id="message" class="message">Checking authentication…</p>
<noscript>
<p class="message">JavaScript is required for the automatic redirect.</p>
</noscript>
<a id="fallback-link" class="link" href="https://app.ots-signs.com">
Click here if you are not redirected automatically
</a>
</div>
<script>
(function () {
var TARGET_HOST = "https://app.ots-signs.com";
/**
* Validate that `to` is a safe relative path:
* - must be a non-empty string
* - must start with a single "/"
* - must NOT start with "//" (protocol-relative — would escape the host)
* - must NOT contain "@" (prevents https://app.ots-signs.com@evil.com)
* - must NOT contain "\" (prevents path traversal tricks in some parsers)
*/
function isValidPath(to) {
return (
typeof to === "string" &&
to.length > 0 &&
to.charAt(0) === "/" &&
to.charAt(1) !== "/" &&
to.indexOf("@") === -1 &&
to.indexOf("\\") === -1
);
}
/**
* Extract the customer slug from the CMS base path.
* The CMS always runs at /{customerslug}/cms/…
* so pathname.split('/')[1] gives the slug.
*/
function getSlug() {
var parts = window.location.pathname.split("/");
// parts[0] = "" (before leading /), parts[1] = customerslug
return parts[1] || "";
}
function getQueryParam(name) {
try {
var params = new URLSearchParams(window.location.search);
return params.get(name);
} catch (e) {
var match = window.location.search.match(
new RegExp("[?&]" + name + "=([^&]*)")
);
return match ? decodeURIComponent(match[1].replace(/\+/g, " ")) : null;
}
}
var to = getQueryParam("to");
var slug = getSlug();
var destination = isValidPath(to)
? TARGET_HOST + to
: TARGET_HOST + (slug ? "/" + slug : "");
// Update the visible fallback link
var link = document.getElementById("fallback-link");
if (link) link.href = destination;
var spinner = document.getElementById("spinner");
var checkmark = document.getElementById("checkmark");
var message = document.getElementById("message");
// Check CMS web session auth by fetching the CMS root and following redirects.
// The CMS always runs at /{slug}/cms/:
// - Unauthenticated: 302 → /cms/login (final response.url contains "/login")
// - Authenticated: 302 → /cms/dashboard (final response.url does NOT contain "/login")
// Both cases produce an opaqueredirect with redirect:'manual', so we instead let
// the browser follow redirects and inspect where it ultimately lands.
var cmsRootUrl = window.location.origin + "/" + slug + "/cms/";
fetch(cmsRootUrl, {
method: "GET",
credentials: "include" // sends the CMS session cookie; follow redirects (default)
})
.then(function (response) {
// If the browser ended up on the login page, the session is not authenticated
if (!response.url || response.url.indexOf("/login") !== -1) {
throw new Error("unauthenticated");
}
return response;
})
.then(function () {
// Authenticated — show the green checkmark for 2 seconds then redirect
spinner.style.display = "none";
checkmark.classList.add("visible");
message.textContent = "Auth to CMS";
if (link) link.style.display = "inline";
setTimeout(function () {
window.location.replace(destination);
}, 2000);
})
.catch(function () {
// Not authenticated — send to the CMS login page, preserving the return URL
var returnUrl = encodeURIComponent(window.location.href);
var loginUrl = window.location.origin + "/" + slug + "/cms/login?redirect=" + returnUrl;
window.location.replace(loginUrl);
});
})();
</script>
</body>
</html>