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

523 lines
13 KiB
Markdown

# 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:
```python
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:
```typescript
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:
```typescript
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:
```typescript
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:
```bash
# 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`:
```typescript
import { AuthProvider } from './useAuth';
function App() {
return (
<AuthProvider>
{/* Your app components */}
</AuthProvider>
);
}
```
## Usage Examples
### Backend: Protecting Routes
```python
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
```typescript
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
```typescript
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:**
```json
{
"username": "string",
"password": "string"
}
```
**Response (200):**
```json
{
"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):**
```json
{
"message": "Logout successful"
}
```
#### GET /api/auth/me
Get current user information (requires authentication).
**Response (200):**
```json
{
"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):**
```json
{
"authenticated": true,
"role": "admin"
}
```
Or if not authenticated:
```json
{
"authenticated": false
}
```
## Testing
### Manual Testing
1. **Create test users** (via database or future user management UI):
```sql
INSERT INTO users (username, password_hash, role, is_active)
VALUES ('testuser', '<bcrypt_hash>', 'user', 1);
```
2. **Test login**:
```bash
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**:
```bash
curl http://localhost:8000/api/auth/me -b cookies.txt
```
4. **Test protected route**:
```bash
# 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):
```python
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
- User model: `models/models.py`
- Auth routes: `server/routes/auth.py`
- Permissions: `server/permissions.py`
- API client: `dashboard/src/apiAuth.ts`
- Auth context: `dashboard/src/useAuth.tsx`
- Flask sessions: https://flask.palletsprojects.com/en/latest/api/#sessions
- Bcrypt: https://pypi.org/project/bcrypt/