ref(frontend): Replace Bootstrap with Tailwind CSS

Swap the Bootstrap CDN for a self-hosted Tailwind CSS build, mirroring
the myice setup: standalone Tailwind CLI v3.4.17, a one-off build-css.sh
script, and a gitignored generated style.css.

- Add tailwind.config.js, style-input.css, build-css.sh at repo root
- Rewrite templates/index.html: drop Bootstrap link/script, link
  /style.css, convert all classes to Tailwind utilities; showAlert now
  uses a class lookup so the JIT scanner detects dynamic classes
- Add /style.css FileResponse route in main.py; drop cdn.jsdelivr.net
  from script-src/style-src in the CSP
- Flip test assertion to assert cdn.jsdelivr.net is absent from CSP

The generated style.css and tailwindcss binary are gitignored; run
./build-css.sh to regenerate (or --watch for dev).
This commit is contained in:
2026-07-08 14:10:37 +02:00
parent 1a807cfeb4
commit 9c7f2bde11
7 changed files with 100 additions and 34 deletions
+35 -30
View File
@@ -4,54 +4,53 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OIDC Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h1 class="text-center h3">OIDC Login</h1>
<div class="container mx-auto mt-5 px-4">
<div class="flex justify-center">
<div class="w-full md:w-1/2">
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<h1 class="text-center text-2xl font-bold">OIDC Login</h1>
</div>
<div class="card-body">
<div class="p-4">
<!-- Login Section -->
<section id="login-section" class="text-center">
<p>Login with your Infomaniak account</p>
<button id="login-btn" class="btn btn-primary w-100" aria-label="Login with Infomaniak">Login with Infomaniak</button>
<button id="login-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded transition" aria-label="Login with Infomaniak">Login with Infomaniak</button>
</section>
<!-- User Info Section -->
<section id="user-info" class="d-none">
<section id="user-info" class="hidden">
<div class="mb-4">
<h2 class="h5">Welcome, <span id="user-name">User</span>!</h2>
<h2 class="text-xl font-bold">Welcome, <span id="user-name">User</span>!</h2>
<p class="mb-0">Email: <span id="user-email"></span></p>
</div>
<!-- Secret Phrase Card -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="h5 mb-0">Secret Phrase</h3>
<button id="refresh-secret-btn" class="btn btn-secondary btn-sm" aria-label="Refresh secret phrase">Refresh</button>
<div class="bg-white rounded-lg shadow-md mb-4 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
<h3 class="text-xl font-bold mb-0">Secret Phrase</h3>
<button id="refresh-secret-btn" class="bg-gray-500 hover:bg-gray-600 text-white text-sm py-1 px-2 rounded transition" aria-label="Refresh secret phrase">Refresh</button>
</div>
<div class="card-body">
<div class="p-4">
<div id="secret-result" class="mb-0">Checking for secret phrase...</div>
</div>
</div>
<!-- Bitcoin Price Card -->
<div class="card mb-4">
<div class="card-header">
<h3 class="h5 mb-0">Bitcoin Price in CHF</h3>
<div class="bg-white rounded-lg shadow-md mb-4 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<h3 class="text-xl font-bold mb-0">Bitcoin Price in CHF</h3>
</div>
<div class="card-body">
<button id="fetch-btc-btn" class="btn btn-success mb-3" aria-label="Fetch Bitcoin Price">Fetch Bitcoin Price</button>
<div class="p-4">
<button id="fetch-btc-btn" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded mb-3 transition" aria-label="Fetch Bitcoin Price">Fetch Bitcoin Price</button>
<div id="btc-price-result" class="mb-0"></div>
</div>
</div>
<button id="logout-btn" class="btn btn-danger w-100" aria-label="Logout">Logout</button>
<button id="logout-btn" class="w-full bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded transition" aria-label="Logout">Logout</button>
</section>
</div>
</div>
@@ -157,12 +156,18 @@
* Show an alert message in the specified element
* @param {HTMLElement} element - Element to show the message in
* @param {string} message - Message to display
* @param {string} type - Bootstrap alert type (success, danger, warning, info)
* @param {string} type - Alert type (success, danger, warning, info)
*/
showAlert(element, message, type = 'info') {
const alertClasses = {
success: 'bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded',
danger: 'bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded',
warning: 'bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-3 rounded',
info: 'bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded'
};
element.replaceChildren();
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.className = alertClasses[type] || alertClasses.info;
alert.setAttribute('role', 'alert');
if (typeof message === 'string') {
alert.textContent = message;
@@ -427,8 +432,8 @@
* @param {Object} user - User object with email, first_name, last_name
*/
showUserInfo(user) {
elements.loginSection.classList.add('d-none');
elements.userInfo.classList.remove('d-none');
elements.loginSection.classList.add('hidden');
elements.userInfo.classList.remove('hidden');
// Display user's full name
elements.userName.textContent = utils.getUserDisplayName(user);
@@ -441,8 +446,8 @@
* Hide user information section
*/
hideUserInfo() {
elements.loginSection.classList.remove('d-none');
elements.userInfo.classList.add('d-none');
elements.loginSection.classList.remove('hidden');
elements.userInfo.classList.add('hidden');
},
/**
@@ -699,4 +704,4 @@
})();
</script>
</body>
</html>
</html>