Refactor to manager-centric model, add team calendar for all users

Key changes:
- Removed office-centric model (deleted offices.py, office-rules)
- Renamed to team-rules, managers are part of their own team
- Team calendar visible to all (read-only for employees)
- Admins can have a manager assigned
This commit is contained in:
Stefano Manfredi
2025-12-02 13:30:04 +00:00
parent 2ad8ba3424
commit 7168fa4b72
30 changed files with 1016 additions and 910 deletions

View File

@@ -3,55 +3,52 @@
## Prerequisites
- org-stack running on rocky@rocketscale.it
- Git repository on git.rocketscale.it
- Git repository on git.rocketscale.it (optional, can use rsync)
## Step 1: Push to Git
## Directory Structure
```bash
# On development machine
cd /mnt/code/boilerplate/org-parking
git init
git add .
git commit -m "Initial commit: Parking Manager"
git remote add origin git@git.rocketscale.it:rocky/parking-manager.git
git push -u origin main
Parking is deployed as a **separate directory** alongside org-stack:
```
~/
├── org-stack/ # Main stack (Caddy, Authelia, LLDAP, etc.)
│ ├── compose.yml
│ ├── Caddyfile
│ ├── authelia/
│ └── .env
└── org-parking/ # Parking app (separate)
├── compose.yml # Production compose (connects to org-stack network)
├── .env # Own .env with PARKING_SECRET_KEY
├── Dockerfile
└── ...
```
## Step 2: Clone on Server
## Step 1: Deploy to Server
Option A - Using rsync (recommended for development):
```bash
# From development machine
rsync -avz --exclude '.git' --exclude '__pycache__' --exclude '*.pyc' \
--exclude '.env' --exclude 'data/' --exclude '*.db' --exclude '.venv/' \
/path/to/org-parking/ rocky@rocketscale.it:~/org-parking/
```
Option B - Using git:
```bash
# SSH to server
ssh rocky@rocketscale.it
# Clone into org-stack
cd ~/org-stack
git clone git@git.rocketscale.it:rocky/parking-manager.git parking
cd ~
git clone git@git.rocketscale.it:rocky/parking-manager.git org-parking
```
## Step 3: Add to .env
## Step 2: Create Production compose.yml
Add to `~/org-stack/.env`:
```bash
# Parking Manager
PARKING_SECRET_KEY=your-random-secret-key-here
```
Generate a secret key:
```bash
python3 -c "import secrets; print(secrets.token_hex(32))"
```
## Step 4: Add to compose.yml
Add the parking service to `~/org-stack/compose.yml`:
Create `~/org-parking/compose.yml` on the server:
```yaml
# ===========================================================================
# Parking Manager - Parking Spot Management
# ===========================================================================
services:
parking:
build: ./parking
build: .
container_name: parking
restart: unless-stopped
volumes:
@@ -68,18 +65,34 @@ Add the parking service to `~/org-stack/compose.yml`:
- SMTP_FROM=${SMTP_FROM:-}
networks:
- org-network
depends_on:
- authelia
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
parking_data:
networks:
org-network:
external: true
name: org-stack_org-network
```
Add to volumes section:
```yaml
parking_data: # Parking SQLite database
## Step 3: Create .env File
Create `~/org-parking/.env` with a secret key:
```bash
cd ~/org-parking
python3 -c "import secrets; print(f'PARKING_SECRET_KEY={secrets.token_hex(32)}')" > .env
```
Add `parking` to Caddy's depends_on list.
**Note**: Each directory needs its own `.env` file since docker compose only reads from the current directory.
## Step 5: Add to Caddyfile
## Step 4: Add to Caddyfile
Add to `~/org-stack/Caddyfile`:
@@ -91,27 +104,57 @@ parking.rocketscale.it {
}
```
## Step 6: Create LLDAP Groups
## Step 5: Add Authelia Access Control Rule
**Important**: Authelia's `access_control` must include parking.rocketscale.it or you'll get 403 Forbidden.
Edit `~/org-stack/authelia/configuration.yml` and add to the `access_control.rules` section:
```yaml
access_control:
default_policy: deny
rules:
# ... existing rules ...
# Parking Manager - require authentication
- domain: parking.rocketscale.it
policy: one_factor
```
After editing, restart Authelia:
```bash
cd ~/org-stack
docker compose restart authelia
```
## Step 6: Create LLDAP Group
In lldap (https://ldap.rocketscale.it):
1. Create group: `parking_admins` (follows lldap naming convention)
2. Create group: `managers` (reusable across apps)
3. Add yourself to `parking_admins`
1. Create group: `parking_admins`
2. Add yourself (or whoever should be admin) to this group
## Step 7: Deploy
**Role Management:**
- `parking_admins` group → **admin** role (synced from LLDAP on each login)
- **manager** role → assigned by admin in the app UI (Manage Users page)
- **employee** role → default for all other users
The admin can promote users to manager and assign offices via the Manage Users page.
## Step 7: Build and Deploy
```bash
cd ~/org-stack
# Build and start parking service
cd ~/org-parking
docker compose build parking
docker compose up -d parking
docker compose up -d
# Reload Caddy to pick up new domain
# Reload Caddy to pick up new domain (from org-stack)
cd ~/org-stack
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# Check logs
cd ~/org-parking
docker compose logs -f parking
```
@@ -124,14 +167,53 @@ docker compose logs -f parking
## Troubleshooting
### 403 Forbidden from Authelia
- Authelia's `access_control` doesn't have a rule for parking.rocketscale.it
- Add the domain to `~/org-stack/authelia/configuration.yml` (see Step 6)
- Restart Authelia: `docker compose restart authelia`
### 401 Unauthorized
- Check Authelia headers are being passed
- Check `docker compose logs authelia`
### Login redirect loop (keeps redirecting to /login)
- Frontend JS must use async auth checking for Authelia mode
- The `api.requireAuth()` must call `/api/auth/me` endpoint (not check localStorage)
- Ensure all page JS files use: `currentUser = await api.requireAuth();`
### User has wrong role
- Verify LLDAP group membership
- Roles sync on each login
- **Admin role**: Verify user is in `parking_admins` LLDAP group (synced on each login)
- **Manager role**: Must be assigned by admin via Manage Users page (not from LLDAP)
- **Employee role**: Default for users not in `parking_admins` group
### Database errors
- Check volume mount: `docker compose exec parking ls -la /app/data`
- Check permissions: `docker compose exec parking id`
## Architecture Notes
### Authelia Integration
The app supports two authentication modes:
1. **JWT mode** (standalone): Users login via `/login`, get JWT token stored in localStorage
2. **Authelia mode** (SSO): Authelia handles login, passes headers to backend
When `AUTHELIA_ENABLED=true`:
- Backend reads user info from headers: `Remote-User`, `Remote-Email`, `Remote-Name`, `Remote-Groups`
- Users are auto-created on first login
- Roles are synced from LLDAP groups on each request
- Frontend calls `/api/auth/me` to get user info (backend reads headers)
### Role Management
Only the **admin** role is synced from LLDAP:
```python
AUTHELIA_ADMIN_GROUP = "parking_admins" # → role: admin
```
Other roles are managed within the app:
- **manager**: Assigned by admin via Manage Users page
- **employee**: Default role for all non-admin users
This separation allows:
- LLDAP to control who has admin access
- App admin to assign manager roles and office assignments without LLDAP changes