feat(Makefile): replace Cloud Run deployment with Firebase Hosting for launchpad
feat(firebase.json): remove site property from api-harness hosting config feat(firebase): migrate launchpad to firebase hosting with auth This commit migrates the internal launchpad from Cloud Run with IAP to Firebase Hosting with Firebase Authentication. This change simplifies the deployment process and leverages Firebase's authentication capabilities for user access control. The following changes were made: - Updated the Makefile to remove Cloud Run deployment tasks and add Firebase Hosting deployment tasks. - Removed the Dockerfile and .gcloudignore files, as they are no longer needed for Firebase Hosting. - Updated the firebase.json file to configure Firebase Hosting for the launchpad. - Modified the launchpad's index.html to include Firebase Authentication logic. - Updated the iap-users.txt file to be used as a whitelist for Firebase Authentication. - Added a launchpad-dev target to run the launchpad locally using the Firebase Hosting emulator. - Removed the configure-iap-launchpad target, as IAP is no longer used. - Removed the site property from the api-harness hosting configuration in firebase.json, as it is not needed. The migration to Firebase Hosting provides the following benefits: - Simplified deployment process. - Firebase Authentication for user access control. - Improved scalability and reliability. - Reduced operational overhead.
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KROW Launchpad</title>
|
||||
<title>Krow DevOps Launchpad</title>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.svg">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
@@ -73,15 +73,8 @@
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .7;
|
||||
}
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .7; }
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
@@ -106,18 +99,14 @@
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563;
|
||||
/* text-gray-600 */
|
||||
text-align: left;
|
||||
background-color: #f9fafb;
|
||||
/* bg-gray-50 */
|
||||
border-radius: 0.5rem;
|
||||
/* rounded-lg */
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.accordion-button:hover {
|
||||
background-color: #f3f4f6;
|
||||
/* bg-gray-100 */
|
||||
}
|
||||
|
||||
.accordion-button .chevron {
|
||||
@@ -134,182 +123,78 @@
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
backdrop-filter: blur(4px);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Markdown styling */
|
||||
.markdown-content {
|
||||
line-height: 1.7;
|
||||
color: #374151;
|
||||
}
|
||||
.markdown-content { line-height: 1.7; color: #374151; }
|
||||
.markdown-content h1 { font-size: 2em; font-weight: 700; margin-top: 1.5em; margin-bottom: 0.5em; padding-bottom: 0.3em; border-bottom: 2px solid #e5e7eb; color: #111827; }
|
||||
.markdown-content h1:first-child { margin-top: 0; }
|
||||
.markdown-content h2 { font-size: 1.5em; font-weight: 600; margin-top: 1.5em; margin-bottom: 0.5em; padding-bottom: 0.2em; border-bottom: 1px solid #e5e7eb; color: #111827; }
|
||||
.markdown-content h3 { font-size: 1.25em; font-weight: 600; margin-top: 1.2em; margin-bottom: 0.5em; color: #111827; }
|
||||
.markdown-content h4 { font-size: 1.1em; font-weight: 600; margin-top: 1em; margin-bottom: 0.5em; color: #111827; }
|
||||
.markdown-content p { margin-bottom: 1em; }
|
||||
.markdown-content ul, .markdown-content ol { margin-bottom: 1em; padding-left: 2em; }
|
||||
.markdown-content ul { list-style-type: disc; }
|
||||
.markdown-content ol { list-style-type: decimal; }
|
||||
.markdown-content li { margin-bottom: 0.5em; }
|
||||
.markdown-content code { background-color: #f3f4f6; padding: 0.2em 0.4em; border-radius: 0.25em; font-size: 0.9em; font-family: 'Courier New', monospace; color: #dc2626; }
|
||||
.markdown-content pre { background-color: #1f2937; color: #f9fafb; padding: 1em; border-radius: 0.5em; overflow-x: auto; margin-bottom: 1em; }
|
||||
.markdown-content pre code { background-color: transparent; padding: 0; color: inherit; }
|
||||
.markdown-content blockquote { border-left: 4px solid #3b82f6; padding-left: 1em; margin: 1em 0; color: #6b7280; font-style: italic; }
|
||||
.markdown-content a { color: #3b82f6; text-decoration: underline; }
|
||||
.markdown-content a:hover { color: #2563eb; }
|
||||
.markdown-content table { width: 100%; border-collapse: collapse; margin-bottom: 1em; }
|
||||
.markdown-content th, .markdown-content td { border: 1px solid #e5e7eb; padding: 0.5em; text-align: left; }
|
||||
.markdown-content th { background-color: #f9fafb; font-weight: 600; }
|
||||
.markdown-content img { max-width: 100%; height: auto; border-radius: 0.5em; margin: 1em 0; }
|
||||
.markdown-content hr { border: none; border-top: 2px solid #e5e7eb; margin: 2em 0; }
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 2em;
|
||||
font-weight: 700;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
color: #111827;
|
||||
/* Loading Overlay */
|
||||
#auth-loading {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: white; z-index: 50;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
.markdown-content h1:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
padding-bottom: 0.2em;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.markdown-content h4 {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
margin-bottom: 1em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.markdown-content ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.markdown-content ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 0.25em;
|
||||
font-size: 0.9em;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: #1f2937;
|
||||
color: #f9fafb;
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding-left: 1em;
|
||||
margin: 1em 0;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.markdown-content th,
|
||||
.markdown-content td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content th {
|
||||
background-color: #f9fafb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 2px solid #e5e7eb;
|
||||
margin: 2em 0;
|
||||
#login-screen {
|
||||
display: none;
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||
z-index: 40;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen">
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
|
||||
<!-- Auth Loading State -->
|
||||
<div id="auth-loading">
|
||||
<div class="w-16 h-16 border-4 border-primary-200 border-t-primary-600 rounded-full animate-spin mb-4"></div>
|
||||
<p class="text-gray-500 font-medium">Verifying access...</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Screen -->
|
||||
<div id="login-screen" class="flex">
|
||||
<div class="bg-white p-8 rounded-2xl shadow-xl max-w-md w-full mx-4 text-center">
|
||||
<div class="flex justify-center mb-6">
|
||||
<img src="logo.svg" alt="KROW Logo" class="h-16 w-16" onerror="this.src='https://via.placeholder.com/64?text=KROW'">
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Internal DevOps Launchpad</h1>
|
||||
<p class="text-gray-500 mb-8">Please sign in to access internal development resources.</p>
|
||||
|
||||
<button id="google-signin-btn" class="w-full flex items-center justify-center space-x-3 bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-medium py-3 px-4 rounded-xl transition-all shadow-sm">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||
</svg>
|
||||
<span>Sign in with Google</span>
|
||||
</button>
|
||||
<p id="login-error" class="mt-4 text-red-500 text-sm hidden"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content (Protected) -->
|
||||
<div id="app-content" class="hidden flex h-screen overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-72 bg-white shadow-xl flex flex-col border-r border-gray-200">
|
||||
@@ -320,8 +205,8 @@
|
||||
<img src="logo.svg" alt="Krow Logo" style="height: 60px;" onerror="this.style.display='none'">
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900">KROW</h1>
|
||||
<p class="text-xs text-gray-500">Launchpad</p>
|
||||
<h1 class="text-xl font-bold text-gray-900">KROW DevOps</h1>
|
||||
<p class="text-xs text-gray-500">Launchpad Hub</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,9 +232,15 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-4 border-t border-gray-200 bg-gray-50">
|
||||
<div class="flex items-center space-x-2 text-xs text-gray-500">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full badge-pulse"></div>
|
||||
<span>System Online</span>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center space-x-2 text-xs text-gray-500">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full badge-pulse"></div>
|
||||
<span>System Online</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||
<span id="user-email" class="text-xs text-gray-400 truncate max-w-[150px]"></span>
|
||||
<button id="logout-btn" class="text-xs text-red-500 hover:text-red-700 font-medium">Sign Out</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -361,8 +252,8 @@
|
||||
<div id="home-view" class="p-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-2">Welcome to KROW Launchpad</h2>
|
||||
<p class="text-gray-600">Your central hub for workforce management infrastructure</p>
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-2">Krow DevOps Launchpad</h2>
|
||||
<p class="text-gray-600">Central hub for KROW development and operations infrastructure</p>
|
||||
</div>
|
||||
|
||||
<div id="links-container" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
@@ -428,6 +319,130 @@
|
||||
<!-- Panzoom -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@panzoom/panzoom@4.5.1/dist/panzoom.min.js"></script>
|
||||
|
||||
<!-- Firebase Auth Logic -->
|
||||
<script type="module">
|
||||
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.8.0/firebase-app.js';
|
||||
import { getAuth, GoogleAuthProvider, signInWithPopup, onAuthStateChanged, signOut } from 'https://www.gstatic.com/firebasejs/10.8.0/firebase-auth.js';
|
||||
|
||||
// Elements
|
||||
const authLoading = document.getElementById('auth-loading');
|
||||
const loginScreen = document.getElementById('login-screen');
|
||||
const appContent = document.getElementById('app-content');
|
||||
const googleBtn = document.getElementById('google-signin-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const loginError = document.getElementById('login-error');
|
||||
const userEmailSpan = document.getElementById('user-email');
|
||||
|
||||
let pendingError = null;
|
||||
|
||||
// Initialize Firebase
|
||||
// Note: fetch('/__/firebase/init.json') works only on deployed Firebase Hosting or firebase serve
|
||||
async function initAuth() {
|
||||
try {
|
||||
let config;
|
||||
// Fetch config from hosting environment
|
||||
const res = await fetch('/__/firebase/init.json');
|
||||
if (!res.ok) throw new Error('Failed to load Firebase config. Ensure you are running via "make launchpad-dev" or deployed on Hosting.');
|
||||
config = await res.json();
|
||||
console.log("Firebase Config Loaded:", config.projectId);
|
||||
|
||||
const app = initializeApp(config);
|
||||
const auth = getAuth(app);
|
||||
|
||||
// Auth State Observer
|
||||
onAuthStateChanged(auth, async (user) => {
|
||||
if (user) {
|
||||
// User is signed in, check if allowed
|
||||
const allowed = await checkAccess(user.email);
|
||||
if (allowed) {
|
||||
showApp(user);
|
||||
} else {
|
||||
pendingError = `Access Denied: <b>${user.email}</b> is not authorized.<br>Please contact the Krow Internal Developers or an Administrator to request access.`;
|
||||
signOut(auth);
|
||||
}
|
||||
} else {
|
||||
// User is signed out
|
||||
if (pendingError) {
|
||||
showLogin(pendingError);
|
||||
pendingError = null;
|
||||
} else {
|
||||
showLogin();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Login Handler
|
||||
googleBtn.addEventListener('click', () => {
|
||||
// Clear previous errors
|
||||
loginError.classList.add('hidden');
|
||||
const provider = new GoogleAuthProvider();
|
||||
signInWithPopup(auth, provider).catch((error) => {
|
||||
console.error("Login failed:", error);
|
||||
showLogin("Login failed: " + error.message);
|
||||
});
|
||||
});
|
||||
|
||||
// Logout Handler
|
||||
logoutBtn.addEventListener('click', () => {
|
||||
signOut(auth);
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("Auth init error:", e);
|
||||
// Fallback for local dev without firebase serving
|
||||
if (window.location.hostname === 'localhost') {
|
||||
alert("Firebase Auth failed to load. Ensure you are using 'firebase serve' or have configured local auth.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAccess(email) {
|
||||
try {
|
||||
// Fetch the whitelist
|
||||
const res = await fetch('iap-users.txt');
|
||||
if (!res.ok) return false;
|
||||
const text = await res.text();
|
||||
|
||||
// Parse lines
|
||||
const lines = text.split('\n');
|
||||
const allowedEmails = lines
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'))
|
||||
.map(line => line.replace(/^user:/, '').trim());
|
||||
|
||||
return allowedEmails.includes(email);
|
||||
} catch (e) {
|
||||
console.error("Failed to check access list:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function showApp(user) {
|
||||
authLoading.style.display = 'none';
|
||||
loginScreen.style.display = 'none';
|
||||
appContent.classList.remove('hidden');
|
||||
userEmailSpan.textContent = user.email;
|
||||
|
||||
// Trigger link loading if not already done
|
||||
if (typeof loadLinks === 'function') loadLinks();
|
||||
}
|
||||
|
||||
function showLogin(errorMsg) {
|
||||
authLoading.style.display = 'none';
|
||||
appContent.classList.add('hidden');
|
||||
loginScreen.style.display = 'flex';
|
||||
if (errorMsg) {
|
||||
loginError.innerHTML = errorMsg;
|
||||
loginError.classList.remove('hidden');
|
||||
} else {
|
||||
loginError.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Start
|
||||
initAuth();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
let allDiagrams = [];
|
||||
let allDocuments = [];
|
||||
|
||||
Reference in New Issue
Block a user