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:
@@ -132,3 +132,7 @@ dmypy.json
|
|||||||
|
|
||||||
# Additional files from existing .gitignore
|
# Additional files from existing .gitignore
|
||||||
.envrc
|
.envrc
|
||||||
|
|
||||||
|
# Tailwind CSS generated artifacts
|
||||||
|
style.css
|
||||||
|
tailwindcss
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Build CSS using Tailwind CSS standalone CLI
|
||||||
|
# For local development, run:
|
||||||
|
# ./build-css.sh # one-off build
|
||||||
|
# ./build-css.sh --watch # watch mode
|
||||||
|
|
||||||
|
version="v3.4.17"
|
||||||
|
os="$(uname -s)"
|
||||||
|
arch="$(uname -m)"
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
Linux*) platform="linux";;
|
||||||
|
Darwin*) platform="macos";;
|
||||||
|
*) echo "Unsupported OS: $os"; exit 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$arch" in
|
||||||
|
x86_64) arch="x64";;
|
||||||
|
arm64) arch="arm64";;
|
||||||
|
aarch64) arch="arm64";;
|
||||||
|
*) echo "Unsupported architecture: $arch"; exit 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
binary="tailwindcss-${platform}-${arch}"
|
||||||
|
url="https://github.com/tailwindlabs/tailwindcss/releases/download/${version}/${binary}"
|
||||||
|
|
||||||
|
if [ ! -f "tailwindcss" ]; then
|
||||||
|
echo "Downloading Tailwind CSS standalone CLI (${version} ${platform}/${arch})..."
|
||||||
|
curl -sL "$url" -o tailwindcss
|
||||||
|
chmod +x tailwindcss
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${1:-}" == "--watch" ]; then
|
||||||
|
echo "Building CSS in watch mode..."
|
||||||
|
./tailwindcss -i ./style-input.css -o ./style.css --watch
|
||||||
|
else
|
||||||
|
echo "Building CSS..."
|
||||||
|
./tailwindcss -i ./style-input.css -o ./style.css --minify
|
||||||
|
fi
|
||||||
@@ -335,8 +335,8 @@ async def read_root():
|
|||||||
)
|
)
|
||||||
csp = (
|
csp = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
|
f"script-src 'self' 'nonce-{nonce}'; "
|
||||||
"style-src 'self' https://cdn.jsdelivr.net; "
|
"style-src 'self'; "
|
||||||
"connect-src 'self' https://login.infomaniak.com https://api.coingecko.com; "
|
"connect-src 'self' https://login.infomaniak.com https://api.coingecko.com; "
|
||||||
"img-src 'self'; "
|
"img-src 'self'; "
|
||||||
"object-src 'none'; "
|
"object-src 'none'; "
|
||||||
@@ -389,6 +389,11 @@ async def favicon():
|
|||||||
"""Serve the favicon"""
|
"""Serve the favicon"""
|
||||||
return FileResponse("templates/favicon.ico")
|
return FileResponse("templates/favicon.ico")
|
||||||
|
|
||||||
|
@app.get("/style.css")
|
||||||
|
async def stylesheet():
|
||||||
|
"""Serve the Tailwind-generated stylesheet"""
|
||||||
|
return FileResponse("style.css", media_type="text/css")
|
||||||
|
|
||||||
@app.get("/config", response_model=ClientConfig)
|
@app.get("/config", response_model=ClientConfig)
|
||||||
async def get_client_config():
|
async def get_client_config():
|
||||||
"""Serve client configuration to frontend"""
|
"""Serve client configuration to frontend"""
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./templates/index.html"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
+34
-29
@@ -4,54 +4,53 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>OIDC Login</title>
|
<title>OIDC Login</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link rel="stylesheet" href="/style.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
<div class="container mx-auto mt-5 px-4">
|
||||||
<div class="row justify-content-center">
|
<div class="flex justify-center">
|
||||||
<div class="col-md-6">
|
<div class="w-full md:w-1/2">
|
||||||
<div class="card">
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
<div class="card-header">
|
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||||
<h1 class="text-center h3">OIDC Login</h1>
|
<h1 class="text-center text-2xl font-bold">OIDC Login</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-4">
|
||||||
<!-- Login Section -->
|
<!-- Login Section -->
|
||||||
<section id="login-section" class="text-center">
|
<section id="login-section" class="text-center">
|
||||||
<p>Login with your Infomaniak account</p>
|
<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>
|
</section>
|
||||||
|
|
||||||
<!-- User Info Section -->
|
<!-- User Info Section -->
|
||||||
<section id="user-info" class="d-none">
|
<section id="user-info" class="hidden">
|
||||||
<div class="mb-4">
|
<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>
|
<p class="mb-0">Email: <span id="user-email"></span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Secret Phrase Card -->
|
<!-- Secret Phrase Card -->
|
||||||
<div class="card mb-4">
|
<div class="bg-white rounded-lg shadow-md mb-4 overflow-hidden">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
||||||
<h3 class="h5 mb-0">Secret Phrase</h3>
|
<h3 class="text-xl font-bold mb-0">Secret Phrase</h3>
|
||||||
<button id="refresh-secret-btn" class="btn btn-secondary btn-sm" aria-label="Refresh secret phrase">Refresh</button>
|
<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>
|
||||||
<div class="card-body">
|
<div class="p-4">
|
||||||
<div id="secret-result" class="mb-0">Checking for secret phrase...</div>
|
<div id="secret-result" class="mb-0">Checking for secret phrase...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bitcoin Price Card -->
|
<!-- Bitcoin Price Card -->
|
||||||
<div class="card mb-4">
|
<div class="bg-white rounded-lg shadow-md mb-4 overflow-hidden">
|
||||||
<div class="card-header">
|
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||||
<h3 class="h5 mb-0">Bitcoin Price in CHF</h3>
|
<h3 class="text-xl font-bold mb-0">Bitcoin Price in CHF</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-4">
|
||||||
<button id="fetch-btc-btn" class="btn btn-success mb-3" aria-label="Fetch Bitcoin Price">Fetch Bitcoin Price</button>
|
<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 id="btc-price-result" class="mb-0"></div>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,12 +156,18 @@
|
|||||||
* Show an alert message in the specified element
|
* Show an alert message in the specified element
|
||||||
* @param {HTMLElement} element - Element to show the message in
|
* @param {HTMLElement} element - Element to show the message in
|
||||||
* @param {string} message - Message to display
|
* @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') {
|
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();
|
element.replaceChildren();
|
||||||
const alert = document.createElement('div');
|
const alert = document.createElement('div');
|
||||||
alert.className = `alert alert-${type}`;
|
alert.className = alertClasses[type] || alertClasses.info;
|
||||||
alert.setAttribute('role', 'alert');
|
alert.setAttribute('role', 'alert');
|
||||||
if (typeof message === 'string') {
|
if (typeof message === 'string') {
|
||||||
alert.textContent = message;
|
alert.textContent = message;
|
||||||
@@ -427,8 +432,8 @@
|
|||||||
* @param {Object} user - User object with email, first_name, last_name
|
* @param {Object} user - User object with email, first_name, last_name
|
||||||
*/
|
*/
|
||||||
showUserInfo(user) {
|
showUserInfo(user) {
|
||||||
elements.loginSection.classList.add('d-none');
|
elements.loginSection.classList.add('hidden');
|
||||||
elements.userInfo.classList.remove('d-none');
|
elements.userInfo.classList.remove('hidden');
|
||||||
|
|
||||||
// Display user's full name
|
// Display user's full name
|
||||||
elements.userName.textContent = utils.getUserDisplayName(user);
|
elements.userName.textContent = utils.getUserDisplayName(user);
|
||||||
@@ -441,8 +446,8 @@
|
|||||||
* Hide user information section
|
* Hide user information section
|
||||||
*/
|
*/
|
||||||
hideUserInfo() {
|
hideUserInfo() {
|
||||||
elements.loginSection.classList.remove('d-none');
|
elements.loginSection.classList.remove('hidden');
|
||||||
elements.userInfo.classList.add('d-none');
|
elements.userInfo.classList.add('hidden');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ def test_root_serves_csp_with_nonce():
|
|||||||
csp = response.headers.get("content-security-policy", "")
|
csp = response.headers.get("content-security-policy", "")
|
||||||
assert "nonce-" in csp
|
assert "nonce-" in csp
|
||||||
assert "script-src" in csp
|
assert "script-src" in csp
|
||||||
assert "cdn.jsdelivr.net" in csp
|
assert "cdn.jsdelivr.net" not in csp
|
||||||
assert "connect-src" in csp
|
assert "connect-src" in csp
|
||||||
assert "login.infomaniak.com" in csp
|
assert "login.infomaniak.com" in csp
|
||||||
assert "api.coingecko.com" in csp
|
assert "api.coingecko.com" in csp
|
||||||
|
|||||||
Reference in New Issue
Block a user