mirror of
https://github.com/sassanix/Warracker.git
synced 2026-01-06 05:29:39 -06:00
Add OIDC SSO, exact expiration dates, memory optimization, and major UI/UX enhancements
This update introduces comprehensive OpenID Connect (OIDC) Single Sign-On support with dynamic configuration via the database and full frontend/backend integration. Key additions include: - OIDC SSO login via external providers (e.g., Google, Keycloak), with automatic user provisioning and session linking. - Admin settings UI for enabling/disabling SSO and managing provider credentials. - Provider-branded SSO buttons with dynamic labels, icons, and styles. - Exact warranty expiration date support alongside duration-based input, with full validation and UI enhancements. - Full UI responsiveness for warranty field updates, tag creation, and note editing. - Memory usage optimization for low-resource deployments via configurable modes (optimized, ultra-light, performance). - Numerous fixes for SSO authentication flow, UI sync issues, database constraints, and modal interactions. - Upgraded dependencies for security, performance, and compatibility (Flask 3.0.3, Gunicorn 23.0.0, etc.). - Frontend improvements: Chart.js loading fix, tooltips for long product names, and dark/light mode-compatible footer. This release significantly improves authentication flexibility, performance, and user experience across all major components.
This commit is contained in:
162
CHANGELOG.md
162
CHANGELOG.md
@@ -1,5 +1,165 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## 0.9.9.9 - 2025-06-01
|
||||
|
||||
### Added
|
||||
- **OIDC SSO (Single Sign-On) Integration:**
|
||||
- Implemented OpenID Connect (OIDC) authentication flow using Authlib, allowing users to log in via external OIDC providers (e.g., Google, Keycloak).
|
||||
- **Backend (`app.py`):**
|
||||
- OIDC client configuration (Client ID, Secret, Issuer URL, Scope, Provider Name) is dynamically loaded from the `site_settings` database table at application startup.
|
||||
- OIDC feature can be enabled/disabled globally via the `oidc_enabled` setting in the database.
|
||||
- Added OIDC-specific routes: `/api/oidc/login` (initiates login) and `/api/oidc/callback` (handles redirect from IdP).
|
||||
- User provisioning: If an OIDC user doesn't exist locally, a new user account is created. Existing OIDC users are logged in.
|
||||
- Updated Admin Settings API (`/api/admin/settings`) to manage these OIDC parameters.
|
||||
- Database migration `023_add_oidc_columns_to_users.sql` adds `oidc_sub`, `oidc_issuer` to `users` table and `login_method` to `user_sessions` table for linking local users to OIDC identities.
|
||||
- **Frontend:**
|
||||
- **Login Page (`login.html`, `auth.js`):** Added an "Login with SSO Provider" button that initiates the OIDC flow.
|
||||
- **Admin Settings Page (`settings-new.html`, `settings-new.js`):** New "OIDC SSO Configuration" section for admins to enable/disable OIDC and configure provider details.
|
||||
- **Auth Redirect Page (`auth-redirect.html`):** Handles the token received from the `/api/oidc/callback` and logs the user in.
|
||||
- **Dependencies (`requirements.txt`):** Added `Authlib` and `requests`.
|
||||
- _Files: `backend/app.py`, `backend/migrations/023_add_oidc_columns_to_users.sql`, `backend/requirements.txt`, `frontend/login.html`, `frontend/auth.js`, `frontend/settings-new.html`, `frontend/settings-new.js`, `frontend/auth-redirect.html`_
|
||||
- **SSO Registration Control:** When the site's registration setting is disabled, new users cannot register via SSO. Only existing users who already have accounts can use SSO to log in. This provides administrators with granular control over user registration while maintaining SSO functionality for existing users.
|
||||
- **Backend:** Enhanced OIDC callback handler to check the `registration_enabled` site setting before creating new user accounts via SSO.
|
||||
- **Frontend:** Added appropriate error message for users when SSO registration is blocked due to disabled registrations.
|
||||
- First user registration via SSO is still allowed regardless of the setting (same as regular registration).
|
||||
- _Files: `backend/oidc_handler.py`, `frontend/auth-redirect.html`_
|
||||
- **Provider-Specific SSO Button Branding:** Enhanced the SSO login button to show provider-specific branding and icons for well-known OIDC providers.
|
||||
- **Supported Providers:** Google, GitHub, Microsoft/Azure, Facebook, Twitter, LinkedIn, Apple, Discord, GitLab, Bitbucket, Keycloak, and Okta.
|
||||
- **Dynamic Button Text:** Shows "Login with Google", "Login with GitHub", etc., based on the configured `OIDC_PROVIDER_NAME`.
|
||||
- **Provider Icons:** Uses appropriate Font Awesome icons for each provider (e.g., Google logo for Google, GitHub logo for GitHub).
|
||||
- **Brand Colors:** Each provider button uses authentic brand colors with proper hover effects.
|
||||
- **Fallback Support:** Unknown providers display the generic "Login with SSO Provider" button with default styling.
|
||||
- **Environment Integration:** Works automatically with the `OIDC_PROVIDER_NAME` environment variable and database settings.
|
||||
- _Files: `frontend/login.html`, `backend/oidc_handler.py`_
|
||||
- **Exact Expiration Date Input:** Enhanced warranty entry to support exact expiration dates as an alternative to duration-based input.
|
||||
- **New Warranty Input Method:** Users can now choose between "Warranty Duration" (years/months/days) and "Exact Expiration Date" options when adding or editing warranties.
|
||||
- **UI Enhancements:** Added radio button selection for warranty entry method with smooth form field transitions and validation.
|
||||
- **Backend Support:** Both `add_warranty` and `update_warranty` API endpoints now handle `exact_expiration_date` parameter alongside duration fields.
|
||||
- **Smart Duration Display:** Warranty cards automatically calculate and display duration text even when using exact expiration dates, eliminating "N/A" values.
|
||||
- **Form Validation:** Enhanced validation ensures either exact date or duration is provided for non-lifetime warranties, with appropriate error messages.
|
||||
- **Date Calculation:** Added robust date calculation helper function that properly handles different month lengths, leap years, and edge cases.
|
||||
- _Files: `frontend/index.html`, `frontend/script.js`, `frontend/style.css`, `backend/app.py`_
|
||||
- **Complete Field Updates:** All warranty fields (product info, dates, serial numbers, tags, documents) update immediately in the interface.
|
||||
- **Fallback Safety:** Maintains fallback to server reload if local update fails, ensuring data consistency.
|
||||
- _Files: `frontend/script.js`_
|
||||
- **Memory Usage Optimization:** Significantly reduced RAM consumption for better performance on resource-constrained servers.
|
||||
- **Dynamic Memory Modes:** Configurable via `WARRACKER_MEMORY_MODE` environment variable with "optimized" (default), "ultra-light", and "performance" options.
|
||||
- **Optimized Mode:** 2 gevent workers with 128MB memory limits (~60-80MB total RAM usage).
|
||||
- **Ultra-Light Mode:** 1 sync worker with 64MB memory limit (~40-50MB total RAM usage for minimal servers).
|
||||
- **Performance Mode:** 4 gevent workers with 256MB memory limits (~150-200MB total RAM usage for high-performance servers).
|
||||
- **Worker Configuration:** Added memory limits per worker, connection pooling optimization, and preload_app for memory sharing.
|
||||
- **Database Pool:** Optimized connection pool from 10 to 4 maximum connections with memory-conscious settings.
|
||||
- **Flask Configuration:** Added memory-efficient settings including disabled JSON sorting, optimized session handling, and file serving optimizations.
|
||||
- **Dependency Addition:** Added gevent for more efficient async request handling compared to sync workers.
|
||||
- **Expected Reduction:** RAM usage configurable from ~40MB (ultra-light) to ~200MB (performance) based on server specifications.
|
||||
- _Files: `backend/gunicorn_config.py`, `backend/requirements.txt`, `backend/app.py`, `backend/db_handler.py`, `docker-compose.yml`_
|
||||
|
||||
### Changed
|
||||
- **Environment Variables for OIDC (Docker):** While OIDC configuration is primarily managed via the database through the admin UI, the `docker-compose.yml` can include placeholder environment variables for initial setup or documentation:
|
||||
- `OIDC_ENABLED` (e.g., `true` or `false`)
|
||||
- `OIDC_PROVIDER_NAME` (e.g., `google`, `keycloak`)
|
||||
- `OIDC_CLIENT_ID`
|
||||
- `OIDC_CLIENT_SECRET`
|
||||
- `OIDC_ISSUER_URL`
|
||||
- `OIDC_SCOPE` (e.g., `openid email profile`)
|
||||
- **`FRONTEND_URL` Environment Variable:** Emphasized the importance of correctly setting the `FRONTEND_URL` environment variable for the backend service in `docker-compose.yml` to ensure correct OIDC callback redirects.
|
||||
- _Files: `docker-compose.yml`, `Docker/docker-compose.yml`_
|
||||
|
||||
### Fixed
|
||||
- Resolved various startup and runtime errors related to OIDC client initialization in a multi-worker (Gunicorn) environment, particularly concerning database connection pool stability and consistent loading of OIDC settings.
|
||||
- Corrected OIDC metadata fetching by ensuring the proper Issuer URL is used (e.g., `https://accounts.google.com` for Google).
|
||||
- Resolved `TypeError: Cannot read properties of null (reading 'classList')` during logout on `about.html` by ensuring the `loadingContainer` element is present and accessible to `script.js`'s `showLoading()` function. Modified `showLoading()` and `hideLoading()` to dynamically find the container if not initially available.
|
||||
- Fixed `SyntaxError: Identifier 'AuthManager' has already been declared` on `about.html` by removing a duplicate import of `auth.js` from the `<head>`, ensuring it is loaded only once at the end of the `<body>`.
|
||||
- Standardized the user menu dropdown button ID to `userMenuBtn` across `index.html`, `status.html`, and `about.html`. Consolidated user menu JavaScript logic into `auth.js`, removing redundant code from `settings-new.js` and `fix-auth-buttons.js` to ensure consistent dropdown behavior.
|
||||
- _Files: `backend/app.py`, `frontend/about.html`, `frontend/script.js`, `frontend/index.html`, `frontend/status.html`, `frontend/auth.js`, `frontend/settings-new.js`, `frontend/fix-auth-buttons.js`_
|
||||
- **SSO Warranty Display Issue:** Fixed a critical timing problem where SSO users would see a blank warranty list after login. The issue was caused by a race condition where user preferences were loaded before user authentication was fully established.
|
||||
- Enhanced `auth-redirect.html` to fetch and store user information immediately after SSO login, ensuring `user_info` is available when the main application loads.
|
||||
- Fixed preference key prefix calculation to prevent mismatched user preference keys for SSO users.
|
||||
- _Files: `frontend/auth-redirect.html`, `frontend/auth.js`, `frontend/script.js`_
|
||||
- **SSO Tag Creation Issue:** Resolved an issue where SSO users could not create new tags due to authentication token handling problems.
|
||||
- Updated tag creation functions (`createTag`, `updateTag`, `deleteTag`) to use `window.auth.getToken()` instead of directly accessing localStorage.
|
||||
- Added enhanced error handling and debugging for tag creation API calls.
|
||||
- Ensured consistent token management across all tag-related operations.
|
||||
- _Files: `frontend/script.js`_
|
||||
- **Database Tag Constraint Issue:** Fixed database constraint violation that prevented users from creating tags with names that already existed for other users.
|
||||
- Created migration `024_fix_tags_constraint.sql` to update the database schema.
|
||||
- Dropped the old `tags_name_key` unique constraint that only considered tag names.
|
||||
- Added new `tags_name_user_id_key` constraint allowing the same tag name for different users.
|
||||
- Added proper user_id foreign key constraint and performance index.
|
||||
- _Files: `backend/migrations/024_fix_tags_constraint.sql`_
|
||||
- **About Page User Menu Issue:** Fixed user menu dropdown not functioning on the about page for SSO users.
|
||||
- Resolved script loading order conflicts that prevented user menu event listeners from being attached.
|
||||
- Moved inline authentication check from head to body to prevent timing conflicts with `auth.js`.
|
||||
- Added backup user menu handler specifically for the about page with enhanced debugging.
|
||||
- Fixed JavaScript error caused by `getEventListeners` function not being available in all browsers.
|
||||
- _Files: `frontend/about.html`, `frontend/auth.js`_
|
||||
- **About Page Logout Issue:** Resolved logout functionality errors on the about page.
|
||||
- Added safe loading spinner functions specifically for the about page to prevent `TypeError: Cannot read properties of null (reading 'classList')` error.
|
||||
- Implemented backup logout handler with multiple fallback levels for reliability.
|
||||
- Ensured logout works even when main auth system encounters errors.
|
||||
- _Files: `frontend/about.html`, `frontend/script.js`_
|
||||
- **Immediate Warranty Updates:** Optimized warranty editing for instant UI updates without server reloads.
|
||||
- **Performance Improvement:** Warranty changes now appear immediately in the UI instead of waiting for server data reload.
|
||||
- **Local Data Sync:** Edit modal updates local warranty data instantly upon successful save, eliminating loading delays.
|
||||
- **Robust Date Handling:** Enhanced date arithmetic for duration-based warranties with proper month/year overflow handling.
|
||||
- **Complete Field Updates:** All warranty fields (product info, dates, serial numbers, tags, documents) update immediately in the interface.
|
||||
- **Fallback Safety:** Maintains fallback to server reload if local update fails, ensuring data consistency.
|
||||
- _Files: `frontend/script.js`_
|
||||
- **Warranty Entry Method Persistence:** Fixed warranty entry method selection not being remembered correctly in edit modal.
|
||||
- **Issue:** When editing warranties that were created using "Exact Expiration Date" method, the edit modal would incorrectly switch to "Warranty Duration" method.
|
||||
- **Root Cause:** Display logic was overwriting original duration values (0,0,0 for exact date method), making method detection impossible.
|
||||
- **Solution:** Added `original_input_method` tracking to preserve the user's original warranty entry method choice.
|
||||
- **Data Separation:** Implemented separate `display_duration_*` fields for warranty card display while preserving original duration values for method detection.
|
||||
- **Enhanced Detection:** Edit modal now directly checks `original_input_method` field instead of guessing from duration data.
|
||||
- **Result:** Users can now edit warranties and the modal correctly remembers whether they originally used "Warranty Duration" or "Exact Expiration Date" method.
|
||||
- _Files: `frontend/script.js`_
|
||||
- **Notes Modal and Edit Modal Integration:** Fixed issues with notes editing workflow and seamless transition to warranty editing.
|
||||
- **Notes Editing for Exact Date Warranties:** Resolved issue where warranties created with exact expiration dates couldn't have their notes edited via the notes-link modal due to invalid duration validation.
|
||||
- **Stale Notes in Edit Modal:** Fixed problem where editing a warranty immediately after saving notes via the notes modal would show old note content instead of the newly saved content.
|
||||
- **Notes Modal UI Issues:** Fixed "Edit Notes" button showing outdated note content when clicked after saving new notes in the same modal session.
|
||||
- **Enhanced Notes Modal:** Added "Edit Warranty" button directly in the notes modal footer for seamless transition to full warranty editing without modal layering conflicts.
|
||||
- **Immediate Data Sync:** Enhanced notes saving to immediately update the global warranties array, ensuring edit modal always shows current data.
|
||||
- **Modal State Management:** Improved modal closing behavior when opening edit modal from notes modal to prevent UI conflicts.
|
||||
- _Files: `frontend/script.js`_
|
||||
- **Status Page Edit Modal Consistency:** Fixed inconsistency between edit warranty modals on index.html and status.html pages.
|
||||
- **Missing Warranty Entry Method Selection:** Added the "Warranty Entry Method" radio button selection to the status.html edit modal, allowing users to choose between "Warranty Duration" and "Exact Expiration Date" methods.
|
||||
- **Missing Exact Expiration Field:** Added the hidden "Exact Expiration Date" input field that appears when the exact date method is selected.
|
||||
- **Feature Parity:** The edit warranty modal on status.html now has identical functionality to the one on index.html, ensuring consistent user experience across both pages.
|
||||
- **Complete Modal Structure:** All tabs (Product, Warranty, Documents, Tags), form fields, and functionality now match exactly between both pages.
|
||||
- _Files: `frontend/status.html`_
|
||||
|
||||
### Dependencies
|
||||
- **Updated Python Dependencies:** Resolved `pkg_resources` deprecation warning by updating backend dependencies to modern versions.
|
||||
- **Gunicorn:** Updated from `20.1.0` to `23.0.0` - eliminates `pkg_resources` deprecation warning by using `importlib.metadata` instead.
|
||||
- **Flask:** Updated from `2.0.1` to `3.0.3` - includes security patches and improved compatibility.
|
||||
- **Werkzeug:** Updated from `2.0.1` to `3.0.3` - maintains compatibility with Flask 3.x.
|
||||
- **Other Dependencies:** Updated `flask-cors`, `Flask-Login`, `PyJWT`, `email-validator`, `python-dateutil`, `Authlib`, `requests`, and `gevent` to latest stable versions.
|
||||
- **Setuptools Protection:** Added `setuptools<81` pin to prevent future compatibility issues.
|
||||
- _Files: `backend/requirements.txt`_
|
||||
|
||||
### Frontend
|
||||
- **Chart.js Compatibility Fix:** Resolved "Chart is not defined" error on status page by replacing ES module version with UMD version.
|
||||
- **Issue:** Status page charts failed to load with `ReferenceError: Chart is not defined` because the local `chart.js` file was in ES module format.
|
||||
- **Root Cause:** ES modules require `<script type="module">` tags but the HTML was using regular `<script>` tags.
|
||||
- **Solution:** Replaced ES module version with locally downloaded UMD (Universal Module Definition) version from Chart.js v4.4.9, eliminating CDN dependency.
|
||||
- **Result:** Chart.js now loads properly with regular script tags and status page charts display correctly using the local file.
|
||||
- _Files: `frontend/chart.js`_
|
||||
- **Product Name Hover Tooltips:** Added hover tooltips to display full product names when they are truncated with ellipsis.
|
||||
- **Enhancement:** Long product names that get cut off with "..." now show the complete product name in a tooltip when hovered over.
|
||||
- **Coverage:** Works across all warranty display modes (grid view, list view, table view) on the main warranties page and the status page table.
|
||||
- **Implementation:** Added `title` attributes to warranty titles and product name cells using escaped HTML for security.
|
||||
- **Result:** Users can now see full product names without having to edit or expand warranty details.
|
||||
- _Files: `frontend/script.js`, `frontend/status.js`_
|
||||
- **Powered by Warracker Footer:** Added branded footer to all pages with dynamic theme support and GitHub repository link.
|
||||
- **Feature:** Added "Powered by Warracker" footer to all application pages linking to the GitHub repository.
|
||||
- **Theme Support:** Implemented JavaScript-based dynamic styling that automatically adapts to light/dark mode changes.
|
||||
- **Light Mode:** Light gray background (`#f5f5f5`), dark text (`#333333`), blue links (`#3498db`) matching the logo.
|
||||
- **Dark Mode:** Dark background (`#1a1a1a`), light text (`#e0e0e0`), light blue links (`#4dabf7`).
|
||||
- **Real-time Updates:** Uses MutationObserver to detect theme changes and update footer styling automatically.
|
||||
- **Cross-domain Compatibility:** Includes fallback inline styles and direct color values to ensure consistent display across different hosting environments.
|
||||
- _Files: All frontend HTML pages, `frontend/style.css`_
|
||||
|
||||
## [0.9.9.8] - 2025-05-24
|
||||
|
||||
|
||||
@@ -359,4 +519,4 @@
|
||||
- **Layout:** Updated `.panel-header` CSS to use Flexbox for aligning the title (`h2`) to the left and action buttons (`.panel-header-actions` containing Add Warranty and Refresh buttons) to the right (`frontend/style.css`).
|
||||
- **Layout:** Removed the `border-bottom` style from the `.panel-header` / `.warranties-panel h2` for a cleaner look (`frontend/style.css`).
|
||||
- **Branding:** Updated the website `<title>` to include "Warracker" (`frontend/index.html`).
|
||||
- **UI:** Added an "Add New Warranty" button (`#showAddWarrantyBtn`) to trigger the new modal (`frontend/index.html`
|
||||
- **UI:** Added an "Add New Warranty" button (`#showAddWarrantyBtn`) to trigger the new modal (`frontend/index.html`
|
||||
|
||||
152
Docker/.env.example
Normal file
152
Docker/.env.example
Normal file
@@ -0,0 +1,152 @@
|
||||
# Warracker Environment Variables Configuration
|
||||
# Copy this file to .env and customize the values for your deployment
|
||||
|
||||
|
||||
### ** Database Configuration**
|
||||
|
||||
# Database connection settings
|
||||
DB_HOST=warrackerdb
|
||||
DB_NAME=warranty_db
|
||||
DB_USER=warranty_user
|
||||
DB_PASSWORD=warranty_password
|
||||
|
||||
# Database admin credentials (used for migrations and setup)
|
||||
DB_ADMIN_USER=warracker_admin
|
||||
DB_ADMIN_PASSWORD=change_this_password_in_production
|
||||
|
||||
# PostgreSQL-specific settings (for the database container)
|
||||
POSTGRES_DB=warranty_db
|
||||
POSTGRES_USER=warranty_user
|
||||
POSTGRES_PASSWORD=warranty_password
|
||||
|
||||
|
||||
### Security Configuration**
|
||||
|
||||
# Application secret key for JWT tokens and Flask sessions
|
||||
# IMPORTANT: Generate a strong, unique secret key for production!
|
||||
SECRET_KEY=your_very_secret_flask_key_change_me
|
||||
|
||||
# JWT token expiration time (in hours)
|
||||
JWT_EXPIRATION_HOURS=24
|
||||
|
||||
|
||||
### Email/SMTP Configuration**
|
||||
|
||||
# SMTP server settings for sending notifications and password resets
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=youremail@gmail.com
|
||||
SMTP_PASSWORD=your_email_password
|
||||
|
||||
# Optional SMTP settings
|
||||
SMTP_USE_TLS=true
|
||||
SMTP_USE_SSL=false
|
||||
SMTP_SENDER_EMAIL=noreply@warracker.com
|
||||
|
||||
|
||||
### **URL Configuration**
|
||||
|
||||
# Frontend URL (used for redirects and email links)
|
||||
# IMPORTANT: Must match your public-facing URL for OIDC and email links to work
|
||||
FRONTEND_URL=http://localhost:8005
|
||||
|
||||
# Application base URL (used for links in emails and redirects)
|
||||
APP_BASE_URL=http://localhost:8005
|
||||
|
||||
|
||||
### **File Upload Configuration**
|
||||
|
||||
# Maximum file upload size in megabytes
|
||||
MAX_UPLOAD_MB=32
|
||||
|
||||
# Nginx maximum body size (should match or exceed MAX_UPLOAD_MB)
|
||||
NGINX_MAX_BODY_SIZE_VALUE=32M
|
||||
|
||||
|
||||
### **Performance & Memory Configuration**
|
||||
|
||||
# Memory optimization mode
|
||||
# Options: optimized (default), ultra-light, performance
|
||||
# - optimized: 2 workers, ~60-80MB RAM usage (recommended for most deployments)
|
||||
# - ultra-light: 1 worker, ~40-50MB RAM usage (for very limited resources)
|
||||
# - performance: 4 workers, ~150-200MB RAM usage (for high-traffic deployments)
|
||||
WARRACKER_MEMORY_MODE=optimized
|
||||
|
||||
|
||||
### **OIDC/SSO Configuration (Optional)**
|
||||
|
||||
# Enable/disable OIDC SSO functionality
|
||||
OIDC_ENABLED=false
|
||||
|
||||
# OIDC Provider settings
|
||||
# Provider name (affects button branding: google, github, microsoft, keycloak, etc.)
|
||||
OIDC_PROVIDER_NAME=oidc
|
||||
|
||||
# OIDC client credentials (obtain from your OIDC provider)
|
||||
OIDC_CLIENT_ID=
|
||||
OIDC_CLIENT_SECRET=
|
||||
|
||||
# OIDC issuer URL (e.g., https://accounts.google.com)
|
||||
OIDC_ISSUER_URL=
|
||||
|
||||
# OIDC scope (space-separated list of scopes)
|
||||
OIDC_SCOPE=openid email profile
|
||||
|
||||
### **Development/Debugging Configuration (Optional)**
|
||||
|
||||
# Flask environment (development/production)
|
||||
FLASK_ENV=production
|
||||
|
||||
# Flask debug mode (true/false)
|
||||
FLASK_DEBUG=false
|
||||
|
||||
# Flask run port (for development)
|
||||
FLASK_RUN_PORT=5000
|
||||
|
||||
# Python unbuffered output (helpful for Docker logs)
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
|
||||
### **Example Configurations**
|
||||
|
||||
**Gmail SMTP:**
|
||||
```bash
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=youremail@gmail.com
|
||||
SMTP_PASSWORD=your_app_password
|
||||
SMTP_USE_TLS=true
|
||||
```
|
||||
|
||||
**Google OIDC:**
|
||||
```bash
|
||||
OIDC_ENABLED=true
|
||||
OIDC_PROVIDER_NAME=google
|
||||
OIDC_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
|
||||
OIDC_CLIENT_SECRET=your_google_client_secret
|
||||
OIDC_ISSUER_URL=https://accounts.google.com
|
||||
OIDC_SCOPE=openid email profile
|
||||
```
|
||||
|
||||
**Production deployment:**
|
||||
```bash
|
||||
SECRET_KEY=super_long_random_string_generated_securely
|
||||
DB_PASSWORD=strong_database_password_123
|
||||
DB_ADMIN_PASSWORD=different_strong_admin_password_456
|
||||
FRONTEND_URL=https://warracker.yourdomain.com
|
||||
APP_BASE_URL=https://warracker.yourdomain.com
|
||||
SMTP_HOST=smtp.yourdomain.com
|
||||
SMTP_USERNAME=warracker@yourdomain.com
|
||||
MAX_UPLOAD_MB=64
|
||||
NGINX_MAX_BODY_SIZE_VALUE=64M
|
||||
WARRACKER_MEMORY_MODE=performance
|
||||
```
|
||||
|
||||
## **How to Use**
|
||||
|
||||
1. **Copy this configuration** into a file named `.env` in your Docker folder
|
||||
2. **Customize the values** according to your specific deployment needs
|
||||
3. **Generate strong passwords** for production use, especially for `SECRET_KEY`, `DB_PASSWORD`, and `DB_ADMIN_PASSWORD`
|
||||
4. **Set your domain URLs** correctly for `FRONTEND_URL` and `APP_BASE_URL` if deploying publicly
|
||||
5. **Configure SMTP** if you want email functionality for password resets and notifications
|
||||
6. **Set up OIDC/SSO** if you want single sign-on capabilities
|
||||
@@ -17,6 +17,19 @@ services:
|
||||
- SECRET_KEY=${APP_SECRET_KEY:-your_strong_default_secret_key_here}
|
||||
- MAX_UPLOAD_MB=32 # Example: Set max upload size to 32MB
|
||||
- NGINX_MAX_BODY_SIZE_VALUE=32M # For Nginx, ensure this matches MAX_UPLOAD_MB in concept (e.g., 32M)
|
||||
# OIDC SSO Configuration (User needs to set these based on their OIDC provider)
|
||||
- OIDC_PROVIDER_NAME=${OIDC_PROVIDER_NAME:-oidc}
|
||||
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-} # e.g., your_oidc_client_id_from_provider
|
||||
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-} # e.g., your_oidc_client_secret_from_provider
|
||||
- OIDC_ISSUER_URL=${OIDC_ISSUER_URL:-} # e.g., https://your-oidc-provider.com/auth/realms/your-realm
|
||||
- OIDC_SCOPE=${OIDC_SCOPE:-openid email profile}
|
||||
# URL settings (Important for OIDC redirects and email links)
|
||||
# Ensure these point to the public-facing URL of your application
|
||||
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:8005}
|
||||
- APP_BASE_URL=${APP_BASE_URL:-http://localhost:8005}
|
||||
# Memory optimization settings
|
||||
- WARRACKER_MEMORY_MODE=${WARRACKER_MEMORY_MODE:-optimized} # Options: optimized (default), ultra-light
|
||||
- PYTHONUNBUFFERED=1
|
||||
# - FLASK_DEBUG=0
|
||||
depends_on:
|
||||
warrackerdb:
|
||||
@@ -41,4 +54,4 @@ services:
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
warracker_uploads:
|
||||
warracker_uploads:
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -28,9 +28,19 @@ RUN pip install --no-cache-dir --upgrade pip
|
||||
COPY backend/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy backend code
|
||||
# Copy main application and config files to /app
|
||||
COPY backend/app.py .
|
||||
COPY backend/gunicorn_config.py .
|
||||
|
||||
# Create the backend package directory in /app and copy modules into it
|
||||
RUN mkdir -p /app/backend
|
||||
COPY backend/__init__.py /app/backend/
|
||||
COPY backend/auth_utils.py /app/backend/
|
||||
COPY backend/db_handler.py /app/backend/
|
||||
COPY backend/extensions.py /app/backend/
|
||||
COPY backend/oidc_handler.py /app/backend/
|
||||
|
||||
# Copy other utility scripts and migrations
|
||||
COPY backend/fix_permissions.py .
|
||||
COPY backend/fix_permissions.sql .
|
||||
COPY backend/migrations/ /app/migrations/
|
||||
@@ -39,6 +49,7 @@ COPY backend/migrations/ /app/migrations/
|
||||
COPY frontend/*.html /var/www/html/
|
||||
COPY frontend/*.js /var/www/html/
|
||||
COPY frontend/*.css /var/www/html/
|
||||
COPY frontend/manifest.json /var/www/html/manifest.json
|
||||
# Add favicon and images
|
||||
COPY frontend/favicon.ico /var/www/html/
|
||||
COPY frontend/img/ /var/www/html/img/
|
||||
|
||||
1
backend/__init__.py
Normal file
1
backend/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# This file makes 'backend' a Python package
|
||||
BIN
backend/__pycache__/app.cpython-39.pyc
Normal file
BIN
backend/__pycache__/app.cpython-39.pyc
Normal file
Binary file not shown.
1000
backend/app.py
1000
backend/app.py
File diff suppressed because it is too large
Load Diff
19
backend/auth_utils.py
Normal file
19
backend/auth_utils.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# backend/auth_utils.py
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from flask import current_app # To access app.config
|
||||
|
||||
def generate_token(user_id):
|
||||
"""Generate a JWT token for the user"""
|
||||
payload = {
|
||||
'exp': datetime.utcnow() + current_app.config['JWT_EXPIRATION_DELTA'],
|
||||
'iat': datetime.utcnow(),
|
||||
'sub': user_id
|
||||
}
|
||||
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
|
||||
|
||||
# Note: You can later move decode_token, token_required, admin_required here too.
|
||||
# For token_required and admin_required, you'll also need:
|
||||
# from functools import wraps
|
||||
# from flask import request, jsonify
|
||||
# And you'd need to import get_db_connection, release_db_connection from your db_handler.
|
||||
91
backend/db_handler.py
Normal file
91
backend/db_handler.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# backend/db_handler.py
|
||||
import os
|
||||
import psycopg2
|
||||
from psycopg2 import pool
|
||||
import logging
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# PostgreSQL connection details
|
||||
DB_HOST = os.environ.get('DB_HOST', 'warrackerdb')
|
||||
DB_NAME = os.environ.get('DB_NAME', 'warranty_db')
|
||||
DB_USER = os.environ.get('DB_USER', 'warranty_user')
|
||||
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'warranty_password')
|
||||
|
||||
connection_pool = None # Global connection pool for this module
|
||||
|
||||
def init_db_pool(max_retries=5, retry_delay=5):
|
||||
global connection_pool # Ensure we're modifying the global variable in this module
|
||||
attempt = 0
|
||||
last_exception = None
|
||||
|
||||
if connection_pool is not None:
|
||||
logger.info("[DB_HANDLER] Database connection pool already initialized.")
|
||||
return connection_pool
|
||||
|
||||
while attempt < max_retries:
|
||||
try:
|
||||
logger.info(f"[DB_HANDLER] Attempting to initialize database pool (attempt {attempt+1}/{max_retries})")
|
||||
# Optimized connection pool for memory efficiency
|
||||
connection_pool = pool.SimpleConnectionPool(
|
||||
1, 4, # Reduced from 1,10 to 1,4 for memory efficiency
|
||||
host=DB_HOST,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
# Memory optimization settings
|
||||
connect_timeout=10, # Connection timeout
|
||||
application_name='warracker_optimized' # Identify connections
|
||||
)
|
||||
logger.info("[DB_HANDLER] Database connection pool initialized successfully.")
|
||||
return connection_pool # Return the pool for external check if needed
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
logger.error(f"[DB_HANDLER] Database connection pool initialization error: {e}")
|
||||
logger.info(f"[DB_HANDLER] Retrying in {retry_delay} seconds...")
|
||||
time.sleep(retry_delay)
|
||||
attempt += 1
|
||||
|
||||
logger.error(f"[DB_HANDLER] Failed to initialize database pool after {max_retries} attempts.")
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
else:
|
||||
raise Exception("Unknown error creating database pool")
|
||||
|
||||
def get_db_connection():
|
||||
global connection_pool
|
||||
if connection_pool is None:
|
||||
logger.error("[DB_HANDLER] Database connection pool is None. Attempting to re-initialize.")
|
||||
init_db_pool() # Attempt to initialize it
|
||||
if connection_pool is None: # If still None after attempt
|
||||
logger.critical("[DB_HANDLER] CRITICAL: Database pool re-initialization failed.")
|
||||
raise Exception("Database connection pool is not initialized and could not be re-initialized.")
|
||||
try:
|
||||
return connection_pool.getconn()
|
||||
except Exception as e:
|
||||
logger.error(f"[DB_HANDLER] Error getting connection from pool: {e}")
|
||||
raise
|
||||
|
||||
def release_db_connection(conn):
|
||||
global connection_pool
|
||||
if connection_pool:
|
||||
try:
|
||||
connection_pool.putconn(conn)
|
||||
except Exception as e:
|
||||
logger.error(f"[DB_HANDLER] Error releasing connection to pool: {e}. Connection state: {conn.closed if conn else 'N/A'}")
|
||||
# If putconn fails, the connection might be broken or the pool is in a bad state.
|
||||
# Attempt to close the connection directly as a fallback.
|
||||
if conn and not conn.closed:
|
||||
try:
|
||||
conn.close()
|
||||
logger.info("[DB_HANDLER] Connection closed directly after putconn failure.")
|
||||
except Exception as close_err:
|
||||
logger.error(f"[DB_HANDLER] Error closing connection directly after putconn failed: {close_err}")
|
||||
else:
|
||||
logger.warning("[DB_HANDLER] Connection pool is None, cannot release connection to pool. Attempting to close directly.")
|
||||
if conn and not conn.closed:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.error(f"[DB_HANDLER] Error closing connection directly (pool was None): {e}")
|
||||
4
backend/extensions.py
Normal file
4
backend/extensions.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# backend/extensions.py
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
|
||||
oauth = OAuth()
|
||||
@@ -3,20 +3,58 @@
|
||||
|
||||
"""
|
||||
Gunicorn configuration file for Warracker application.
|
||||
This ensures scheduler only runs in one worker process.
|
||||
Optimized for memory efficiency with configurable modes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import multiprocessing
|
||||
|
||||
# Server configurations
|
||||
# Server configurations - Dynamic based on memory mode
|
||||
bind = "0.0.0.0:5000"
|
||||
workers = 4
|
||||
worker_class = "sync"
|
||||
|
||||
# Check memory mode from environment variable
|
||||
memory_mode = os.environ.get('WARRACKER_MEMORY_MODE', 'optimized').lower()
|
||||
|
||||
if memory_mode == 'ultra-light':
|
||||
# Ultra-lightweight configuration for very memory-constrained environments
|
||||
workers = 1 # Single worker for minimal memory usage (~40-50MB total)
|
||||
worker_class = "sync" # Sync worker for lowest memory overhead
|
||||
worker_connections = 50 # Reduced connections
|
||||
max_requests = 500 # More frequent worker restarts to prevent memory leaks
|
||||
worker_rlimit_as = 67108864 # 64MB per worker limit
|
||||
print("Using ULTRA-LIGHT memory mode - minimal RAM usage, lower concurrency")
|
||||
elif memory_mode == 'performance':
|
||||
# High-performance configuration for servers with plenty of RAM
|
||||
workers = 4 # Original worker count for maximum concurrency
|
||||
worker_class = "gevent" # Efficient async I/O handling
|
||||
worker_connections = 200 # Higher connection limit per worker
|
||||
max_requests = 2000 # Less frequent restarts for better performance
|
||||
worker_rlimit_as = 268435456 # 256MB per worker limit
|
||||
print("Using PERFORMANCE memory mode - maximum concurrency and performance")
|
||||
else:
|
||||
# Default optimized configuration for balanced performance and memory usage
|
||||
workers = 2 # Reduced from 4 to save ~75MB RAM
|
||||
worker_class = "gevent" # More memory efficient than sync workers
|
||||
worker_connections = 100 # Limit concurrent connections per worker
|
||||
max_requests = 1000 # Restart workers after handling requests to prevent memory leaks
|
||||
worker_rlimit_as = 134217728 # 128MB per worker limit
|
||||
print("Using OPTIMIZED memory mode - balanced RAM usage and performance")
|
||||
|
||||
# Common settings for both modes
|
||||
timeout = 120
|
||||
keepalive = 5
|
||||
max_requests_jitter = 50 # Add randomness to prevent thundering herd
|
||||
|
||||
# Set worker environment variables
|
||||
# Enhanced settings for file handling to prevent Content-Length mismatches
|
||||
limit_request_line = 8190 # Increase request line limit
|
||||
limit_request_fields = 200 # Increase header fields limit
|
||||
limit_request_field_size = 8190 # Increase header field size limit
|
||||
|
||||
# Memory management (common to both modes)
|
||||
preload_app = True # Share memory between workers (saves RAM)
|
||||
worker_tmp_dir = "/dev/shm" # Use RAM disk for worker temporary files
|
||||
|
||||
# Process management callbacks
|
||||
def worker_int(worker):
|
||||
"""Called just after a worker exited on SIGINT or SIGQUIT."""
|
||||
print(f"Worker {worker.pid} received SIGINT/SIGQUIT")
|
||||
@@ -31,18 +69,24 @@ def worker_exit(server, worker):
|
||||
|
||||
def on_starting(server):
|
||||
"""Called just before the master process is initialized."""
|
||||
print("Server is starting")
|
||||
print("Server is starting with memory-optimized configuration")
|
||||
|
||||
def post_fork(server, worker):
|
||||
"""Called just after a worker has been forked."""
|
||||
os.environ["GUNICORN_WORKER_ID"] = str(worker.age - 1) # Worker ID starts at 0
|
||||
os.environ["GUNICORN_WORKER_ID"] = str(worker.age - 1)
|
||||
os.environ["GUNICORN_WORKER_PROCESS_NAME"] = f"worker-{worker.age - 1}"
|
||||
os.environ["GUNICORN_WORKER_CLASS"] = worker_class
|
||||
|
||||
print(f"Worker {worker.pid} (ID: {worker.age - 1}) forked")
|
||||
print(f"Worker {worker.pid} (ID: {worker.age - 1}) forked with memory optimization")
|
||||
|
||||
def pre_fork(server, worker):
|
||||
"""Called just before a worker is forked."""
|
||||
print(f"Forking worker #{worker.age}")
|
||||
|
||||
print(f"Gunicorn configuration loaded with {workers} workers")
|
||||
print(f"Gunicorn configuration loaded: {workers} {worker_class} workers in {memory_mode.upper()} mode")
|
||||
print(f"Memory limit per worker: {worker_rlimit_as // 1024 // 1024}MB, Max connections: {worker_connections if 'worker_connections' in locals() else 'N/A'}")
|
||||
|
||||
# To switch memory modes, set WARRACKER_MEMORY_MODE environment variable:
|
||||
# - "optimized" (default): 2 gevent workers, balanced performance and memory usage (~60-80MB)
|
||||
# - "ultra-light": 1 sync worker, minimal memory usage (~40-50MB, lower concurrency)
|
||||
# - "performance": 4 gevent workers, high-performance mode (~200MB)
|
||||
31
backend/migrations/023_add_oidc_columns_to_users.sql
Normal file
31
backend/migrations/023_add_oidc_columns_to_users.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
-- Add oidc_sub and oidc_issuer columns to the users table
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS oidc_sub VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS oidc_issuer VARCHAR(255);
|
||||
|
||||
-- Make password_hash nullable for OIDC-only users
|
||||
-- This assumes the column 'password_hash' exists.
|
||||
-- If it might not, the original DO $$ block with IF EXISTS for the column is safer.
|
||||
-- However, given it's a core user attribute, it should exist.
|
||||
ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL;
|
||||
|
||||
-- Add a unique constraint for oidc_sub and oidc_issuer.
|
||||
-- This will fail if the constraint already exists, which is acceptable as the migration runner
|
||||
-- should catch the error and skip/log if the migration was already partially applied.
|
||||
-- A more robust way is to check information_schema, but we're simplifying due to `RAISE` issues.
|
||||
ALTER TABLE users ADD CONSTRAINT uq_users_oidc_sub_issuer UNIQUE (oidc_sub, oidc_issuer);
|
||||
|
||||
-- Add login_method to user_sessions table
|
||||
ALTER TABLE user_sessions
|
||||
ADD COLUMN IF NOT EXISTS login_method VARCHAR(50) DEFAULT 'local';
|
||||
|
||||
-- Update existing sessions to 'local' if login_method is NULL
|
||||
UPDATE user_sessions
|
||||
SET login_method = 'local'
|
||||
WHERE login_method IS NULL;
|
||||
|
||||
-- Make login_method not nullable after updating existing rows
|
||||
-- This assumes the column 'login_method' now exists.
|
||||
ALTER TABLE user_sessions ALTER COLUMN login_method SET NOT NULL;
|
||||
|
||||
-- End of migration
|
||||
52
backend/migrations/024_fix_tags_constraint.sql
Normal file
52
backend/migrations/024_fix_tags_constraint.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
-- Fix tags table constraints to allow per-user tag names
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Check if user_id column exists, if not add it
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'tags' AND column_name = 'user_id'
|
||||
) THEN
|
||||
-- Add user_id column as nullable first
|
||||
ALTER TABLE tags ADD COLUMN user_id INTEGER;
|
||||
|
||||
-- Update existing tags to have user_id = 1 (assuming admin user)
|
||||
UPDATE tags SET user_id = 1 WHERE user_id IS NULL;
|
||||
|
||||
-- Make the column NOT NULL
|
||||
ALTER TABLE tags ALTER COLUMN user_id SET NOT NULL;
|
||||
|
||||
-- Add foreign key constraint
|
||||
ALTER TABLE tags ADD CONSTRAINT fk_tags_user_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
|
||||
-- Drop the old unique constraint on name only (if it exists)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_name = 'tags' AND constraint_name = 'tags_name_key'
|
||||
) THEN
|
||||
ALTER TABLE tags DROP CONSTRAINT tags_name_key;
|
||||
RAISE NOTICE 'Dropped old tags_name_key constraint';
|
||||
END IF;
|
||||
|
||||
-- Add new unique constraint on name and user_id (if it doesn't exist)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_name = 'tags' AND constraint_name = 'tags_name_user_id_key'
|
||||
) THEN
|
||||
ALTER TABLE tags ADD CONSTRAINT tags_name_user_id_key
|
||||
UNIQUE (name, user_id);
|
||||
RAISE NOTICE 'Added new tags_name_user_id_key constraint';
|
||||
END IF;
|
||||
|
||||
-- Create index for faster lookups (if it doesn't exist)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'tags' AND indexname = 'idx_tags_user_id'
|
||||
) THEN
|
||||
CREATE INDEX idx_tags_user_id ON tags (user_id);
|
||||
RAISE NOTICE 'Created idx_tags_user_id index';
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE 'Tags table constraint fix completed successfully';
|
||||
END $$;
|
||||
253
backend/oidc_handler.py
Normal file
253
backend/oidc_handler.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# backend/oidc_handler.py
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime # Ensure timedelta is imported if used, though not in this snippet
|
||||
from flask import Blueprint, jsonify, redirect, url_for, current_app, request, session
|
||||
|
||||
# Import shared extensions and utilities
|
||||
from backend.extensions import oauth
|
||||
from backend.db_handler import get_db_connection, release_db_connection
|
||||
from backend.auth_utils import generate_token
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__) # Or use current_app.logger inside routes
|
||||
|
||||
oidc_bp = Blueprint('oidc', __name__) # url_prefix will be set when registering in app.py
|
||||
|
||||
@oidc_bp.route('/oidc/login') # Original path was /api/oidc/login
|
||||
def oidc_login_route():
|
||||
if not current_app.config.get('OIDC_ENABLED'):
|
||||
logger.warning("[OIDC_HANDLER] OIDC login attempt while OIDC is disabled.")
|
||||
return jsonify({'message': 'OIDC (SSO) login is not enabled.'}), 403
|
||||
|
||||
oidc_provider_name = current_app.config.get('OIDC_PROVIDER_NAME')
|
||||
if not oidc_provider_name:
|
||||
logger.error("[OIDC_HANDLER] OIDC is enabled but provider name not configured.")
|
||||
return jsonify({'message': 'OIDC provider not configured correctly.'}), 500
|
||||
|
||||
# Corrected url_for to use blueprint name
|
||||
redirect_uri = url_for('oidc.oidc_callback_route', _external=True)
|
||||
|
||||
# HTTPS check for production
|
||||
if os.environ.get('FLASK_ENV') == 'production' and not redirect_uri.startswith('https'):
|
||||
redirect_uri = redirect_uri.replace('http://', 'https://', 1)
|
||||
|
||||
logger.info(f"[OIDC_HANDLER] /oidc/login redirect_uri: {redirect_uri}")
|
||||
return oauth.create_client(oidc_provider_name).authorize_redirect(redirect_uri)
|
||||
|
||||
@oidc_bp.route('/oidc/callback') # Original path was /api/oidc/callback
|
||||
def oidc_callback_route():
|
||||
if not current_app.config.get('OIDC_ENABLED'):
|
||||
logger.warning("[OIDC_HANDLER] OIDC callback received while OIDC is disabled.")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', current_app.config.get('APP_BASE_URL', 'http://localhost:8080')).rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=oidc_disabled")
|
||||
|
||||
oidc_provider_name = current_app.config.get('OIDC_PROVIDER_NAME')
|
||||
if not oidc_provider_name:
|
||||
logger.error("[OIDC_HANDLER] OIDC provider name not configured for callback.")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', current_app.config.get('APP_BASE_URL', 'http://localhost:8080')).rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=oidc_misconfigured")
|
||||
|
||||
client = oauth.create_client(oidc_provider_name)
|
||||
try:
|
||||
token_data = client.authorize_access_token()
|
||||
except Exception as e:
|
||||
logger.error(f"[OIDC_HANDLER] OIDC callback error authorizing access token: {e}")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=token_exchange_failed")
|
||||
|
||||
if not token_data:
|
||||
logger.error("[OIDC_HANDLER] OIDC callback: Failed to retrieve access token.")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=token_missing")
|
||||
|
||||
userinfo = token_data.get('userinfo')
|
||||
if not userinfo:
|
||||
try:
|
||||
userinfo = client.userinfo(token=token_data)
|
||||
except Exception as e:
|
||||
logger.error(f"[OIDC_HANDLER] OIDC callback error fetching userinfo: {e}")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=userinfo_fetch_failed")
|
||||
|
||||
if not userinfo:
|
||||
logger.error("[OIDC_HANDLER] OIDC callback: Failed to retrieve userinfo.")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=userinfo_missing")
|
||||
|
||||
oidc_subject = userinfo.get('sub')
|
||||
oidc_issuer = userinfo.get('iss')
|
||||
|
||||
if not oidc_subject:
|
||||
logger.error("[OIDC_HANDLER] OIDC callback: 'sub' (subject) missing in userinfo.")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=subject_missing")
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
# Check for existing OIDC user
|
||||
cur.execute("SELECT id, username, email, is_admin FROM users WHERE oidc_sub = %s AND oidc_issuer = %s AND is_active = TRUE",
|
||||
(oidc_subject, oidc_issuer))
|
||||
user_db_data = cur.fetchone()
|
||||
|
||||
user_id = None
|
||||
is_new_user = False
|
||||
|
||||
if user_db_data:
|
||||
user_id = user_db_data[0]
|
||||
logger.info(f"[OIDC_HANDLER] Existing OIDC user found with ID {user_id} for sub {oidc_subject}")
|
||||
else:
|
||||
# Check if registration is enabled before creating new users
|
||||
cur.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'site_settings'
|
||||
)
|
||||
""")
|
||||
table_exists = cur.fetchone()[0]
|
||||
|
||||
registration_enabled = True
|
||||
if table_exists:
|
||||
# Get registration_enabled setting
|
||||
cur.execute("SELECT value FROM site_settings WHERE key = 'registration_enabled'")
|
||||
result = cur.fetchone()
|
||||
|
||||
if result:
|
||||
registration_enabled = result[0].lower() == 'true'
|
||||
|
||||
# Check if there are any users (first user can register regardless of setting)
|
||||
cur.execute('SELECT COUNT(*) FROM users')
|
||||
user_count = cur.fetchone()[0]
|
||||
|
||||
# If registration is disabled and this is not the first user, deny SSO signup
|
||||
if not registration_enabled and user_count > 0:
|
||||
logger.warning(f"[OIDC_HANDLER] New OIDC user registration denied - registrations are disabled. Subject: {oidc_subject}")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', current_app.config.get('APP_BASE_URL', 'http://localhost:8080')).rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=registration_disabled")
|
||||
|
||||
# New user provisioning
|
||||
is_new_user = True
|
||||
email = userinfo.get('email')
|
||||
if not email:
|
||||
logger.error("[OIDC_HANDLER] 'email' missing in userinfo for new OIDC user.")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=email_missing_for_new_user")
|
||||
|
||||
# Check for email conflict with local account
|
||||
cur.execute("SELECT id FROM users WHERE email = %s AND (oidc_sub IS NULL OR oidc_issuer IS NULL)", (email,))
|
||||
if cur.fetchone():
|
||||
logger.warning(f"[OIDC_HANDLER] Email {email} already exists for a local account. OIDC user cannot be created.")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=email_conflict_local_account")
|
||||
|
||||
username = userinfo.get('preferred_username') or userinfo.get('name') or email.split('@')[0]
|
||||
# Ensure username uniqueness
|
||||
cur.execute("SELECT id FROM users WHERE username = %s", (username,))
|
||||
if cur.fetchone():
|
||||
username = f"{username}_{str(uuid.uuid4())[:4]}" # Short random suffix
|
||||
|
||||
first_name = userinfo.get('given_name', '')
|
||||
last_name = userinfo.get('family_name', '')
|
||||
|
||||
cur.execute('SELECT COUNT(*) FROM users')
|
||||
user_count = cur.fetchone()[0]
|
||||
|
||||
# Determine admin status: first user OR email matches configured admin email
|
||||
is_first_user_admin = (user_count == 0)
|
||||
|
||||
admin_email_from_env = current_app.config.get('ADMIN_EMAIL', '').lower()
|
||||
oidc_user_email_lower = email.lower() if email else ''
|
||||
|
||||
is_email_match_admin = False
|
||||
if admin_email_from_env and oidc_user_email_lower == admin_email_from_env:
|
||||
is_email_match_admin = True
|
||||
logger.info(f"[OIDC_HANDLER] New OIDC user email {oidc_user_email_lower} matches ADMIN_EMAIL {admin_email_from_env}.")
|
||||
|
||||
is_admin = is_first_user_admin or is_email_match_admin
|
||||
|
||||
if is_admin and not is_first_user_admin:
|
||||
logger.info(f"[OIDC_HANDLER] Granting admin rights to new OIDC user {oidc_user_email_lower} based on email match.")
|
||||
elif is_first_user_admin:
|
||||
logger.info(f"[OIDC_HANDLER] Granting admin rights to new OIDC user {oidc_user_email_lower} as they are the first user.")
|
||||
|
||||
|
||||
# Insert new OIDC user
|
||||
cur.execute(
|
||||
"""INSERT INTO users (username, email, first_name, last_name, is_admin, oidc_sub, oidc_issuer, is_active)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, TRUE) RETURNING id""",
|
||||
(username, email, first_name, last_name, is_admin, oidc_subject, oidc_issuer)
|
||||
)
|
||||
user_id = cur.fetchone()[0]
|
||||
logger.info(f"[OIDC_HANDLER] New OIDC user created with ID {user_id} for sub {oidc_subject}")
|
||||
|
||||
if user_id:
|
||||
app_session_token = generate_token(user_id) # Generate app-specific JWT
|
||||
|
||||
# Update last login timestamp
|
||||
cur.execute('UPDATE users SET last_login = %s WHERE id = %s', (datetime.utcnow(), user_id))
|
||||
|
||||
# Log OIDC session in user_sessions table
|
||||
ip_address = request.remote_addr
|
||||
user_agent = request.headers.get('User-Agent', '')
|
||||
# Use a different UUID for session_token in DB if needed, or re-use app_session_token if appropriate for your session model
|
||||
db_session_token = str(uuid.uuid4())
|
||||
expires_at = datetime.utcnow() + current_app.config['JWT_EXPIRATION_DELTA']
|
||||
|
||||
cur.execute(
|
||||
'INSERT INTO user_sessions (user_id, session_token, expires_at, ip_address, user_agent, login_method) VALUES (%s, %s, %s, %s, %s, %s)',
|
||||
(user_id, db_session_token, expires_at, ip_address, user_agent, 'oidc')
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
frontend_url = os.environ.get('FRONTEND_URL', current_app.config.get('APP_BASE_URL', 'http://localhost:8080')).rstrip('/')
|
||||
redirect_target = f"{frontend_url}/auth-redirect.html?token={app_session_token}"
|
||||
if is_new_user:
|
||||
redirect_target += "&new_user=true"
|
||||
|
||||
logger.info(f"[OIDC_HANDLER] /oidc/callback redirecting to frontend: {redirect_target}")
|
||||
return redirect(redirect_target)
|
||||
else:
|
||||
logger.error("[OIDC_HANDLER] /oidc/callback User ID not established after DB ops.")
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=user_processing_failed")
|
||||
|
||||
except Exception as e: # Catch more specific psycopg2.Error if preferred
|
||||
logger.error(f"[OIDC_HANDLER] OIDC callback: Database or general error: {e}", exc_info=True)
|
||||
if conn: conn.rollback()
|
||||
frontend_login_url = os.environ.get('FRONTEND_URL', 'http://localhost:8080').rstrip('/') + "/login.html"
|
||||
return redirect(f"{frontend_login_url}?oidc_error=internal_error")
|
||||
finally:
|
||||
if conn: release_db_connection(conn)
|
||||
|
||||
@oidc_bp.route('/auth/oidc-status', methods=['GET']) # Path relative to blueprint's url_prefix
|
||||
def get_oidc_status_route():
|
||||
conn = None
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT value FROM site_settings WHERE key = 'oidc_enabled'")
|
||||
result = cur.fetchone()
|
||||
oidc_is_enabled = False
|
||||
if result and result[0] is not None:
|
||||
oidc_is_enabled = str(result[0]).lower() == 'true'
|
||||
|
||||
cur.execute("SELECT value FROM site_settings WHERE key = 'oidc_provider_name'")
|
||||
provider_name_result = cur.fetchone()
|
||||
oidc_provider_name = 'SSO Provider' # Default button text
|
||||
if provider_name_result and provider_name_result[0]:
|
||||
raw_name = provider_name_result[0]
|
||||
# Simple capitalization for display
|
||||
oidc_provider_name = raw_name.capitalize() if raw_name else 'SSO Provider'
|
||||
|
||||
return jsonify({
|
||||
"oidc_enabled": oidc_is_enabled,
|
||||
"oidc_provider_display_name": oidc_provider_name
|
||||
}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"[OIDC_HANDLER] Error fetching OIDC status: {e}")
|
||||
return jsonify({"oidc_enabled": False, "oidc_provider_display_name": "SSO Provider"}), 200 # Default to false on error
|
||||
finally:
|
||||
if conn:
|
||||
release_db_connection(conn)
|
||||
@@ -1,11 +1,15 @@
|
||||
Flask==2.0.1
|
||||
gunicorn==20.1.0
|
||||
psycopg2-binary==2.9.3
|
||||
Werkzeug==2.0.1
|
||||
flask-cors==3.0.10
|
||||
Flask-Login==0.6.2
|
||||
Flask==3.0.3
|
||||
gunicorn==23.0.0
|
||||
psycopg2-binary==2.9.9
|
||||
Werkzeug==3.0.3
|
||||
flask-cors==4.0.1
|
||||
Flask-Login==0.6.3
|
||||
Flask-Bcrypt==1.0.1
|
||||
PyJWT==2.6.0
|
||||
email-validator==1.3.1
|
||||
PyJWT==2.8.0
|
||||
email-validator==2.1.1
|
||||
APScheduler==3.10.4
|
||||
python-dateutil
|
||||
python-dateutil==2.9.0
|
||||
Authlib==1.3.1
|
||||
requests==2.32.3
|
||||
gevent==24.2.1
|
||||
setuptools<81
|
||||
|
||||
@@ -19,7 +19,21 @@ services:
|
||||
- SMTP_PORT=${SMTP_PORT:-1025}
|
||||
- SMTP_USERNAME=${SMTP_USERNAME:-notifications@warracker.com}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD:-}
|
||||
- SECRET_KEY=${SECRET_KEY:-your_very_secret_flask_key_change_me} # For Flask session and JWT
|
||||
# OIDC SSO Configuration (User needs to set these based on their OIDC provider)
|
||||
- OIDC_PROVIDER_NAME=${OIDC_PROVIDER_NAME:-oidc}
|
||||
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-} # e.g., your_oidc_client_id
|
||||
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-} # e.g., your_oidc_client_secret
|
||||
- OIDC_ISSUER_URL=${OIDC_ISSUER_URL:-} # e.g., https://your-oidc-provider.com/auth/realms/your-realm
|
||||
- OIDC_SCOPE=${OIDC_SCOPE:-openid email profile}
|
||||
# URL settings (Important for redirects and email links)
|
||||
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:8005} # Public URL of the frontend (matching the port mapping)
|
||||
- APP_BASE_URL=${APP_BASE_URL:-http://localhost:8005} # Public base URL of the application for links
|
||||
- PYTHONUNBUFFERED=1
|
||||
# Memory optimization settings
|
||||
- WARRACKER_MEMORY_MODE=${WARRACKER_MEMORY_MODE:-optimized} # Options: optimized (default), ultra-light, performance
|
||||
- MAX_UPLOAD_MB=${MAX_UPLOAD_MB:-16} # Reduced from 32MB default for memory efficiency
|
||||
- NGINX_MAX_BODY_SIZE_VALUE=${NGINX_MAX_BODY_SIZE_VALUE:-16M} # Match upload limit
|
||||
depends_on:
|
||||
warrackerdb:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -25,22 +25,6 @@
|
||||
<script src="theme-loader.js"></script> <!-- Apply theme early -->
|
||||
<script src="include-auth-new.js"></script> <!-- Handles auth state display -->
|
||||
<script src="fix-auth-buttons-loader.js"></script> <!-- Fixes auth button display timing -->
|
||||
<script src="auth.js"></script> <!-- Load auth.js early for authentication check -->
|
||||
|
||||
<!-- Authentication Check -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check if user is authenticated
|
||||
const authToken = localStorage.getItem('auth_token');
|
||||
const userInfo = localStorage.getItem('user_info');
|
||||
|
||||
if (!authToken || !userInfo) {
|
||||
// User is not authenticated, redirect to login page
|
||||
window.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
@@ -71,7 +55,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div id="userMenu" class="user-menu" style="display: none;">
|
||||
<button id="userBtn" class="user-btn">
|
||||
<button id="userMenuBtn" class="user-btn">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<span id="userDisplayName">User</span>
|
||||
</button>
|
||||
@@ -106,7 +90,7 @@
|
||||
<div class="about-container" style="background-color: var(--card-bg); color: var(--text-color); padding: 20px; border-radius: 8px; margin-top: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); width: 100%; box-sizing: border-box;">
|
||||
<h1 style="color: var(--primary-color); margin-bottom: 20px;">About Warracker</h1>
|
||||
|
||||
<p><strong>Version:</strong> v0.9.9.8</p>
|
||||
<p><strong>Version:</strong> v0.9.9.9</p>
|
||||
|
||||
<div id="versionTracker" style="margin-bottom: 20px;">
|
||||
<p><strong>Update Status:</strong> <span id="updateStatus">Checking for updates...</span></p>
|
||||
@@ -160,20 +144,224 @@
|
||||
</div>
|
||||
|
||||
<!-- Scripts loaded at the end of body -->
|
||||
<script src="auth.js"></script> <!-- Added for user menu and other auth interactions -->
|
||||
<script src="auth.js?v=2"></script> <!-- Added for user menu and other auth interactions -->
|
||||
<script src="script.js"></script> <!-- Include main site script, might contain helpers -->
|
||||
<script src="version-checker.js"></script> <!-- Version checker script -->
|
||||
|
||||
<!-- Explicitly call setupUIEventListeners for this page -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('About page: DOMContentLoaded triggered');
|
||||
|
||||
// Add safe loading functions for about page
|
||||
window.showLoading = function() {
|
||||
const loadingContainer = document.getElementById('loadingContainer');
|
||||
if (loadingContainer && loadingContainer.classList) {
|
||||
loadingContainer.classList.add('active');
|
||||
console.log('About page: Loading spinner shown');
|
||||
} else {
|
||||
console.log('About page: Loading container not found or unavailable');
|
||||
}
|
||||
};
|
||||
|
||||
window.hideLoading = function() {
|
||||
const loadingContainer = document.getElementById('loadingContainer');
|
||||
if (loadingContainer && loadingContainer.classList) {
|
||||
loadingContainer.classList.remove('active');
|
||||
console.log('About page: Loading spinner hidden');
|
||||
} else {
|
||||
console.log('About page: Loading container not found or unavailable');
|
||||
}
|
||||
};
|
||||
|
||||
// Keep theme initialization here if needed for specific timing
|
||||
if (typeof initializeTheme === 'function') {
|
||||
console.log('About page: Calling initializeTheme');
|
||||
initializeTheme(); // Call theme initialization if it exists
|
||||
}
|
||||
|
||||
// Add specific debugging for user menu elements
|
||||
setTimeout(() => {
|
||||
const userMenuBtn = document.getElementById('userMenuBtn');
|
||||
const userMenuDropdown = document.getElementById('userMenuDropdown');
|
||||
|
||||
console.log('About page: User menu debug info:', {
|
||||
userMenuBtn: !!userMenuBtn,
|
||||
userMenuDropdown: !!userMenuDropdown,
|
||||
userMenuBtnId: userMenuBtn ? userMenuBtn.id : 'not found',
|
||||
dropdownId: userMenuDropdown ? userMenuDropdown.id : 'not found',
|
||||
authModuleAvailable: !!window.auth,
|
||||
isAuthenticated: window.auth ? window.auth.isAuthenticated() : 'auth not available'
|
||||
});
|
||||
|
||||
if (userMenuBtn) {
|
||||
console.log('About page: userMenuBtn element:', userMenuBtn);
|
||||
console.log('About page: userMenuBtn element found, proceeding with backup handler setup');
|
||||
}
|
||||
|
||||
// Add a backup user menu handler specifically for about page
|
||||
if (userMenuBtn && userMenuDropdown) {
|
||||
console.log('About page: Setting up backup user menu handler');
|
||||
console.log('About page: Current dropdown classes:', userMenuDropdown.className);
|
||||
console.log('About page: Current dropdown active state:', userMenuDropdown.classList.contains('active'));
|
||||
|
||||
// Test CSS rules by temporarily adding/removing active class
|
||||
console.log('About page: Testing CSS rules...');
|
||||
userMenuDropdown.classList.add('active');
|
||||
const testDisplayActive = window.getComputedStyle(userMenuDropdown).display;
|
||||
userMenuDropdown.classList.remove('active');
|
||||
const testDisplayInactive = window.getComputedStyle(userMenuDropdown).display;
|
||||
console.log('About page: CSS test results:', {
|
||||
displayWhenActive: testDisplayActive,
|
||||
displayWhenInactive: testDisplayInactive
|
||||
});
|
||||
|
||||
// Remove any existing click listeners and add our own
|
||||
const newUserMenuBtn = userMenuBtn.cloneNode(true);
|
||||
userMenuBtn.parentNode.replaceChild(newUserMenuBtn, userMenuBtn);
|
||||
|
||||
newUserMenuBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('About page: Backup user menu clicked');
|
||||
console.log('About page: Dropdown before toggle:', {
|
||||
classes: userMenuDropdown.className,
|
||||
hasActive: userMenuDropdown.classList.contains('active'),
|
||||
style: userMenuDropdown.style.display
|
||||
});
|
||||
|
||||
const isCurrentlyActive = userMenuDropdown.classList.contains('active');
|
||||
|
||||
if (isCurrentlyActive) {
|
||||
userMenuDropdown.classList.remove('active');
|
||||
console.log('About page: User menu closed');
|
||||
} else {
|
||||
userMenuDropdown.classList.add('active');
|
||||
console.log('About page: User menu opened');
|
||||
}
|
||||
|
||||
// Double-check the state after toggle
|
||||
console.log('About page: Dropdown after toggle:', {
|
||||
classes: userMenuDropdown.className,
|
||||
hasActive: userMenuDropdown.classList.contains('active'),
|
||||
computedStyle: window.getComputedStyle(userMenuDropdown).display
|
||||
});
|
||||
});
|
||||
|
||||
// Add global click listener to close menu when clicking outside
|
||||
const outsideClickHandler = (e) => {
|
||||
if (userMenuDropdown.classList.contains('active') &&
|
||||
!userMenuDropdown.contains(e.target) &&
|
||||
!newUserMenuBtn.contains(e.target)) {
|
||||
userMenuDropdown.classList.remove('active');
|
||||
console.log('About page: User menu closed by outside click');
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure this listener is added after any others
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', outsideClickHandler);
|
||||
console.log('About page: Outside click handler added');
|
||||
}, 10);
|
||||
|
||||
console.log('About page: Backup user menu handler set up successfully');
|
||||
|
||||
// Add specific logout handler for the about page
|
||||
const logoutMenuItem = document.getElementById('logoutMenuItem');
|
||||
if (logoutMenuItem) {
|
||||
// Clone the logout menu item to remove any existing listeners
|
||||
const newLogoutMenuItem = logoutMenuItem.cloneNode(true);
|
||||
logoutMenuItem.parentNode.replaceChild(newLogoutMenuItem, logoutMenuItem);
|
||||
|
||||
newLogoutMenuItem.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('About page: Logout clicked (backup handler)');
|
||||
|
||||
// Close the user menu
|
||||
userMenuDropdown.classList.remove('active');
|
||||
|
||||
try {
|
||||
// Clear auth data manually since showLoading might fail
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user_info');
|
||||
|
||||
// Try to call the auth manager logout if available
|
||||
if (window.auth && typeof window.auth.logout === 'function') {
|
||||
console.log('About page: Calling auth.logout()');
|
||||
await window.auth.logout();
|
||||
} else {
|
||||
console.log('About page: Auth manager not available, redirecting manually');
|
||||
// Redirect manually if auth manager fails
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('About page: Logout error, falling back to manual redirect:', error);
|
||||
// Clear auth data and redirect as fallback
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user_info');
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
});
|
||||
|
||||
console.log('About page: Backup logout handler set up');
|
||||
}
|
||||
} else {
|
||||
console.error('About page: User menu elements not found!', {
|
||||
userMenuBtn: !!userMenuBtn,
|
||||
userMenuDropdown: !!userMenuDropdown
|
||||
});
|
||||
}
|
||||
}, 200); // Delay to allow auth.js to complete setup
|
||||
|
||||
// Move authentication check here, after auth.js has had time to set up
|
||||
setTimeout(() => {
|
||||
// Check if user is authenticated (with a small delay to allow auth.js to process)
|
||||
const authToken = localStorage.getItem('auth_token');
|
||||
const userInfo = localStorage.getItem('user_info');
|
||||
|
||||
if (!authToken || !userInfo) {
|
||||
// User is not authenticated, redirect to login page
|
||||
console.log('About page: User not authenticated, redirecting to login');
|
||||
window.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
}, 100); // Small delay to allow auth.js to complete setup
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<div class="loading-container" id="loadingContainer">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || document.documentElement.classList.contains('dark-mode') || document.body.classList.contains('dark-mode');
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
// Dark mode styles - using same background as header (#2d2d2d)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
// Light mode styles - using same background as header (#ffffff)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
const obs = new MutationObserver(applyFooterStyles);
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
|
||||
obs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
190
frontend/auth-redirect.html
Normal file
190
frontend/auth-redirect.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Authenticating...</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="theme-loader.js"></script>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.error-message {
|
||||
color: #f44336; /* Red for errors */
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2 id="statusMessage">Authenticating, please wait...</h2>
|
||||
<div id="errorMessage" class="error-message" style="display: none;"></div>
|
||||
<a href="login.html" id="loginLink" style="display: none;">Return to Login</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function fetchUserInfoAndRedirect(token, isNewUser) {
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
const errorMessageEl = document.getElementById('errorMessage');
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
|
||||
try {
|
||||
statusMessage.textContent = 'Fetching user information...';
|
||||
|
||||
// Fetch user info using the token
|
||||
const response = await fetch('/api/auth/validate-token', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.valid && data.user && data.user.id) {
|
||||
// Store user info in localStorage
|
||||
localStorage.setItem('user_info', JSON.stringify(data.user));
|
||||
|
||||
// Update status message
|
||||
if (isNewUser) {
|
||||
statusMessage.textContent = 'Account created successfully! Redirecting...';
|
||||
} else {
|
||||
statusMessage.textContent = 'Login successful! Redirecting...';
|
||||
}
|
||||
|
||||
// Redirect to the main application page
|
||||
setTimeout(() => {
|
||||
window.location.href = 'index.html';
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error('Invalid user data received');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Failed to validate token: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user info:', error);
|
||||
|
||||
// Clear potentially invalid token
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user_info');
|
||||
|
||||
statusMessage.textContent = 'SSO Login Failed';
|
||||
errorMessageEl.textContent = 'Failed to fetch user information. Please try logging in again.';
|
||||
errorMessageEl.style.display = 'block';
|
||||
loginLink.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
const errorMessageEl = document.getElementById('errorMessage');
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('token');
|
||||
const oidcError = params.get('oidc_error');
|
||||
const newUser = params.get('new_user');
|
||||
|
||||
if (oidcError) {
|
||||
let message = 'An unknown error occurred during SSO login.';
|
||||
switch (oidcError) {
|
||||
case 'token_exchange_failed':
|
||||
message = 'Failed to exchange authorization code for tokens with the SSO provider.';
|
||||
break;
|
||||
case 'token_missing':
|
||||
message = 'Access token was not received from the SSO provider.';
|
||||
break;
|
||||
case 'userinfo_fetch_failed':
|
||||
message = 'Failed to fetch user information from the SSO provider.';
|
||||
break;
|
||||
case 'userinfo_missing':
|
||||
message = 'User information was not received from the SSO provider.';
|
||||
break;
|
||||
case 'subject_missing':
|
||||
message = 'User subject identifier (sub) was missing from SSO provider response.';
|
||||
break;
|
||||
case 'email_missing_for_new_user':
|
||||
message = 'Email address was not provided by SSO provider, which is required for new user registration.';
|
||||
break;
|
||||
case 'email_conflict_local_account':
|
||||
message = 'The email address from your SSO provider is already associated with an existing local account. Please log in with your local credentials or contact support.';
|
||||
break;
|
||||
case 'registration_disabled':
|
||||
message = 'New user registration via SSO is currently disabled. Only existing users can log in with SSO. Please contact your administrator if you need an account.';
|
||||
break;
|
||||
case 'user_processing_failed':
|
||||
message = 'Failed to process user information after SSO login.';
|
||||
break;
|
||||
case 'db_error':
|
||||
message = 'A database error occurred during SSO login. Please try again later.';
|
||||
break;
|
||||
case 'internal_error':
|
||||
message = 'An internal server error occurred during SSO login. Please try again later.';
|
||||
break;
|
||||
}
|
||||
statusMessage.textContent = 'SSO Login Failed';
|
||||
errorMessageEl.textContent = message;
|
||||
errorMessageEl.style.display = 'block';
|
||||
loginLink.style.display = 'inline';
|
||||
} else if (token) {
|
||||
localStorage.setItem('auth_token', token);
|
||||
|
||||
// Fetch user info immediately after storing token to ensure it's available
|
||||
// when the main app loads, preventing preference key mismatches
|
||||
fetchUserInfoAndRedirect(token, newUser === 'true');
|
||||
} else {
|
||||
// No token and no error, unexpected state, redirect to login
|
||||
statusMessage.textContent = 'Redirecting to login...';
|
||||
setTimeout(() => {
|
||||
window.location.href = 'login.html';
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || document.documentElement.classList.contains('dark-mode') || document.body.classList.contains('dark-mode');
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
// Dark mode styles - using same background as header (#2d2d2d)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
// Light mode styles - using same background as header (#ffffff)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
const obs = new MutationObserver(applyFooterStyles);
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
|
||||
obs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
699
frontend/auth.js
699
frontend/auth.js
@@ -3,385 +3,362 @@
|
||||
* Handles user login, logout, and authentication state management
|
||||
*/
|
||||
|
||||
// DOM Elements
|
||||
const authContainer = document.getElementById('authContainer');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
const userBtn = document.getElementById('userBtn');
|
||||
const userMenuDropdown = document.getElementById('userMenuDropdown');
|
||||
const userDisplayName = document.getElementById('userDisplayName');
|
||||
const userName = document.getElementById('userName');
|
||||
const userEmail = document.getElementById('userEmail');
|
||||
const logoutMenuItem = document.getElementById('logoutMenuItem');
|
||||
const profileMenuItem = document.getElementById('profileMenuItem');
|
||||
class AuthManager {
|
||||
constructor() {
|
||||
this.token = null;
|
||||
this.currentUser = null;
|
||||
this.onLogoutCallbacks = [];
|
||||
|
||||
// New UI Elements (from screenshot)
|
||||
const loginButton = document.querySelector('a[href="login.html"]');
|
||||
const registerButton = document.querySelector('a[href="register.html"]');
|
||||
const usernameDisplay = document.querySelector('.user-name');
|
||||
|
||||
// Authentication state
|
||||
let currentUser = null;
|
||||
let authToken = null;
|
||||
|
||||
// Initialize authentication
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initial check of authentication state
|
||||
checkAuthState();
|
||||
|
||||
// Set up periodic check of auth state (every 30 seconds)
|
||||
// setInterval(checkAuthState, 30000); // Consider if needed, can cause flicker
|
||||
|
||||
// --- USER MENU TOGGLE ---
|
||||
const userMenuBtn = document.getElementById('userMenuBtn');
|
||||
const userMenuDropdown = document.getElementById('userMenuDropdown');
|
||||
console.log('auth.js: Checking for user menu elements...', { userMenuBtn, userMenuDropdown }); // Debug log
|
||||
if (userMenuBtn && userMenuDropdown) {
|
||||
console.log('auth.js: User menu elements found, adding listener.'); // Debug log
|
||||
userMenuBtn.addEventListener('click', (e) => {
|
||||
console.log('auth.js: User menu button CLICKED!'); // *** ADDED LOG ***
|
||||
e.stopPropagation();
|
||||
userMenuDropdown.classList.toggle('active');
|
||||
console.log('auth.js: User menu dropdown toggled.', { active: userMenuDropdown.classList.contains('active') }); // *** ADDED LOG ***
|
||||
});
|
||||
} else {
|
||||
console.log('auth.js: User menu button or dropdown not found on this page.'); // Debug log
|
||||
}
|
||||
|
||||
// --- SETTINGS GEAR MENU TOGGLE (Moved from settings-new.js) ---
|
||||
const settingsBtn = document.getElementById('settingsBtn'); // Gear icon button
|
||||
const settingsMenu = document.getElementById('settingsMenu'); // The dropdown menu itself
|
||||
console.log('auth.js: Checking for settings menu elements...', { settingsBtn, settingsMenu }); // Debug log
|
||||
if (settingsBtn && settingsMenu) {
|
||||
console.log('auth.js: Settings menu elements found, adding listeners.'); // Debug log
|
||||
// Toggle settings menu when settings button is clicked
|
||||
settingsBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation(); // Prevent click from closing menu immediately
|
||||
settingsMenu.classList.toggle('active');
|
||||
console.log('auth.js: Settings button clicked, menu toggled.', { active: settingsMenu.classList.contains('active') }); // Debug log
|
||||
});
|
||||
} else {
|
||||
console.log('auth.js: Settings button or menu not found on this page.'); // Debug log
|
||||
}
|
||||
// --- END SETTINGS GEAR MENU TOGGLE ---
|
||||
|
||||
// Close menus when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
// Close user menu
|
||||
if (userMenuDropdown && userMenuBtn &&
|
||||
userMenuDropdown.classList.contains('active') &&
|
||||
!userMenuDropdown.contains(e.target) &&
|
||||
!userMenuBtn.contains(e.target)) {
|
||||
userMenuDropdown.classList.remove('active');
|
||||
// Initial state load from localStorage
|
||||
this.token = localStorage.getItem('auth_token');
|
||||
const userInfoString = localStorage.getItem('user_info');
|
||||
if (userInfoString) {
|
||||
try {
|
||||
this.currentUser = JSON.parse(userInfoString);
|
||||
} catch (e) {
|
||||
console.error('Auth.js: Corrupt user_info in localStorage. Clearing.');
|
||||
this.currentUser = null;
|
||||
localStorage.removeItem('user_info');
|
||||
// Consider clearing token as well if user_info is corrupt
|
||||
// localStorage.removeItem('auth_token');
|
||||
// this.token = null;
|
||||
}
|
||||
}
|
||||
// Close settings menu
|
||||
if (settingsMenu && settingsBtn &&
|
||||
settingsMenu.classList.contains('active') &&
|
||||
!settingsMenu.contains(e.target) &&
|
||||
!settingsBtn.contains(e.target)) {
|
||||
settingsMenu.classList.remove('active');
|
||||
console.log('auth.js: Click outside closed settings menu.'); // Debug log
|
||||
}
|
||||
});
|
||||
|
||||
// Logout functionality
|
||||
const logoutMenuItem = document.getElementById('logoutMenuItem');
|
||||
if (logoutMenuItem) {
|
||||
logoutMenuItem.addEventListener('click', logout);
|
||||
console.log('[Auth.js] Initial state:', { token: this.token ? 'present' : 'null', currentUser: this.currentUser });
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
// User is authenticated if both token and currentUser (with an id) are present
|
||||
return !!(this.token && this.currentUser && this.currentUser.id);
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
// Always return the current state of this.token, which should be synced with localStorage
|
||||
return this.token;
|
||||
}
|
||||
|
||||
clearAuthData() {
|
||||
console.log('[Auth.js] Clearing auth data.');
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user_info');
|
||||
this.token = null;
|
||||
this.currentUser = null;
|
||||
this.onLogoutCallbacks.forEach(cb => cb());
|
||||
}
|
||||
|
||||
// Profile menu item link (assuming it's an <a> tag now)
|
||||
// const profileLink = document.querySelector('.user-menu-item a[href="settings-new.html"]');
|
||||
// No special listener needed if it's just a link
|
||||
});
|
||||
onLogout(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.onLogoutCallbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated and update UI accordingly
|
||||
*/
|
||||
function checkAuthState() {
|
||||
// Get latest token from localStorage
|
||||
authToken = localStorage.getItem('auth_token');
|
||||
const userInfo = localStorage.getItem('user_info');
|
||||
async checkAuthState(isInitialLoad = false) {
|
||||
console.log('[Auth.js] checkAuthState called. Initial load:', isInitialLoad);
|
||||
this.token = localStorage.getItem('auth_token'); // Re-read token, might have changed (e.g. by auth-redirect.js)
|
||||
const userInfoString = localStorage.getItem('user_info');
|
||||
|
||||
this.currentUser = null; // Reset before check
|
||||
|
||||
if (userInfoString) {
|
||||
try {
|
||||
this.currentUser = JSON.parse(userInfoString);
|
||||
} catch (e) {
|
||||
console.error('Auth.js: Failed to parse user_info from localStorage during checkAuthState. Clearing auth data.', e);
|
||||
this.clearAuthData(); // Clear potentially corrupt data
|
||||
this.updateUIBasedOnAuthState(); // Update UI to reflect logged-out state
|
||||
return; // Exit early
|
||||
}
|
||||
}
|
||||
|
||||
if (this.token) {
|
||||
// If token exists, try to validate it and fetch/confirm user_info
|
||||
// This is crucial if user_info was missing or to refresh/validate existing user_info
|
||||
console.log('[Auth.js] Token found. Validating and fetching user info...');
|
||||
try {
|
||||
const response = await fetch('/api/auth/validate-token', {
|
||||
headers: { 'Authorization': `Bearer ${this.token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.valid && data.user && data.user.id) {
|
||||
this.currentUser = data.user;
|
||||
localStorage.setItem('user_info', JSON.stringify(this.currentUser)); // Ensure localStorage is up-to-date
|
||||
console.log('[Auth.js] Token validated, user_info updated/confirmed:', this.currentUser);
|
||||
} else {
|
||||
console.warn('[Auth.js] Token validation failed or user data invalid from API. Clearing auth data.');
|
||||
this.clearAuthData();
|
||||
}
|
||||
} else {
|
||||
console.warn(`[Auth.js] Token validation API call failed (status: ${response.status}). Clearing auth data.`);
|
||||
this.clearAuthData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Auth.js] Error validating token / fetching user info:', error);
|
||||
this.clearAuthData();
|
||||
}
|
||||
} else {
|
||||
// No token, ensure everything is cleared
|
||||
if (this.currentUser) { // If there was user_info but no token, clear user_info
|
||||
console.log('[Auth.js] No token found, but user_info was present. Clearing user_info.');
|
||||
this.clearAuthData();
|
||||
}
|
||||
}
|
||||
|
||||
this.updateUIBasedOnAuthState();
|
||||
}
|
||||
|
||||
updateUIBasedOnAuthState() {
|
||||
const isAuthenticated = this.isAuthenticated();
|
||||
this._updateDOMForAuthState(isAuthenticated, this.currentUser);
|
||||
this.dispatchAuthStateEvent(isAuthenticated, this.currentUser);
|
||||
}
|
||||
|
||||
if (authToken && userInfo) {
|
||||
_updateDOMForAuthState(isAuthenticated, user) {
|
||||
const authContainer = document.getElementById('authContainer');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
const userDisplayName = document.getElementById('userDisplayName');
|
||||
const userNameMenu = document.getElementById('userName');
|
||||
const userEmailMenu = document.getElementById('userEmail');
|
||||
const logoutMenuItem = document.getElementById('logoutMenuItem');
|
||||
|
||||
// Select all potential login/register buttons more broadly
|
||||
const loginButtons = document.querySelectorAll('a[href="login.html"], .login-btn');
|
||||
const registerButtons = document.querySelectorAll('a[href="register.html"], .register-btn');
|
||||
const genericAuthButtonsContainers = document.querySelectorAll('.auth-buttons');
|
||||
|
||||
|
||||
if (isAuthenticated && user) {
|
||||
console.log('Auth.js: Updating UI for AUTHENTICATED user:', user);
|
||||
if (authContainer) { authContainer.style.display = 'none'; authContainer.style.visibility = 'hidden'; }
|
||||
|
||||
if (userMenu) {
|
||||
userMenu.style.display = 'block'; // Or 'flex' based on CSS
|
||||
userMenu.style.visibility = 'visible';
|
||||
const displayNameText = user.first_name || user.username || 'User';
|
||||
const fullNameText = `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.username || 'User Name';
|
||||
if (userDisplayName) userDisplayName.textContent = displayNameText;
|
||||
if (userNameMenu) userNameMenu.textContent = fullNameText;
|
||||
if (userEmailMenu && user.email) userEmailMenu.textContent = user.email;
|
||||
}
|
||||
loginButtons.forEach(btn => { btn.style.display = 'none'; btn.style.visibility = 'hidden'; });
|
||||
registerButtons.forEach(btn => { btn.style.display = 'none'; btn.style.visibility = 'hidden'; });
|
||||
genericAuthButtonsContainers.forEach(container => {
|
||||
if (container.id !== 'authContainer') { // Avoid double-hiding if authContainer also has .auth-buttons
|
||||
container.style.display = 'none'; container.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
|
||||
if (logoutMenuItem) {
|
||||
logoutMenuItem.style.display = 'flex'; // Assuming it's a flex item
|
||||
// Ensure logout listener is attached (can be done once in constructor or DOMContentLoaded)
|
||||
}
|
||||
} else {
|
||||
console.log('Auth.js: Updating UI for UNAUTHENTICATED user.');
|
||||
if (authContainer) { authContainer.style.display = 'flex'; authContainer.style.visibility = 'visible'; }
|
||||
|
||||
if (userMenu) { userMenu.style.display = 'none'; userMenu.style.visibility = 'hidden'; }
|
||||
|
||||
// Reset user display names if they exist
|
||||
if (userDisplayName) userDisplayName.textContent = 'User';
|
||||
if (userNameMenu) userNameMenu.textContent = 'User Name';
|
||||
if (userEmailMenu) userEmailMenu.textContent = 'user@example.com';
|
||||
|
||||
loginButtons.forEach(btn => { btn.style.display = 'inline-block'; btn.style.visibility = 'visible'; });
|
||||
registerButtons.forEach(btn => { btn.style.display = 'inline-block'; btn.style.visibility = 'visible'; });
|
||||
genericAuthButtonsContainers.forEach(container => {
|
||||
if (container.id !== 'authContainer') {
|
||||
container.style.display = 'flex'; // Or 'block'
|
||||
container.style.visibility = 'visible';
|
||||
}
|
||||
});
|
||||
if (logoutMenuItem) { logoutMenuItem.style.display = 'none'; }
|
||||
}
|
||||
}
|
||||
|
||||
dispatchAuthStateEvent(isAuthenticated, user) {
|
||||
console.log('[Auth.js] Dispatching authStateReady event', { isAuthenticated, user });
|
||||
// Ensure this event is dispatched after the current call stack clears
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('authStateReady', {
|
||||
detail: { isAuthenticated, user }
|
||||
}));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async login(username, password) {
|
||||
// Assuming showLoading/hideLoading are global or part of another module
|
||||
if (typeof showLoading === 'function') showLoading();
|
||||
try {
|
||||
currentUser = JSON.parse(userInfo);
|
||||
updateUIForAuthenticatedUser();
|
||||
validateToken();
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.token = data.token;
|
||||
this.currentUser = data.user;
|
||||
localStorage.setItem('auth_token', this.token);
|
||||
localStorage.setItem('user_info', JSON.stringify(this.currentUser));
|
||||
this.updateUIBasedOnAuthState();
|
||||
return data; // Return data for login.js to handle redirect
|
||||
} else {
|
||||
throw new Error(data.message || 'Login failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing user info:', error);
|
||||
clearAuthData();
|
||||
updateUIForUnauthenticatedUser();
|
||||
this.clearAuthData(); // Ensure state is cleared on login failure
|
||||
this.updateUIBasedOnAuthState();
|
||||
throw error; // Re-throw for login.js to handle
|
||||
} finally {
|
||||
if (typeof hideLoading === 'function') hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
if (typeof showLoading === 'function') showLoading();
|
||||
const currentTokenForApiCall = this.token; // Use current token for API call
|
||||
|
||||
this.clearAuthData(); // Clear local state immediately
|
||||
this.updateUIBasedOnAuthState(); // Update UI to logged-out state
|
||||
|
||||
try {
|
||||
if (currentTokenForApiCall) {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${currentTokenForApiCall}` }
|
||||
});
|
||||
console.log('[Auth.js] Logout API call successful.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Auth.js] Logout API call failed, but user is logged out locally.', error);
|
||||
} finally {
|
||||
if (typeof hideLoading === 'function') hideLoading();
|
||||
// Redirect to login page after all operations
|
||||
if (window.location.pathname !== '/login.html') {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addAuthHeader(options = {}) {
|
||||
const token = this.getToken();
|
||||
if (!token) return options;
|
||||
|
||||
const headers = options.headers || {};
|
||||
return { ...options, headers: { ...headers, 'Authorization': `Bearer ${token}`}};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and export
|
||||
window.auth = new AuthManager();
|
||||
|
||||
// Initial check on DOMContentLoaded
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('[Auth.js] DOMContentLoaded - performing initial async auth state check.');
|
||||
await window.auth.checkAuthState(true); // Ensure auth state is processed first
|
||||
|
||||
// Setup user menu toggle
|
||||
const userMenuBtn_original = document.getElementById('userMenuBtn');
|
||||
const userMenuDropdown = document.getElementById('userMenuDropdown');
|
||||
|
||||
if (userMenuBtn_original && userMenuDropdown) {
|
||||
console.log('[Auth.js] Setting up user menu. Button:', userMenuBtn_original, 'Dropdown:', userMenuDropdown);
|
||||
|
||||
// Robust listener attachment: clone the button to remove any prior listeners
|
||||
const userMenuBtn = userMenuBtn_original.cloneNode(true);
|
||||
userMenuBtn_original.parentNode.replaceChild(userMenuBtn, userMenuBtn_original);
|
||||
|
||||
userMenuBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // Prevent click from immediately closing due to document listener
|
||||
console.log('[Auth.js] userMenuBtn clicked - DEBUG INFO:', {
|
||||
userMenuDropdown: !!userMenuDropdown,
|
||||
dropdownClassList: userMenuDropdown ? Array.from(userMenuDropdown.classList) : 'not found',
|
||||
hasActiveClass: userMenuDropdown ? userMenuDropdown.classList.contains('active') : 'dropdown not found',
|
||||
buttonId: userMenuBtn.id,
|
||||
dropdownId: userMenuDropdown ? userMenuDropdown.id : 'not found'
|
||||
});
|
||||
|
||||
userMenuDropdown.classList.toggle('active');
|
||||
|
||||
const isNowActive = userMenuDropdown.classList.contains('active');
|
||||
console.log('[Auth.js] User menu toggled via userMenuBtn. Active:', isNowActive);
|
||||
|
||||
// Add a temporary debug check to see if it gets closed immediately
|
||||
setTimeout(() => {
|
||||
const stillActive = userMenuDropdown.classList.contains('active');
|
||||
console.log('[Auth.js] User menu status after 100ms:', stillActive);
|
||||
if (isNowActive && !stillActive) {
|
||||
console.warn('[Auth.js] User menu was closed immediately! Possible global click interference.');
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
console.log('[Auth.js] User menu click listener attached to userMenuBtn.');
|
||||
} else {
|
||||
updateUIForUnauthenticatedUser();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI elements for authenticated user
|
||||
*/
|
||||
function updateUIForAuthenticatedUser() {
|
||||
// Fire a global event so other scripts (like script.js) can react to authentication being ready
|
||||
setTimeout(() => {
|
||||
console.log('[auth.js] Dispatching authStateReady event');
|
||||
window.dispatchEvent(new Event('authStateReady'));
|
||||
}, 0);
|
||||
console.log('auth.js: Updating UI for authenticated user');
|
||||
|
||||
// Log the user data being used
|
||||
console.log('auth.js: Current user data from state:', currentUser);
|
||||
if (!currentUser) {
|
||||
console.error('auth.js: Cannot update UI, currentUser is null or undefined.');
|
||||
return;
|
||||
console.warn('[Auth.js] User menu button (userMenuBtn) or dropdown (userMenuDropdown) not found. Menu interactivity might be affected.');
|
||||
}
|
||||
|
||||
// Hide login/register buttons
|
||||
if (authContainer) {
|
||||
console.log('auth.js: Hiding authContainer');
|
||||
authContainer.style.display = 'none';
|
||||
authContainer.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
// Show user menu
|
||||
if (userMenu) {
|
||||
console.log('auth.js: Showing userMenu');
|
||||
userMenu.style.display = 'block';
|
||||
userMenu.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
// Update user info in the header menu
|
||||
let displayName = currentUser.username || 'User';
|
||||
const fullName = `${currentUser.first_name || ''} ${currentUser.last_name || ''}`.trim() || currentUser.username || 'User Name'; // Fallback added
|
||||
const email = currentUser.email || 'user@example.com'; // Fallback added
|
||||
|
||||
// Log the values being set
|
||||
console.log(`auth.js: Setting display name (short): [${displayName}]`);
|
||||
console.log(`auth.js: Setting full name (menu): [${fullName}]`);
|
||||
console.log(`auth.js: Setting email (menu): [${email}]`);
|
||||
// Setup settings gear menu toggle (if elements exist on the current page)
|
||||
const settingsBtn_original = document.getElementById('settingsBtn');
|
||||
const settingsMenu = document.getElementById('settingsMenu'); // The dropdown menu itself
|
||||
if (settingsBtn_original && settingsMenu) {
|
||||
const settingsBtn = settingsBtn_original.cloneNode(true);
|
||||
settingsBtn_original.parentNode.replaceChild(settingsBtn, settingsBtn_original);
|
||||
|
||||
if (userDisplayName) userDisplayName.textContent = displayName;
|
||||
if (userName) userName.textContent = fullName;
|
||||
if (userEmail) userEmail.textContent = email;
|
||||
|
||||
// Hide all login and register buttons
|
||||
const loginButtons = document.querySelectorAll('a[href="login.html"], .login-btn, .auth-btn.login-btn');
|
||||
const registerButtons = document.querySelectorAll('a[href="register.html"], .register-btn, .auth-btn.register-btn');
|
||||
|
||||
console.log('auth.js: Found login buttons:', loginButtons.length);
|
||||
console.log('auth.js: Found register buttons:', registerButtons.length);
|
||||
|
||||
loginButtons.forEach(button => {
|
||||
console.log('auth.js: Hiding login button');
|
||||
button.style.display = 'none';
|
||||
button.style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
registerButtons.forEach(button => {
|
||||
console.log('auth.js: Hiding register button');
|
||||
button.style.display = 'none';
|
||||
button.style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
// Hide any containers with the auth-buttons class
|
||||
const authButtonsContainers = document.querySelectorAll('.auth-buttons');
|
||||
authButtonsContainers.forEach(container => {
|
||||
console.log('auth.js: Hiding auth buttons container');
|
||||
container.style.display = 'none';
|
||||
container.style.visibility = 'hidden';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI elements for unauthenticated user
|
||||
*/
|
||||
function updateUIForUnauthenticatedUser() {
|
||||
console.log('auth.js: Updating UI for unauthenticated user');
|
||||
|
||||
// Show login/register buttons
|
||||
if (authContainer) {
|
||||
console.log('auth.js: Showing authContainer');
|
||||
authContainer.style.display = 'flex';
|
||||
authContainer.style.visibility = 'visible';
|
||||
settingsBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
settingsMenu.classList.toggle('active');
|
||||
console.log('[Auth.js] Settings menu toggled via settingsBtn.');
|
||||
});
|
||||
console.log('[Auth.js] Settings menu click listener attached to settingsBtn.');
|
||||
}
|
||||
|
||||
// Hide user menu
|
||||
if (userMenu) {
|
||||
console.log('auth.js: Hiding userMenu');
|
||||
userMenu.style.display = 'none';
|
||||
userMenu.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
// Clear user info
|
||||
if (userDisplayName) userDisplayName.textContent = 'User';
|
||||
if (userName) userName.textContent = 'User Name';
|
||||
if (userEmail) userEmail.textContent = 'user@example.com';
|
||||
|
||||
// Show all login and register buttons
|
||||
const loginButtons = document.querySelectorAll('a[href="login.html"], .login-btn, .auth-btn.login-btn');
|
||||
const registerButtons = document.querySelectorAll('a[href="register.html"], .register-btn, .auth-btn.register-btn');
|
||||
|
||||
loginButtons.forEach(button => {
|
||||
button.style.display = 'inline-block';
|
||||
button.style.visibility = 'visible';
|
||||
});
|
||||
|
||||
registerButtons.forEach(button => {
|
||||
button.style.display = 'inline-block';
|
||||
button.style.visibility = 'visible';
|
||||
});
|
||||
|
||||
// Show any containers with the auth-buttons class
|
||||
const authButtonsContainers = document.querySelectorAll('.auth-buttons');
|
||||
authButtonsContainers.forEach(container => {
|
||||
container.style.display = 'flex';
|
||||
container.style.visibility = 'visible';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*/
|
||||
async function logout() {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
// Call logout API
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
// Global click listener to close dropdowns - ensure this is added only once
|
||||
if (!window._authJsGlobalClickListenerAdded) {
|
||||
document.addEventListener('click', (e) => {
|
||||
console.log('[Auth.js] Global click detected on:', e.target);
|
||||
|
||||
// Re-fetch elements by ID inside the listener to ensure they are current
|
||||
const currentDropdown = document.getElementById('userMenuDropdown');
|
||||
const currentButton = document.getElementById('userMenuBtn'); // Use the standardized ID
|
||||
|
||||
if (currentDropdown && currentButton && currentDropdown.classList.contains('active')) {
|
||||
console.log('[Auth.js] User menu is active, checking if click is outside...');
|
||||
const isOutsideDropdown = !currentDropdown.contains(e.target);
|
||||
const isOutsideButton = !currentButton.contains(e.target);
|
||||
console.log('[Auth.js] Click outside dropdown:', isOutsideDropdown, 'outside button:', isOutsideButton);
|
||||
|
||||
if (isOutsideDropdown && isOutsideButton) {
|
||||
currentDropdown.classList.remove('active');
|
||||
console.log('[Auth.js] User menu closed by global click.');
|
||||
}
|
||||
}
|
||||
|
||||
const currentSettingsMenu = document.getElementById('settingsMenu');
|
||||
const currentSettingsBtn = document.getElementById('settingsBtn');
|
||||
if (currentSettingsMenu && currentSettingsBtn && currentSettingsMenu.classList.contains('active') &&
|
||||
!currentSettingsMenu.contains(e.target) && !currentSettingsBtn.contains(e.target)) {
|
||||
currentSettingsMenu.classList.remove('active');
|
||||
console.log('[Auth.js] Settings menu closed by global click.');
|
||||
}
|
||||
});
|
||||
|
||||
// Clear auth data regardless of API response
|
||||
clearAuthData();
|
||||
updateUIForUnauthenticatedUser();
|
||||
|
||||
// Show success message
|
||||
showToast('Logged out successfully', 'success');
|
||||
|
||||
// Redirect to login page
|
||||
window.location.href = 'login.html';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
|
||||
// Still clear auth data even if API call fails
|
||||
clearAuthData();
|
||||
updateUIForUnauthenticatedUser();
|
||||
|
||||
showToast('Logged out', 'info');
|
||||
|
||||
// Redirect to login page even if there was an error
|
||||
window.location.href = 'login.html';
|
||||
} finally {
|
||||
hideLoading();
|
||||
window._authJsGlobalClickListenerAdded = true;
|
||||
console.log('[Auth.js] Global click listener for dropdowns added.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear authentication data from localStorage
|
||||
*/
|
||||
function clearAuthData() {
|
||||
// Clear both localStorage and global variables
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user_info');
|
||||
authToken = null;
|
||||
currentUser = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate token with the server
|
||||
*/
|
||||
async function validateToken() {
|
||||
if (!authToken) {
|
||||
clearAuthData();
|
||||
updateUIForUnauthenticatedUser();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the full URL to avoid path issues
|
||||
const apiUrl = window.location.origin + '/api/auth/validate-token';
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
// Attach logout listener to logout menu item
|
||||
const logoutMenuItem_original = document.getElementById('logoutMenuItem');
|
||||
if (logoutMenuItem_original) {
|
||||
const logoutMenuItem = logoutMenuItem_original.cloneNode(true); // Ensures fresh listener
|
||||
logoutMenuItem_original.parentNode.replaceChild(logoutMenuItem, logoutMenuItem_original);
|
||||
logoutMenuItem.addEventListener('click', () => {
|
||||
console.log('[Auth.js] Logout menu item clicked.');
|
||||
window.auth.logout();
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Token validation failed:', errorData.message);
|
||||
throw new Error(errorData.message || 'Invalid token');
|
||||
}
|
||||
|
||||
// Token is valid, update last active time
|
||||
const data = await response.json();
|
||||
if (data.user) {
|
||||
currentUser = data.user;
|
||||
localStorage.setItem('user_info', JSON.stringify(currentUser));
|
||||
updateUIForAuthenticatedUser();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Token validation error:', error);
|
||||
clearAuthData();
|
||||
updateUIForUnauthenticatedUser();
|
||||
|
||||
// Only show toast if we're on a page that requires authentication
|
||||
if (window.location.pathname !== '/login.html' &&
|
||||
window.location.pathname !== '/register.html' &&
|
||||
window.location.pathname !== '/reset-password.html' &&
|
||||
window.location.pathname !== '/reset-password-request.html') {
|
||||
showToast('Your session has expired. Please login again.', 'warning');
|
||||
}
|
||||
|
||||
return false;
|
||||
console.log('[Auth.js] Logout menu item listener attached.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add authorization header to fetch requests
|
||||
* @param {Object} options - Fetch options
|
||||
* @returns {Object} - Updated fetch options with auth header
|
||||
*/
|
||||
function addAuthHeader(options = {}) {
|
||||
if (!authToken) return options;
|
||||
|
||||
const headers = options.headers || {};
|
||||
|
||||
return {
|
||||
...options,
|
||||
headers: {
|
||||
...headers,
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication token
|
||||
* @returns {string} - The authentication token
|
||||
*/
|
||||
function getToken() {
|
||||
// Always get the latest token from localStorage and update global variable
|
||||
authToken = localStorage.getItem('auth_token');
|
||||
return authToken;
|
||||
}
|
||||
|
||||
// Export authentication functions for use in other scripts
|
||||
window.auth = {
|
||||
isAuthenticated: () => !!localStorage.getItem('auth_token'),
|
||||
getCurrentUser: () => {
|
||||
const userInfo = localStorage.getItem('user_info');
|
||||
return userInfo ? JSON.parse(userInfo) : null;
|
||||
},
|
||||
getToken: getToken,
|
||||
addAuthHeader,
|
||||
checkAuthState,
|
||||
logout
|
||||
};
|
||||
});
|
||||
|
||||
14
frontend/chart.js
Normal file
14
frontend/chart.js
Normal file
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@ console.log('fix-auth-buttons.js loaded and executing');
|
||||
// Function to check if user is authenticated
|
||||
function isAuthenticated() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
console.log('Auth token check:', !!token);
|
||||
// console.log('Auth token check:', !!token); // Keep console logs minimal here if auth.js is primary
|
||||
return !!token;
|
||||
}
|
||||
|
||||
@@ -21,165 +21,56 @@ function getElementsByText(selector, text) {
|
||||
|
||||
// Function to hide login and register buttons if user is authenticated
|
||||
function updateAuthButtons() {
|
||||
console.log('updateAuthButtons executing...');
|
||||
|
||||
// Check if user is authenticated
|
||||
// console.log('fix-auth-buttons.js: updateAuthButtons executing...'); // Keep console logs minimal here
|
||||
if (isAuthenticated()) {
|
||||
console.log('User is authenticated, hiding login/register buttons');
|
||||
|
||||
// Find login and register buttons using valid selectors
|
||||
// console.log('fix-auth-buttons.js: User is authenticated, hiding login/register buttons');
|
||||
const loginButtons = document.querySelectorAll('a[href="login.html"], .login-btn, .auth-btn.login-btn');
|
||||
const registerButtons = document.querySelectorAll('a[href="register.html"], .register-btn, .auth-btn.register-btn');
|
||||
|
||||
// Find buttons by text content
|
||||
const loginButtonsByText = getElementsByText('a, button', 'Login');
|
||||
const registerButtonsByText = getElementsByText('a, button', 'Register');
|
||||
|
||||
const allLoginButtons = [...loginButtons, ...loginButtonsByText];
|
||||
const allRegisterButtons = [...registerButtons, ...registerButtonsByText];
|
||||
|
||||
console.log('Found login buttons:', allLoginButtons.length);
|
||||
console.log('Found register buttons:', allRegisterButtons.length);
|
||||
|
||||
// Hide buttons if they exist
|
||||
allLoginButtons.forEach(button => {
|
||||
console.log('Hiding login button:', button);
|
||||
button.style.display = 'none';
|
||||
button.style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
allRegisterButtons.forEach(button => {
|
||||
console.log('Hiding register button:', button);
|
||||
button.style.display = 'none';
|
||||
button.style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
// Hide auth container if it exists
|
||||
const authContainer = document.getElementById('authContainer');
|
||||
if (authContainer) {
|
||||
console.log('Hiding auth container');
|
||||
authContainer.style.display = 'none';
|
||||
authContainer.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
// Also try to hide by class
|
||||
const authButtonsContainers = document.querySelectorAll('.auth-buttons');
|
||||
console.log('Found auth buttons containers:', authButtonsContainers.length);
|
||||
authButtonsContainers.forEach(container => {
|
||||
console.log('Hiding auth buttons container');
|
||||
container.style.display = 'none';
|
||||
container.style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
// Show user menu if it exists
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
if (userMenu) {
|
||||
console.log('Showing user menu');
|
||||
userMenu.style.display = 'block';
|
||||
userMenu.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
// Show username if it exists
|
||||
const userMenu = document.getElementById('userMenu'); // Ensure this ID is consistent or use userMenuBtn's parent
|
||||
|
||||
loginButtons.forEach(button => { button.style.display = 'none'; button.style.visibility = 'hidden'; });
|
||||
registerButtons.forEach(button => { button.style.display = 'none'; button.style.visibility = 'hidden'; });
|
||||
if (authContainer) { authContainer.style.display = 'none'; authContainer.style.visibility = 'hidden';}
|
||||
if (userMenu) { userMenu.style.display = 'block'; userMenu.style.visibility = 'visible'; }
|
||||
|
||||
const userInfo = localStorage.getItem('user_info');
|
||||
if (userInfo) {
|
||||
try {
|
||||
const user = JSON.parse(userInfo);
|
||||
const username = user.username || 'User';
|
||||
console.log('Setting username to:', username);
|
||||
|
||||
// Find username display elements
|
||||
const usernameDisplays = document.querySelectorAll('.user-name, #userDisplayName, #userName');
|
||||
usernameDisplays.forEach(display => {
|
||||
if (display) {
|
||||
display.textContent = username;
|
||||
display.style.display = 'inline-block';
|
||||
}
|
||||
});
|
||||
|
||||
// Set user email if element exists
|
||||
const userEmail = document.getElementById('userEmail');
|
||||
if (userEmail && user.email) {
|
||||
userEmail.textContent = user.email;
|
||||
const displayName = user.first_name || user.username || 'User';
|
||||
const userDisplayName = document.getElementById('userDisplayName');
|
||||
if (userDisplayName) userDisplayName.textContent = displayName;
|
||||
|
||||
const userNameMenu = document.getElementById('userName');
|
||||
if (userNameMenu) {
|
||||
userNameMenu.textContent = `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.username || 'User Name';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing user info:', error);
|
||||
}
|
||||
const userEmailMenu = document.getElementById('userEmail');
|
||||
if (userEmailMenu && user.email) userEmailMenu.textContent = user.email;
|
||||
} catch (error) { /* console.error('fix-auth-buttons.js: Error parsing user info:', error); */ }
|
||||
}
|
||||
} else {
|
||||
console.log('User is not authenticated, showing login/register buttons');
|
||||
|
||||
// Find login and register buttons in the new UI
|
||||
// console.log('fix-auth-buttons.js: User is not authenticated, showing login/register buttons');
|
||||
const loginButtons = document.querySelectorAll('a[href="login.html"], .login-btn, .auth-btn.login-btn');
|
||||
const registerButtons = document.querySelectorAll('a[href="register.html"], .register-btn, .auth-btn.register-btn');
|
||||
|
||||
// Show buttons if they exist
|
||||
loginButtons.forEach(button => {
|
||||
button.style.display = 'inline-block';
|
||||
button.style.visibility = 'visible';
|
||||
});
|
||||
|
||||
registerButtons.forEach(button => {
|
||||
button.style.display = 'inline-block';
|
||||
button.style.visibility = 'visible';
|
||||
});
|
||||
|
||||
// Show auth container if it exists
|
||||
const authContainer = document.getElementById('authContainer');
|
||||
if (authContainer) {
|
||||
authContainer.style.display = 'flex';
|
||||
authContainer.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
// Also try to show by class
|
||||
const authButtonsContainers = document.querySelectorAll('.auth-buttons');
|
||||
authButtonsContainers.forEach(container => {
|
||||
container.style.display = 'flex';
|
||||
container.style.visibility = 'visible';
|
||||
});
|
||||
|
||||
// Hide user menu if it exists
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
if (userMenu) {
|
||||
userMenu.style.display = 'none';
|
||||
userMenu.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- USER MENU DROPDOWN UNIVERSAL FIX (IMPROVED) ---
|
||||
function setupUserMenuDropdown() {
|
||||
const userMenuBtn = document.getElementById('userMenuBtn') || document.getElementById('userBtn');
|
||||
const userMenuDropdown = document.getElementById('userMenuDropdown');
|
||||
if (userMenuBtn && userMenuDropdown) {
|
||||
// Remove all previous click listeners by replacing the node
|
||||
const newBtn = userMenuBtn.cloneNode(true);
|
||||
userMenuBtn.parentNode.replaceChild(newBtn, userMenuBtn);
|
||||
newBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
userMenuDropdown.classList.toggle('active');
|
||||
});
|
||||
// Only add one document-level listener
|
||||
if (!window._userMenuDropdownListenerAdded) {
|
||||
document.addEventListener('click', function(e) {
|
||||
if (userMenuDropdown.classList.contains('active') &&
|
||||
!userMenuDropdown.contains(e.target) &&
|
||||
!newBtn.contains(e.target)) {
|
||||
userMenuDropdown.classList.remove('active');
|
||||
}
|
||||
});
|
||||
window._userMenuDropdownListenerAdded = true;
|
||||
}
|
||||
loginButtons.forEach(button => { button.style.display = 'inline-block'; button.style.visibility = 'visible'; });
|
||||
registerButtons.forEach(button => { button.style.display = 'inline-block'; button.style.visibility = 'visible'; });
|
||||
if (authContainer) { authContainer.style.display = 'flex'; authContainer.style.visibility = 'visible'; }
|
||||
if (userMenu) { userMenu.style.display = 'none'; userMenu.style.visibility = 'hidden'; }
|
||||
}
|
||||
}
|
||||
|
||||
// Run immediately
|
||||
console.log('Running updateAuthButtons immediately');
|
||||
// console.log('Running updateAuthButtons immediately from fix-auth-buttons.js');
|
||||
updateAuthButtons();
|
||||
setupUserMenuDropdown();
|
||||
|
||||
// Update auth buttons when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DOMContentLoaded event triggered, updating auth buttons');
|
||||
// console.log('DOMContentLoaded event triggered in fix-auth-buttons.js, updating auth buttons');
|
||||
updateAuthButtons();
|
||||
setupUserMenuDropdown();
|
||||
// REMOVE: setupUserMenuDropdown();
|
||||
});
|
||||
@@ -116,7 +116,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div id="userMenu" class="user-menu" style="display: none;">
|
||||
<button id="userBtn" class="user-btn">
|
||||
<button id="userMenuBtn" class="user-btn">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<span id="userDisplayName">User</span>
|
||||
</button>
|
||||
@@ -336,6 +336,21 @@
|
||||
</div>
|
||||
<!-- End Lifetime Checkbox -->
|
||||
|
||||
<!-- Warranty Entry Method Selection -->
|
||||
<div class="form-group" id="warrantyEntryMethod">
|
||||
<label>Warranty Entry Method</label>
|
||||
<div class="warranty-method-options">
|
||||
<label class="radio-option">
|
||||
<input type="radio" id="durationMethod" name="warranty_method" value="duration" checked>
|
||||
<span>Warranty Duration</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" id="exactDateMethod" name="warranty_method" value="exact_date">
|
||||
<span>Exact Expiration Date</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="warrantyDurationFields">
|
||||
<div class="form-group">
|
||||
<label for="warrantyDurationYears">Warranty Period</label>
|
||||
@@ -356,6 +371,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="exactExpirationField" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="exactExpirationDate">Expiration Date</label>
|
||||
<input type="date" id="exactExpirationDate" name="exact_expiration_date" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="purchasePrice">Purchase Price (Optional)</label>
|
||||
<div class="price-input-wrapper">
|
||||
@@ -580,6 +602,21 @@
|
||||
</div>
|
||||
<!-- End Lifetime Checkbox -->
|
||||
|
||||
<!-- Warranty Entry Method Selection -->
|
||||
<div class="form-group" id="editWarrantyEntryMethod">
|
||||
<label>Warranty Entry Method</label>
|
||||
<div class="warranty-method-options">
|
||||
<label class="radio-option">
|
||||
<input type="radio" id="editDurationMethod" name="edit_warranty_method" value="duration" checked>
|
||||
<span>Warranty Duration</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" id="editExactDateMethod" name="edit_warranty_method" value="exact_date">
|
||||
<span>Exact Expiration Date</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editWarrantyDurationFields">
|
||||
<div class="form-group">
|
||||
<label for="editWarrantyDurationYears">Warranty Period</label>
|
||||
@@ -600,6 +637,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editExactExpirationField" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="editExactExpirationDate">Expiration Date</label>
|
||||
<input type="date" id="editExactExpirationDate" name="exact_expiration_date" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editPurchasePrice">Purchase Price (Optional)</label>
|
||||
<div class="price-input-wrapper">
|
||||
@@ -740,7 +784,51 @@
|
||||
</div>
|
||||
|
||||
<script src="auth.js"></script>
|
||||
<script src="script.js"></script>
|
||||
<script src="script.js?v=20250529004"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Apply footer styles based on theme
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||
document.documentElement.classList.contains('dark-mode') ||
|
||||
document.body.classList.contains('dark-mode');
|
||||
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
// Dark mode styles - using same background as header (#2d2d2d)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
// Light mode styles - using same background as header (#ffffff)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply styles when page loads
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver(applyFooterStyles);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme', 'class']
|
||||
});
|
||||
|
||||
// Also watch body for theme changes
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -95,6 +95,151 @@
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Provider-specific SSO button styles */
|
||||
.btn-google {
|
||||
background-color: #4285f4;
|
||||
border-color: #4285f4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-google:hover, .btn-google:focus {
|
||||
background-color: #357ae8;
|
||||
border-color: #357ae8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-github {
|
||||
background-color: #333;
|
||||
border-color: #333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-github:hover, .btn-github:focus {
|
||||
background-color: #24292e;
|
||||
border-color: #24292e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-microsoft {
|
||||
background-color: #0078d4;
|
||||
border-color: #0078d4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-microsoft:hover, .btn-microsoft:focus {
|
||||
background-color: #106ebe;
|
||||
border-color: #106ebe;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-facebook {
|
||||
background-color: #1877f2;
|
||||
border-color: #1877f2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-facebook:hover, .btn-facebook:focus {
|
||||
background-color: #166fe5;
|
||||
border-color: #166fe5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-twitter {
|
||||
background-color: #1da1f2;
|
||||
border-color: #1da1f2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-twitter:hover, .btn-twitter:focus {
|
||||
background-color: #0d95e8;
|
||||
border-color: #0d95e8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-linkedin {
|
||||
background-color: #0077b5;
|
||||
border-color: #0077b5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-linkedin:hover, .btn-linkedin:focus {
|
||||
background-color: #005885;
|
||||
border-color: #005885;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-apple {
|
||||
background-color: #000;
|
||||
border-color: #000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-apple:hover, .btn-apple:focus {
|
||||
background-color: #333;
|
||||
border-color: #333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-discord {
|
||||
background-color: #5865f2;
|
||||
border-color: #5865f2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-discord:hover, .btn-discord:focus {
|
||||
background-color: #4752c4;
|
||||
border-color: #4752c4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-gitlab {
|
||||
background-color: #fca326;
|
||||
border-color: #fca326;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-gitlab:hover, .btn-gitlab:focus {
|
||||
background-color: #fc9403;
|
||||
border-color: #fc9403;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-bitbucket {
|
||||
background-color: #0052cc;
|
||||
border-color: #0052cc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-bitbucket:hover, .btn-bitbucket:focus {
|
||||
background-color: #0047b3;
|
||||
border-color: #0047b3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-keycloak {
|
||||
background-color: #4d4d4d;
|
||||
border-color: #4d4d4d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-keycloak:hover, .btn-keycloak:focus {
|
||||
background-color: #333;
|
||||
border-color: #333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-okta {
|
||||
background-color: #007dc1;
|
||||
border-color: #007dc1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-okta:hover, .btn-okta:focus {
|
||||
background-color: #005a8b;
|
||||
border-color: #005a8b;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Registration status check script -->
|
||||
@@ -138,8 +283,17 @@
|
||||
<i class="fas fa-sign-in-alt"></i> Login
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0; color: var(--text-muted);">
|
||||
<hr style="border-top: 1px solid var(--border-color); margin-bottom: 10px;">
|
||||
OR
|
||||
</div>
|
||||
|
||||
<a href="/api/oidc/login" id="oidcLoginButton" class="btn btn-secondary btn-block" style="text-decoration: none; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="fab fa-openid" style="margin-right: 8px;"></i> Login with SSO Provider
|
||||
</a>
|
||||
|
||||
<div class="auth-links">
|
||||
<div class="auth-links" style="margin-top: 30px;">
|
||||
<a href="register.html">Create Account</a>
|
||||
<a href="reset-password-request.html">Forgot Password?</a>
|
||||
</div>
|
||||
@@ -147,9 +301,156 @@
|
||||
</div>
|
||||
|
||||
<!-- Script for authentication -->
|
||||
<script src="auth.js"></script>
|
||||
<script src="auth.js?v=20250529002"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check for OIDC errors in URL parameters and display them
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const oidcError = urlParams.get('oidc_error');
|
||||
|
||||
if (oidcError) {
|
||||
let errorMessage = 'An unknown error occurred during SSO login.';
|
||||
switch (oidcError) {
|
||||
case 'oidc_disabled':
|
||||
errorMessage = 'SSO login is currently disabled.';
|
||||
break;
|
||||
case 'oidc_misconfigured':
|
||||
errorMessage = 'SSO is not properly configured. Please contact your administrator.';
|
||||
break;
|
||||
case 'token_exchange_failed':
|
||||
errorMessage = 'Failed to exchange authorization code for tokens with the SSO provider.';
|
||||
break;
|
||||
case 'token_missing':
|
||||
errorMessage = 'Access token was not received from the SSO provider.';
|
||||
break;
|
||||
case 'userinfo_fetch_failed':
|
||||
errorMessage = 'Failed to fetch user information from the SSO provider.';
|
||||
break;
|
||||
case 'userinfo_missing':
|
||||
errorMessage = 'User information was not received from the SSO provider.';
|
||||
break;
|
||||
case 'subject_missing':
|
||||
errorMessage = 'User subject identifier (sub) was missing from SSO provider response.';
|
||||
break;
|
||||
case 'email_missing_for_new_user':
|
||||
errorMessage = 'Email address was not provided by SSO provider, which is required for new user registration.';
|
||||
break;
|
||||
case 'email_conflict_local_account':
|
||||
errorMessage = 'The email address from your SSO provider is already associated with an existing local account. Please log in with your local credentials or contact support.';
|
||||
break;
|
||||
case 'registration_disabled':
|
||||
errorMessage = 'New user registration via SSO is currently disabled. Only existing users can log in with SSO. Please contact your administrator if you need an account.';
|
||||
break;
|
||||
case 'user_processing_failed':
|
||||
errorMessage = 'Failed to process user information after SSO login.';
|
||||
break;
|
||||
case 'db_error':
|
||||
errorMessage = 'A database error occurred during SSO login. Please try again later.';
|
||||
break;
|
||||
case 'internal_error':
|
||||
errorMessage = 'An internal server error occurred during SSO login. Please try again later.';
|
||||
break;
|
||||
}
|
||||
showMessage(errorMessage, 'error');
|
||||
|
||||
// Clean up the URL by removing the oidc_error parameter
|
||||
const newUrl = new URL(window.location);
|
||||
newUrl.searchParams.delete('oidc_error');
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
}
|
||||
|
||||
// Fetch OIDC status and customize button based on provider
|
||||
fetch('/api/auth/oidc-status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const oidcButton = document.getElementById('oidcLoginButton');
|
||||
const orSeparator = document.querySelector('.auth-form + div[style*="text-align: center"]');
|
||||
|
||||
if (!data.oidc_enabled) {
|
||||
if (oidcButton) oidcButton.style.display = 'none';
|
||||
if (orSeparator) orSeparator.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Customize button based on provider
|
||||
if (oidcButton && data.oidc_provider_display_name) {
|
||||
const providerName = data.oidc_provider_display_name.toLowerCase();
|
||||
let buttonText = `Login with ${data.oidc_provider_display_name}`;
|
||||
let iconClass = 'fab fa-openid'; // Default icon
|
||||
let buttonClass = 'btn-secondary'; // Default class
|
||||
|
||||
// Provider-specific customizations
|
||||
switch (providerName) {
|
||||
case 'google':
|
||||
iconClass = 'fab fa-google';
|
||||
buttonClass = 'btn-google';
|
||||
break;
|
||||
case 'github':
|
||||
iconClass = 'fab fa-github';
|
||||
buttonClass = 'btn-github';
|
||||
break;
|
||||
case 'microsoft':
|
||||
case 'azure':
|
||||
case 'office365':
|
||||
iconClass = 'fab fa-microsoft';
|
||||
buttonClass = 'btn-microsoft';
|
||||
break;
|
||||
case 'facebook':
|
||||
iconClass = 'fab fa-facebook-f';
|
||||
buttonClass = 'btn-facebook';
|
||||
break;
|
||||
case 'twitter':
|
||||
iconClass = 'fab fa-twitter';
|
||||
buttonClass = 'btn-twitter';
|
||||
break;
|
||||
case 'linkedin':
|
||||
iconClass = 'fab fa-linkedin-in';
|
||||
buttonClass = 'btn-linkedin';
|
||||
break;
|
||||
case 'apple':
|
||||
iconClass = 'fab fa-apple';
|
||||
buttonClass = 'btn-apple';
|
||||
break;
|
||||
case 'discord':
|
||||
iconClass = 'fab fa-discord';
|
||||
buttonClass = 'btn-discord';
|
||||
break;
|
||||
case 'gitlab':
|
||||
iconClass = 'fab fa-gitlab';
|
||||
buttonClass = 'btn-gitlab';
|
||||
break;
|
||||
case 'bitbucket':
|
||||
iconClass = 'fab fa-bitbucket';
|
||||
buttonClass = 'btn-bitbucket';
|
||||
break;
|
||||
case 'keycloak':
|
||||
iconClass = 'fas fa-key';
|
||||
buttonClass = 'btn-keycloak';
|
||||
break;
|
||||
case 'okta':
|
||||
iconClass = 'fas fa-shield-alt';
|
||||
buttonClass = 'btn-okta';
|
||||
break;
|
||||
default:
|
||||
// Keep generic styling for unknown providers
|
||||
buttonText = `Login with ${data.oidc_provider_display_name}`;
|
||||
break;
|
||||
}
|
||||
|
||||
// Update button styling and content
|
||||
oidcButton.className = `btn ${buttonClass} btn-block`;
|
||||
oidcButton.innerHTML = `<i class="${iconClass}" style="margin-right: 8px;"></i> ${buttonText}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching OIDC status:', error);
|
||||
// Hide button on error as a safe default
|
||||
const oidcButton = document.getElementById('oidcLoginButton');
|
||||
const orSeparator = document.querySelector('.auth-form + div[style*="text-align: center"]');
|
||||
if (oidcButton) oidcButton.style.display = 'none';
|
||||
if (orSeparator) orSeparator.style.display = 'none';
|
||||
});
|
||||
|
||||
// Toggle password visibility
|
||||
const passwordToggle = document.querySelector('.password-toggle');
|
||||
const passwordInput = document.getElementById('password');
|
||||
@@ -166,7 +467,6 @@
|
||||
|
||||
// Handle form submission
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const authMessage = document.getElementById('authMessage');
|
||||
|
||||
loginForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
@@ -229,6 +529,7 @@
|
||||
|
||||
// Helper function to show messages
|
||||
function showMessage(message, type) {
|
||||
const authMessage = document.getElementById('authMessage');
|
||||
authMessage.textContent = message;
|
||||
authMessage.className = 'auth-message';
|
||||
authMessage.classList.add(type);
|
||||
@@ -236,5 +537,33 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || document.documentElement.classList.contains('dark-mode') || document.body.classList.contains('dark-mode');
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
// Dark mode styles - using same background as header (#2d2d2d)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
// Light mode styles - using same background as header (#ffffff)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
const obs = new MutationObserver(applyFooterStyles);
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
|
||||
obs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
</style>
|
||||
|
||||
<!-- Registration status check script -->
|
||||
<script src="registration-status.js"></script>
|
||||
<script src="registration-status.js?v=20250529001"></script>
|
||||
<script>
|
||||
// Additional handling for the register page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -603,5 +603,33 @@
|
||||
// Initialize theme when page loads
|
||||
document.addEventListener('DOMContentLoaded', initializeTheme);
|
||||
</script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || document.documentElement.classList.contains('dark-mode') || document.body.classList.contains('dark-mode');
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
// Dark mode styles - using same background as header (#2d2d2d)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
// Light mode styles - using same background as header (#ffffff)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
const obs = new MutationObserver(applyFooterStyles);
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
|
||||
obs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -237,5 +237,34 @@
|
||||
// Initialize theme when page loads
|
||||
document.addEventListener('DOMContentLoaded', initializeTheme);
|
||||
</script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || document.documentElement.classList.contains('dark-mode') || document.body.classList.contains('dark-mode');
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
// Dark mode styles - using same background as header (#2d2d2d)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
// Light mode styles - using same background as header (#ffffff)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
const obs = new MutationObserver(applyFooterStyles);
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
|
||||
obs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -425,5 +425,34 @@
|
||||
// Initialize theme when page loads
|
||||
document.addEventListener('DOMContentLoaded', initializeTheme);
|
||||
</script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || document.documentElement.classList.contains('dark-mode') || document.body.classList.contains('dark-mode');
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
// Dark mode styles - using same background as header (#2d2d2d)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
// Light mode styles - using same background as header (#ffffff)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
const obs = new MutationObserver(applyFooterStyles);
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
|
||||
obs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -445,6 +445,61 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OIDC SSO Configuration Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>OIDC SSO Configuration</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="oidcSettingsForm">
|
||||
<div class="form-group">
|
||||
<div class="preference-item">
|
||||
<div>
|
||||
<label for="oidcEnabled">Enable OIDC SSO</label>
|
||||
<p class="text-muted">Allow users to log in via an OIDC provider.</p>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="oidcEnabled">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oidcProviderName">OIDC Provider Name</label>
|
||||
<input type="text" id="oidcProviderName" class="form-control" placeholder="e.g., oidc, keycloak, google">
|
||||
<small class="text-muted">Internal name for the OIDC client (e.g., 'oidc').</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oidcClientId">Client ID</label>
|
||||
<input type="text" id="oidcClientId" class="form-control" placeholder="Enter OIDC Client ID">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oidcClientSecret">Client Secret</label>
|
||||
<input type="password" id="oidcClientSecret" class="form-control" placeholder="Enter new secret or leave blank to keep existing">
|
||||
<small class="text-muted">Sensitive value. Stored in the database. An application restart is required for changes to take effect.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oidcIssuerUrl">Issuer URL</label>
|
||||
<input type="url" id="oidcIssuerUrl" class="form-control" placeholder="e.g., https://your-provider.com/realms/your-realm">
|
||||
<small class="text-muted">The base URL of your OIDC provider.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oidcScope">Scope</label>
|
||||
<input type="text" id="oidcScope" class="form-control" placeholder="e.g., openid email profile">
|
||||
<small class="text-muted">Space-separated OIDC scopes.</small>
|
||||
</div>
|
||||
|
||||
<button type="button" id="saveOidcSettingsBtn" class="btn btn-primary">Save OIDC Settings</button>
|
||||
<p id="oidcRestartMessage" class="text-muted" style="margin-top: 10px; display: none;">Application restart is required for OIDC settings to take full effect.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -617,7 +672,36 @@
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="auth.js"></script>
|
||||
<script src="settings-new.js"></script>
|
||||
<script src="auth.js?v=4"></script>
|
||||
<script src="settings-new.js?v=20250529001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || document.documentElement.classList.contains('dark-mode') || document.body.classList.contains('dark-mode');
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
// Dark mode styles - using same background as header (#2d2d2d)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
// Light mode styles - using same background as header (#ffffff)
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
const obs = new MutationObserver(applyFooterStyles);
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
|
||||
obs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -43,6 +43,17 @@ const triggerNotificationsBtn = document.getElementById('triggerNotificationsBtn
|
||||
const registrationEnabled = document.getElementById('registrationEnabled');
|
||||
const saveSiteSettingsBtn = document.getElementById('saveSiteSettingsBtn');
|
||||
const emailBaseUrlInput = document.getElementById('emailBaseUrl'); // Added for email base URL
|
||||
|
||||
// OIDC Settings DOM Elements
|
||||
const oidcEnabledToggle = document.getElementById('oidcEnabled');
|
||||
const oidcProviderNameInput = document.getElementById('oidcProviderName');
|
||||
const oidcClientIdInput = document.getElementById('oidcClientId');
|
||||
const oidcClientSecretInput = document.getElementById('oidcClientSecret');
|
||||
const oidcIssuerUrlInput = document.getElementById('oidcIssuerUrl');
|
||||
const oidcScopeInput = document.getElementById('oidcScope');
|
||||
const saveOidcSettingsBtn = document.getElementById('saveOidcSettingsBtn');
|
||||
const oidcRestartMessage = document.getElementById('oidcRestartMessage');
|
||||
|
||||
const currencySymbolInput = document.getElementById('currencySymbol');
|
||||
const currencySymbolSelect = document.getElementById('currencySymbolSelect');
|
||||
const currencySymbolCustom = document.getElementById('currencySymbolCustom');
|
||||
@@ -346,14 +357,7 @@ async function loadUserData() {
|
||||
if (userEmailDisplay) userEmailDisplay.textContent = currentUser.email || 'N/A';
|
||||
// --- END UPDATE ---
|
||||
|
||||
// Check if user is admin and show admin section
|
||||
if (currentUser.is_admin) {
|
||||
if (adminSection) adminSection.style.display = 'block';
|
||||
if (adminSection && adminSection.style.display === 'block') {
|
||||
if (usersTableBody) loadUsers();
|
||||
if (registrationEnabled) loadSiteSettings();
|
||||
}
|
||||
}
|
||||
// Admin section visibility will be determined after API call
|
||||
}
|
||||
|
||||
// Fetch fresh user data from API
|
||||
@@ -384,13 +388,24 @@ async function loadUserData() {
|
||||
if (userEmailDisplay) userEmailDisplay.textContent = userData.email || 'N/A';
|
||||
// --- END UPDATE ---
|
||||
|
||||
// Show admin section if user is admin
|
||||
// Show admin section if user is admin and load admin-specific data
|
||||
if (userData.is_admin) {
|
||||
if (adminSection) adminSection.style.display = 'block';
|
||||
if (adminSection && adminSection.style.display === 'block') {
|
||||
if (usersTableBody) loadUsers();
|
||||
if (registrationEnabled) loadSiteSettings();
|
||||
if (adminSection) {
|
||||
adminSection.style.display = 'block';
|
||||
// Ensure admin-specific data is loaded AFTER section is visible
|
||||
if (usersTableBody) loadUsers();
|
||||
// Check for site settings elements directly to avoid cache timing issues
|
||||
const hasRegistrationToggle = document.getElementById('registrationEnabled');
|
||||
const hasOidcToggle = document.getElementById('oidcEnabled');
|
||||
if (hasRegistrationToggle || hasOidcToggle) {
|
||||
console.log('Admin settings elements found, loading site settings...');
|
||||
loadSiteSettings();
|
||||
} else {
|
||||
console.warn('Admin settings elements not found - this might be a timing/cache issue');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (adminSection) adminSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update localStorage ONLY if data has changed
|
||||
@@ -666,27 +681,23 @@ function setupEventListeners() {
|
||||
console.log('Setting up event listeners');
|
||||
|
||||
// Set up user menu button click handler
|
||||
const userMenuBtn = document.getElementById('userMenuBtn');
|
||||
const userMenuDropdown = document.getElementById('userMenuDropdown');
|
||||
|
||||
if (userMenuBtn && userMenuDropdown) {
|
||||
console.log('Setting up user menu button click handler');
|
||||
|
||||
// Toggle dropdown when user button is clicked
|
||||
userMenuBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
userMenuDropdown.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (userMenuDropdown.classList.contains('active') &&
|
||||
!userMenuDropdown.contains(e.target) &&
|
||||
!userMenuBtn.contains(e.target)) {
|
||||
userMenuDropdown.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
// const userMenuBtn = document.getElementById('userMenuBtn'); // REMOVE/COMMENT OUT
|
||||
// const userMenuDropdown = document.getElementById('userMenuDropdown'); // REMOVE/COMMENT OUT
|
||||
|
||||
// if (userMenuBtn && userMenuDropdown) { // REMOVE/COMMENT OUT THIS ENTIRE BLOCK
|
||||
// console.log('Setting up user menu button click handler');
|
||||
// userMenuBtn.addEventListener('click', function(e) {
|
||||
// e.stopPropagation();
|
||||
// userMenuDropdown.classList.toggle('active');
|
||||
// });
|
||||
// document.addEventListener('click', function(e) {
|
||||
// if (userMenuDropdown.classList.contains('active') &&
|
||||
// !userMenuDropdown.contains(e.target) &&
|
||||
// !userMenuBtn.contains(e.target)) {
|
||||
// userMenuDropdown.classList.remove('active');
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// Dark mode toggle in header (no longer exists)
|
||||
if (darkModeToggle) {
|
||||
@@ -807,7 +818,14 @@ function setupEventListeners() {
|
||||
// Site settings save button
|
||||
if (saveSiteSettingsBtn) {
|
||||
saveSiteSettingsBtn.addEventListener('click', function() {
|
||||
saveSiteSettings();
|
||||
saveSiteSettings(); // This will now also handle non-OIDC site settings
|
||||
});
|
||||
}
|
||||
|
||||
// Save OIDC settings button
|
||||
if (saveOidcSettingsBtn) {
|
||||
saveOidcSettingsBtn.addEventListener('click', function() {
|
||||
saveOidcSettings();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2374,17 +2392,27 @@ function closeAllModals() {
|
||||
*/
|
||||
async function loadSiteSettings() {
|
||||
console.log('Loading site settings...');
|
||||
const adminSection = document.getElementById('adminSection');
|
||||
const registrationToggle = document.getElementById('registrationEnabled');
|
||||
const emailBaseUrlField = document.getElementById('emailBaseUrl'); // Correct variable name
|
||||
|
||||
// Enhanced debugging for element availability
|
||||
console.log('[SiteSettings Debug] DOM readiness check:');
|
||||
console.log(' - document.readyState:', document.readyState);
|
||||
console.log(' - adminSection exists:', !!document.getElementById('adminSection'));
|
||||
console.log(' - registrationEnabled exists:', !!document.getElementById('registrationEnabled'));
|
||||
console.log(' - oidcEnabled exists:', !!document.getElementById('oidcEnabled'));
|
||||
console.log(' - oidcProviderName exists:', !!document.getElementById('oidcProviderName'));
|
||||
console.log(' - oidcClientId exists:', !!document.getElementById('oidcClientId'));
|
||||
|
||||
// Query elements locally within this function scope for population
|
||||
const registrationToggleElem = document.getElementById('registrationEnabled');
|
||||
const emailBaseUrlFieldElem = document.getElementById('emailBaseUrl');
|
||||
const oidcEnabledToggleElem = document.getElementById('oidcEnabled');
|
||||
const oidcProviderNameInputElem = document.getElementById('oidcProviderName');
|
||||
const oidcClientIdInputElem = document.getElementById('oidcClientId');
|
||||
const oidcClientSecretInputElem = document.getElementById('oidcClientSecret');
|
||||
const oidcIssuerUrlInputElem = document.getElementById('oidcIssuerUrl');
|
||||
const oidcScopeInputElem = document.getElementById('oidcScope');
|
||||
|
||||
// // Check if admin section exists
|
||||
// if (!adminSection) {
|
||||
// console.log('Admin section not found, skipping site settings load');
|
||||
// return;
|
||||
// }
|
||||
|
||||
try { // Correct structure: try block starts
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
@@ -2400,75 +2428,125 @@ async function loadSiteSettings() {
|
||||
}
|
||||
|
||||
const settings = await response.json();
|
||||
console.log('[SiteSettings] Raw settings received from API:', settings);
|
||||
|
||||
if (registrationToggle) {
|
||||
registrationToggle.checked = settings.registration_enabled === 'true';
|
||||
if (registrationToggleElem) {
|
||||
registrationToggleElem.checked = settings.registration_enabled === 'true';
|
||||
} else {
|
||||
console.error('[SiteSettings] registrationEnabled element NOT FOUND locally.');
|
||||
}
|
||||
|
||||
if (emailBaseUrlField) { // Use correct variable name
|
||||
emailBaseUrlField.value = settings.email_base_url || 'http://localhost:8080'; // Set the value
|
||||
if (emailBaseUrlFieldElem) {
|
||||
emailBaseUrlFieldElem.value = settings.email_base_url || 'http://localhost:8080';
|
||||
} else {
|
||||
console.error('[SiteSettings] emailBaseUrl element NOT FOUND locally.');
|
||||
}
|
||||
|
||||
// Populate OIDC settings using locally-scoped element variables
|
||||
if (oidcEnabledToggleElem) {
|
||||
console.log('[OIDC Settings] Found oidcEnabledToggleElem. Setting checked to:', settings.oidc_enabled === 'true');
|
||||
oidcEnabledToggleElem.checked = settings.oidc_enabled === 'true';
|
||||
} else {
|
||||
console.error('[OIDC Settings] oidcEnabledToggleElem element NOT FOUND locally.');
|
||||
}
|
||||
|
||||
if (oidcProviderNameInputElem) {
|
||||
console.log('[OIDC Settings] Found oidcProviderNameInputElem. Setting value to:', settings.oidc_provider_name || 'oidc');
|
||||
oidcProviderNameInputElem.value = settings.oidc_provider_name || 'oidc';
|
||||
} else {
|
||||
console.error('[OIDC Settings] oidcProviderNameInputElem element NOT FOUND locally.');
|
||||
}
|
||||
|
||||
if (oidcClientIdInputElem) {
|
||||
console.log('[OIDC Settings] Found oidcClientIdInputElem. Setting value to:', settings.oidc_client_id || '');
|
||||
oidcClientIdInputElem.value = settings.oidc_client_id || '';
|
||||
} else {
|
||||
console.error('[OIDC Settings] oidcClientIdInputElem element NOT FOUND locally.');
|
||||
}
|
||||
|
||||
if (oidcClientSecretInputElem) {
|
||||
console.log('[OIDC Settings] Found oidcClientSecretInputElem. Setting placeholder based on oidc_client_secret_set:', settings.oidc_client_secret_set);
|
||||
oidcClientSecretInputElem.value = ''; // Always clear on load
|
||||
oidcClientSecretInputElem.placeholder = settings.oidc_client_secret_set ? '******** (Set - Enter new to change)' : 'Enter OIDC Client Secret';
|
||||
} else {
|
||||
console.error('[OIDC Settings] oidcClientSecretInputElem element NOT FOUND locally.');
|
||||
}
|
||||
|
||||
if (oidcIssuerUrlInputElem) {
|
||||
console.log('[OIDC Settings] Found oidcIssuerUrlInputElem. Setting value to:', settings.oidc_issuer_url || '');
|
||||
oidcIssuerUrlInputElem.value = settings.oidc_issuer_url || '';
|
||||
} else {
|
||||
console.error('[OIDC Settings] oidcIssuerUrlInputElem element NOT FOUND locally.');
|
||||
}
|
||||
|
||||
if (oidcScopeInputElem) {
|
||||
console.log('[OIDC Settings] Found oidcScopeInputElem. Setting value to:', settings.oidc_scope || 'openid email profile');
|
||||
oidcScopeInputElem.value = settings.oidc_scope || 'openid email profile';
|
||||
} else {
|
||||
console.error('[OIDC Settings] oidcScopeInputElem element NOT FOUND locally.');
|
||||
}
|
||||
|
||||
console.log('Site settings loaded successfully', settings);
|
||||
console.log('Site and OIDC settings loaded and population attempted using locally queried elements.');
|
||||
|
||||
} catch (error) { // Correct structure: catch block follows try
|
||||
console.error('Error loading site settings:', error);
|
||||
} catch (error) {
|
||||
console.error('Error loading or populating site settings:', error);
|
||||
showToast('Failed to load site settings. Please try again.', 'error');
|
||||
} finally { // Correct structure: finally block follows catch
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save site settings
|
||||
* Save site settings (non-OIDC part)
|
||||
*/
|
||||
async function saveSiteSettings() {
|
||||
console.log('Saving site settings...');
|
||||
console.log('Saving site settings (non-OIDC)...');
|
||||
const registrationToggle = document.getElementById('registrationEnabled');
|
||||
const emailBaseUrlField = document.getElementById('emailBaseUrl'); // Get the new field
|
||||
const emailBaseUrlField = document.getElementById('emailBaseUrl');
|
||||
|
||||
const settings = {};
|
||||
const settingsToSave = {};
|
||||
|
||||
if (registrationToggle) {
|
||||
settings.registration_enabled = registrationToggle.checked; // Boolean value will be converted to string in backend
|
||||
settingsToSave.registration_enabled = registrationToggle.checked;
|
||||
}
|
||||
|
||||
if (emailBaseUrlField) {
|
||||
let baseUrl = emailBaseUrlField.value.trim();
|
||||
// Basic validation: check if it looks somewhat like a URL
|
||||
if (baseUrl && (baseUrl.startsWith('http://') || baseUrl.startsWith('https://'))) {
|
||||
// Remove trailing slash if present
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
settings.email_base_url = baseUrl;
|
||||
settingsToSave.email_base_url = baseUrl;
|
||||
} else if (baseUrl) {
|
||||
showToast('Invalid Email Base URL format. It should start with http:// or https://', 'error');
|
||||
return; // Stop saving if format is invalid
|
||||
return;
|
||||
} else {
|
||||
settings.email_base_url = 'http://localhost:8080'; // Use default if empty
|
||||
emailBaseUrlField.value = settings.email_base_url; // Update field with default
|
||||
settingsToSave.email_base_url = 'http://localhost:8080';
|
||||
emailBaseUrlField.value = settingsToSave.email_base_url;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (Object.keys(settingsToSave).length === 0) {
|
||||
showToast('No site settings to save.', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.auth.getToken()}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(settings)
|
||||
body: JSON.stringify(settingsToSave)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
showToast('Site settings saved successfully', 'success');
|
||||
showToast(result.message || 'Site settings saved successfully', 'success');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
showToast(errorData.message || 'Failed to save site settings', 'error');
|
||||
showToast(result.message || 'Failed to save site settings', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving site settings:', error);
|
||||
@@ -2478,6 +2556,59 @@ async function saveSiteSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save OIDC settings
|
||||
*/
|
||||
async function saveOidcSettings() {
|
||||
console.log('Saving OIDC settings...');
|
||||
const oidcSettingsPayload = {
|
||||
oidc_enabled: oidcEnabledToggle ? oidcEnabledToggle.checked : false,
|
||||
oidc_provider_name: oidcProviderNameInput ? oidcProviderNameInput.value.trim() : 'oidc',
|
||||
oidc_client_id: oidcClientIdInput ? oidcClientIdInput.value.trim() : '',
|
||||
oidc_issuer_url: oidcIssuerUrlInput ? oidcIssuerUrlInput.value.trim() : '',
|
||||
oidc_scope: oidcScopeInput ? oidcScopeInput.value.trim() : 'openid email profile',
|
||||
};
|
||||
|
||||
// Only include client_secret if a new value is entered
|
||||
if (oidcClientSecretInput && oidcClientSecretInput.value) {
|
||||
oidcSettingsPayload.oidc_client_secret = oidcClientSecretInput.value;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.auth.getToken()}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(oidcSettingsPayload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
showToast(result.message || 'OIDC settings saved successfully.', 'success');
|
||||
if (result.message && result.message.includes("restart is required")) {
|
||||
if(oidcRestartMessage) oidcRestartMessage.style.display = 'block';
|
||||
} else {
|
||||
if(oidcRestartMessage) oidcRestartMessage.style.display = 'none';
|
||||
}
|
||||
// Clear the secret field after attempting to save
|
||||
if (oidcClientSecretInput) oidcClientSecretInput.value = '';
|
||||
// Reload settings to get the oidc_client_secret_set flag updated
|
||||
loadSiteSettings();
|
||||
} else {
|
||||
showToast(result.message || 'Failed to save OIDC settings.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving OIDC settings:', error);
|
||||
showToast('Failed to save OIDC settings. Please try again.', 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the delete button for user deletion
|
||||
*/
|
||||
@@ -3021,4 +3152,4 @@ if (currencySymbolSelect && currencySymbolCustom) {
|
||||
currencySymbolCustom.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<!-- Load header fix styles to ensure consistent header styling -->
|
||||
<link rel="stylesheet" href="header-fix.css">
|
||||
<!-- Chart.js for visualizations -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="chart.js"></script>
|
||||
<!-- Load fix for auth buttons -->
|
||||
<script src="fix-auth-buttons-loader.js"></script>
|
||||
<script src="script.js" defer></script> <!-- Added script.js -->
|
||||
@@ -241,7 +241,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div id="userMenu" class="user-menu" style="display: none;">
|
||||
<button id="userBtn" class="user-btn">
|
||||
<button id="userMenuBtn" class="user-btn">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<span id="userDisplayName">User</span>
|
||||
</button>
|
||||
@@ -465,6 +465,21 @@
|
||||
</div>
|
||||
<!-- End Lifetime Checkbox -->
|
||||
|
||||
<!-- Warranty Entry Method Selection -->
|
||||
<div class="form-group" id="editWarrantyEntryMethod">
|
||||
<label>Warranty Entry Method</label>
|
||||
<div class="warranty-method-options">
|
||||
<label class="radio-option">
|
||||
<input type="radio" id="editDurationMethod" name="edit_warranty_method" value="duration" checked>
|
||||
<span>Warranty Duration</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" id="editExactDateMethod" name="edit_warranty_method" value="exact_date">
|
||||
<span>Exact Expiration Date</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editWarrantyDurationFields">
|
||||
<div class="form-group">
|
||||
<label for="editWarrantyDurationYears">Warranty Period</label>
|
||||
@@ -485,6 +500,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editExactExpirationField" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="editExactExpirationDate">Expiration Date</label>
|
||||
<input type="date" id="editExactExpirationDate" name="exact_expiration_date" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editPurchasePrice">Purchase Price (Optional)</label>
|
||||
<div class="price-input-wrapper">
|
||||
@@ -623,5 +645,37 @@
|
||||
|
||||
<script src="auth.js"></script>
|
||||
<script src="status.js" defer></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
<p>Powered by <a href="https://github.com/sassanix/warracker" target="_blank" rel="noopener noreferrer" id="warrackerFooterLink">Warracker</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Apply footer styles based on theme
|
||||
function applyFooterStyles() {
|
||||
const footer = document.getElementById('warrackerFooter');
|
||||
const link = document.getElementById('warrackerFooterLink');
|
||||
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||
document.documentElement.classList.contains('dark-mode') ||
|
||||
document.body.classList.contains('dark-mode');
|
||||
|
||||
if (footer) {
|
||||
if (isDarkMode) {
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #444; background-color: #2d2d2d; color: #e0e0e0; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #4dabf7; text-decoration: none; font-weight: 500;';
|
||||
} else {
|
||||
footer.style.cssText = 'margin-top: 50px; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0; background-color: #ffffff; color: #333333; font-size: 0.9rem;';
|
||||
if (link) link.style.cssText = 'color: #3498db; text-decoration: none; font-weight: 500;';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', applyFooterStyles);
|
||||
const observer = new MutationObserver(applyFooterStyles);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
|
||||
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -325,7 +325,7 @@
|
||||
row.className = statusClass;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${escapeHTML(warranty.product_name)}</td>
|
||||
<td title="${escapeHTML(warranty.product_name)}">${escapeHTML(warranty.product_name)}</td>
|
||||
<td>${warranty.purchase_date ? formatDateYYYYMMDD(new Date(warranty.purchase_date)) : 'N/A'}</td>
|
||||
<td>${warranty.is_lifetime ? 'Lifetime' : (warranty.expiration_date ? formatDateYYYYMMDD(new Date(warranty.expiration_date)) : 'N/A')}</td>
|
||||
<td><span class="${statusClass}">${statusText}</span></td>
|
||||
|
||||
@@ -3676,3 +3676,116 @@ input.invalid {
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Style for the warranty duration group when hidden (due to lifetime checkbox) */
|
||||
#warrantyDurationFields.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Warranty Entry Method Styling */
|
||||
.warranty-method-options {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
gap: 20px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.warranty-method-options .radio-option {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.warranty-method-options .radio-option input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.warranty-method-options .radio-option span {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Ensure both add and edit modal warranty methods display horizontally */
|
||||
#warrantyEntryMethod .warranty-method-options,
|
||||
#editWarrantyEntryMethod .warranty-method-options {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Mobile responsive - stack vertically only on very small screens if needed */
|
||||
@media (max-width: 480px) {
|
||||
.warranty-method-options {
|
||||
flex-direction: column !important;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.warranty-method-options .radio-option {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* Powered by Warracker footer */
|
||||
.warracker-footer {
|
||||
margin-top: 50px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background-color: var(--card-bg) !important;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.warracker-footer a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.warracker-footer a:hover {
|
||||
color: #2980b9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Dark theme styles */
|
||||
[data-theme="dark"] .warracker-footer {
|
||||
border-top-color: #444;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .warracker-footer a {
|
||||
color: #4dabf7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .warracker-footer a:hover {
|
||||
color: #339af0;
|
||||
}
|
||||
|
||||
/* Also support html.dark-mode for older implementations */
|
||||
html.dark-mode .warracker-footer {
|
||||
border-top-color: #444;
|
||||
}
|
||||
|
||||
html.dark-mode .warracker-footer a {
|
||||
color: #4dabf7;
|
||||
}
|
||||
|
||||
html.dark-mode .warracker-footer a:hover {
|
||||
color: #339af0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.warracker-footer {
|
||||
margin-top: 30px;
|
||||
padding: 15px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Version checker for Warracker
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const currentVersion = '0.9.9.8'; // Current version of the application
|
||||
const currentVersion = '0.9.9.9'; // Current version of the application
|
||||
const updateStatus = document.getElementById('updateStatus');
|
||||
const updateLink = document.getElementById('updateLink');
|
||||
|
||||
|
||||
28
nginx.conf
28
nginx.conf
@@ -31,6 +31,8 @@ server {
|
||||
image/svg+xml svg svgz;
|
||||
application/pdf pdf;
|
||||
image/x-icon ico;
|
||||
application/json json;
|
||||
application/manifest+json webmanifest manifest;
|
||||
}
|
||||
|
||||
# API requests - proxy to backend (fixed upstream host)
|
||||
@@ -46,6 +48,17 @@ server {
|
||||
# Pass Authorization header to backend
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
|
||||
# Enhanced proxy settings for file handling
|
||||
proxy_buffering off; # Disable buffering for file downloads to prevent content-length mismatches
|
||||
proxy_request_buffering off; # Disable request buffering for uploads
|
||||
proxy_read_timeout 300s; # Increased timeout for large file transfers
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 300s;
|
||||
|
||||
# Prevent proxy from modifying response headers that could cause issues
|
||||
proxy_set_header Connection "";
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Add debug headers to see what's happening
|
||||
add_header X-Debug-Message "API request proxied to backend" always;
|
||||
|
||||
@@ -98,6 +111,21 @@ server {
|
||||
add_header Content-Type text/plain;
|
||||
return 200 "Uploads directory exists: $document_root\n";
|
||||
}
|
||||
|
||||
# Specific handling for manifest.json
|
||||
location = /manifest.json {
|
||||
alias /var/www/html/manifest.json; # Serve from this specific path
|
||||
# The global types block should set the correct Content-Type
|
||||
# (application/manifest+json for 'manifest' extension)
|
||||
# If not found by alias, it will result in a 404, which is correct.
|
||||
# No need for try_files here if alias is used and file must exist.
|
||||
# If you want to be explicit about 404 if alias target doesn't exist:
|
||||
# if (!-f /var/www/html/manifest.json) { return 404; }
|
||||
# However, alias itself should handle this.
|
||||
# Add caching headers if desired, e.g.:
|
||||
# expires 1d;
|
||||
# add_header Cache-Control "public, must-revalidate";
|
||||
}
|
||||
|
||||
# Default location
|
||||
location / {
|
||||
|
||||
Reference in New Issue
Block a user