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.
523 lines
13 KiB
Markdown
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/
|