Files
infoscreen/AUTH_SYSTEM.md
RobbStarkAustria a7df3c2708 feat(dashboard): header user dropdown (Syncfusion) + proper logout; docs: clarify architecture; build: add splitbuttons; bump alpha.10
Dashboard

Add top-right user dropdown using Syncfusion DropDownButton: shows username + role; menu entries “Profil” and “Abmelden”.
Replace custom dropdown logic with Syncfusion component; position at header’s right edge.
Update /logout page to call backend logout and redirect to /login (reliable user switching).
Build/Config

Add @syncfusion/ej2-react-splitbuttons and @syncfusion/ej2-splitbuttons dependencies.
Update Vite optimizeDeps.include to pre-bundle splitbuttons and avoid import-analysis errors.
Docs

README: Rework Architecture Overview with clearer data flow:
Listener consumes MQTT (discovery/heartbeats) and updates API.
Scheduler reads from API and publishes events via MQTT to clients.
Clients send via MQTT and receive via MQTT.
Worker receives commands directly from API and reports results back (no MQTT).
Explicit note: MariaDB is accessed exclusively by the API Server; Dashboard never talks to DB directly.
README: Add SplitButtons to “Syncfusion Components Used”; add troubleshooting steps for @syncfusion/ej2-react-splitbuttons import issues (optimizeDeps + volume reset).
Copilot instructions: Document header user menu and splitbuttons technical notes (deps, optimizeDeps, dev-container node_modules volume).
Program info

Bump to 2025.1.0-alpha.10 with changelog:
UI: Header user menu (DropDownButton with username/role; Profil/Abmelden).
Frontend: Syncfusion SplitButtons integration + Vite pre-bundling config.
Fix: Added README guidance for splitbuttons import errors.
No breaking changes.
2025-10-15 16:33:35 +00:00

13 KiB

Authentication System Documentation

This document describes the authentication and authorization system implemented in the infoscreen_2025 project.

Overview

The system provides session-based authentication with role-based access control (RBAC). It includes:

  • Backend: Flask session-based auth with bcrypt password hashing
  • Frontend: React context/hooks for managing authentication state
  • Permissions: Decorators for protecting routes based on user roles
  • Roles: Four levels (user, editor, admin, superadmin)

Architecture

Backend Components

1. Auth Routes (server/routes/auth.py)

Provides authentication endpoints:

  • POST /api/auth/login - Authenticate user and create session
  • POST /api/auth/logout - End user session
  • GET /api/auth/me - Get current user info (protected)
  • GET /api/auth/check - Quick auth status check

2. Permission Decorators (server/permissions.py)

Decorators for protecting routes:

from server.permissions import require_role, admin_or_higher, editor_or_higher

# Require specific role(s)
@app.route('/admin-settings')
@require_role('admin', 'superadmin')
def admin_settings():
    return "Admin only"

# Convenience decorators
@app.route('/settings')
@admin_or_higher  # admin or superadmin
def settings():
    return "Settings"

@app.route('/events', methods=['POST'])
@editor_or_higher  # editor, admin, or superadmin
def create_event():
    return "Create event"

Available decorators:

  • @require_auth - Just require authentication
  • @require_role(*roles) - Require any of specified roles
  • @superadmin_only - Superadmin only
  • @admin_or_higher - Admin or superadmin
  • @editor_or_higher - Editor, admin, or superadmin

3. Session Configuration (server/wsgi.py)

Flask session configured with:

  • Secret key from FLASK_SECRET_KEY environment variable
  • HTTPOnly cookies (prevent XSS)
  • SameSite=Lax (CSRF protection)
  • Secure flag in production (HTTPS only)

Frontend Components

1. API Client (dashboard/src/apiAuth.ts)

TypeScript functions for auth operations:

import { login, logout, fetchCurrentUser } from './apiAuth';

// Login
await login('username', 'password');

// Get current user
const user = await fetchCurrentUser();

// Logout
await logout();

// Check auth status (lightweight)
const { authenticated, role } = await checkAuth();

Helper functions:

import { hasRole, hasAnyRole, isAdminOrHigher } from './apiAuth';

if (isAdminOrHigher(user)) {
  // Show admin UI
}

2. Auth Context/Hooks (dashboard/src/useAuth.tsx)

React context for managing auth state:

import { useAuth, useCurrentUser, useIsAuthenticated } from './useAuth';

function MyComponent() {
  // Full auth context
  const { user, login, logout, loading, error, isAuthenticated } = useAuth();
  
  // Or just what you need
  const user = useCurrentUser();
  const isAuth = useIsAuthenticated();
  
  if (loading) return <div>Loading...</div>;
  
  if (!isAuthenticated) {
    return <LoginForm onLogin={login} />;
  }
  
  return <div>Welcome {user.username}!</div>;
}

User Roles

Four hierarchical roles with increasing permissions:

Role Value Description Use Case
User user Read-only access View events only
Editor editor Can CRUD events/media Content managers
Admin admin Manage settings, users (except superadmin), groups Organization staff
Superadmin superadmin Full system access Developers, system admins

Permission Matrix

Action User Editor Admin Superadmin
View events
Create/edit events
Manage media
Manage groups/clients
Manage users (non-superadmin)
Manage settings
Manage superadmins
System configuration

Setup Instructions

1. Environment Configuration

Add to your .env file:

# Flask session secret key (REQUIRED)
# Generate with: python -c 'import secrets; print(secrets.token_hex(32))'
FLASK_SECRET_KEY=your_secret_key_here

# Superadmin account (REQUIRED for initial setup)
DEFAULT_SUPERADMIN_USERNAME=superadmin
DEFAULT_SUPERADMIN_PASSWORD=your_secure_password

2. Database Initialization

The superadmin user is created automatically when containers start. See SUPERADMIN_SETUP.md for details.

3. Frontend Integration

Wrap your app with AuthProvider in main.tsx or App.tsx:

import { AuthProvider } from './useAuth';

function App() {
  return (
    <AuthProvider>
      {/* Your app components */}
    </AuthProvider>
  );
}

Usage Examples

Backend: Protecting Routes

from flask import Blueprint
from server.permissions import require_role, admin_or_higher

users_bp = Blueprint("users", __name__, url_prefix="/api/users")

@users_bp.route("", methods=["GET"])
@admin_or_higher
def list_users():
    """List all users - admin+ only"""
    # Implementation
    pass

@users_bp.route("", methods=["POST"])
@require_role('superadmin')
def create_superadmin():
    """Create superadmin - superadmin only"""
    # Implementation
    pass

Frontend: Conditional Rendering

import { useAuth } from './useAuth';
import { isAdminOrHigher, isEditorOrHigher } from './apiAuth';

function NavigationMenu() {
  const { user } = useAuth();
  
  return (
    <nav>
      <a href="/dashboard">Dashboard</a>
      <a href="/events">Events</a>
      
      {isEditorOrHigher(user) && (
        <a href="/events/new">Create Event</a>
      )}
      
      {isAdminOrHigher(user) && (
        <>
          <a href="/settings">Settings</a>
          <a href="/users">Manage Users</a>
          <a href="/groups">Manage Groups</a>
        </>
      )}
    </nav>
  );
}

Frontend: Login Form Example

import { useState } from 'react';
import { useAuth } from './useAuth';

function LoginPage() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const { login, loading, error } = useAuth();
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await login(username, password);
      // Redirect on success
      window.location.href = '/dashboard';
    } catch (err) {
      // Error is already in auth context
      console.error('Login failed:', err);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h1>Login</h1>
      {error && <div className="error">{error}</div>}
      
      <input
        type="text"
        placeholder="Username"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        disabled={loading}
      />
      
      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        disabled={loading}
      />
      
      <button type="submit" disabled={loading}>
        {loading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

Security Considerations

Backend Security

  1. Password Hashing: All passwords hashed with bcrypt (salt rounds default)
  2. Session Security:
    • HTTPOnly cookies (prevent XSS access)
    • SameSite=Lax (CSRF protection)
    • Secure flag in production (HTTPS only)
  3. Secret Key: Must be set via environment variable, not hardcoded
  4. Role Checking: Server-side validation on every protected route

Frontend Security

  1. Credentials: Always use credentials: 'include' in fetch calls
  2. No Password Storage: Never store passwords in localStorage/sessionStorage
  3. Role Gating: UI gating is convenience, not security (always validate server-side)
  4. HTTPS: Always use HTTPS in production

Production Checklist

  • Generate strong FLASK_SECRET_KEY (32+ bytes)
  • Set SESSION_COOKIE_SECURE=True (handled automatically by ENV=production)
  • Use HTTPS with valid TLS certificate
  • Change default superadmin password after first login
  • Review and audit user roles regularly
  • Enable audit logging (future enhancement)

API Reference

Authentication Endpoints

POST /api/auth/login

Authenticate user and create session.

Request:

{
  "username": "string",
  "password": "string"
}

Response (200):

{
  "message": "Login successful",
  "user": {
    "id": 1,
    "username": "admin",
    "role": "admin"
  }
}

Errors:

  • 400 - Missing username or password
  • 401 - Invalid credentials or account disabled

POST /api/auth/logout

End current session.

Response (200):

{
  "message": "Logout successful"
}

GET /api/auth/me

Get current user information (requires authentication).

Response (200):

{
  "id": 1,
  "username": "admin",
  "role": "admin",
  "is_active": true
}

Errors:

  • 401 - Not authenticated or account disabled

GET /api/auth/check

Quick authentication status check.

Response (200):

{
  "authenticated": true,
  "role": "admin"
}

Or if not authenticated:

{
  "authenticated": false
}

Testing

Manual Testing

  1. Create test users (via database or future user management UI):

    INSERT INTO users (username, password_hash, role, is_active)
    VALUES ('testuser', '<bcrypt_hash>', 'user', 1);
    
  2. Test login:

    curl -X POST http://localhost:8000/api/auth/login \
      -H "Content-Type: application/json" \
      -d '{"username":"superadmin","password":"your_password"}' \
      -c cookies.txt
    
  3. Test /me endpoint:

    curl http://localhost:8000/api/auth/me -b cookies.txt
    
  4. Test protected route:

    # Should fail without auth
    curl http://localhost:8000/api/protected
    
    # Should work with cookie
    curl http://localhost:8000/api/protected -b cookies.txt
    

Automated Testing

Example test cases (to be implemented):

def test_login_success():
    response = client.post('/api/auth/login', json={
        'username': 'testuser',
        'password': 'testpass'
    })
    assert response.status_code == 200
    assert 'user' in response.json

def test_login_invalid_credentials():
    response = client.post('/api/auth/login', json={
        'username': 'testuser',
        'password': 'wrongpass'
    })
    assert response.status_code == 401

def test_me_authenticated():
    # Login first
    client.post('/api/auth/login', json={'username': 'testuser', 'password': 'testpass'})
    response = client.get('/api/auth/me')
    assert response.status_code == 200
    assert response.json['username'] == 'testuser'

def test_me_not_authenticated():
    response = client.get('/api/auth/me')
    assert response.status_code == 401

Troubleshooting

Login Not Working

Symptoms: Login endpoint returns 401 even with correct credentials

Solutions:

  1. Verify user exists in database: SELECT * FROM users WHERE username='...'
  2. Check password hash is valid bcrypt format
  3. Verify user is_active=1
  4. Check server logs for bcrypt errors

Session Not Persisting

Symptoms: /api/auth/me returns 401 after successful login

Solutions:

  1. Verify FLASK_SECRET_KEY is set
  2. Check frontend is sending credentials: 'include' in fetch
  3. Verify cookies are being set (check browser DevTools)
  4. Check CORS settings if frontend/backend on different domains

Permission Denied on Protected Route

Symptoms: 403 error on decorated routes

Solutions:

  1. Verify user is logged in (/api/auth/me)
  2. Check user role matches required role
  3. Verify decorator is applied correctly
  4. Check session hasn't expired

TypeScript Errors in Frontend

Symptoms: Type errors when using auth hooks

Solutions:

  1. Ensure AuthProvider is wrapping your app
  2. Import types correctly: import type { User } from './apiAuth'
  3. Check TypeScript config for verbatimModuleSyntax

Next Steps

See userrole-management.md for the complete implementation roadmap:

  1. Extend User Model - Done
  2. Seed Superadmin - Done (init_defaults.py)
  3. Expose Current User Role - Done (this document)
  4. Implement Minimal Role Enforcement - Apply decorators to existing routes
  5. Test the Flow - Verify permissions work correctly
  6. Frontend Role Gating - Update UI components
  7. User Management UI - Build admin interface

References