diff --git a/.gitignore b/.gitignore index eb65529..4647a53 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,8 @@ dmypy.json .idea/ # Additional files from existing .gitignore -.envrc \ No newline at end of file +.envrc + +# Tailwind CSS generated artifacts +style.css +tailwindcss \ No newline at end of file diff --git a/build-css.sh b/build-css.sh new file mode 100755 index 0000000..b2260ab --- /dev/null +++ b/build-css.sh @@ -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 diff --git a/main.py b/main.py index 2d25ee8..6adc5dc 100644 --- a/main.py +++ b/main.py @@ -335,8 +335,8 @@ async def read_root(): ) csp = ( "default-src 'self'; " - f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; " - "style-src 'self' https://cdn.jsdelivr.net; " + f"script-src 'self' 'nonce-{nonce}'; " + "style-src 'self'; " "connect-src 'self' https://login.infomaniak.com https://api.coingecko.com; " "img-src 'self'; " "object-src 'none'; " @@ -389,6 +389,11 @@ async def favicon(): """Serve the favicon""" 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) async def get_client_config(): """Serve client configuration to frontend""" diff --git a/style-input.css b/style-input.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/style-input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..337e040 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./templates/index.html"], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/templates/index.html b/templates/index.html index 3d3048b..76f4361 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,54 +4,53 @@ OIDC Login - - + -
-
-
-
-
-

OIDC Login

+
+
+
+
+
+

OIDC Login

-
+

Login with your Infomaniak account

- +
-
+
@@ -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 @@ })(); - \ No newline at end of file + diff --git a/tests/test_main.py b/tests/test_main.py index 41b2f04..a2dec62 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -38,7 +38,7 @@ def test_root_serves_csp_with_nonce(): csp = response.headers.get("content-security-policy", "") assert "nonce-" 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 "login.infomaniak.com" in csp assert "api.coingecko.com" in csp