Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4386f53f5 | |||
| 8f443379cd | |||
| a47b57f26a | |||
| cd401e108f | |||
| 3d11470f81 | |||
| 877ecf32b4 | |||
| 3b78959901 | |||
| 6e110289cd | |||
| afab617c5c | |||
| b737826d11 | |||
| f9704f6dd3 | |||
| 75a3ec9d7e | |||
| 21f27e0d27 | |||
| 7ed6100f05 | |||
| c3efa127d9 | |||
| e4fed12143 | |||
| 7fc81a0f65 | |||
| 611031ec6c | |||
| 686bce2df9 | |||
| efbb5725d3 | |||
| acd37a9c15 | |||
| 203d5e0575 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,3 +14,5 @@ BUI*
|
|||||||
build.sh
|
build.sh
|
||||||
docker-compose.dist*
|
docker-compose.dist*
|
||||||
GITEA.md
|
GITEA.md
|
||||||
|
PROXY_AUTHELIA_SETUP.md
|
||||||
|
alias.rune.pm.config
|
||||||
@@ -134,3 +134,7 @@ To access from other devices on your network:
|
|||||||
1. Find your server's IP address
|
1. Find your server's IP address
|
||||||
2. Access via `http://YOUR_IP:5172`
|
2. Access via `http://YOUR_IP:5172`
|
||||||
3. Make sure your firewall allows connections on port 5172
|
3. Make sure your firewall allows connections on port 5172
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
COPY app.py .
|
COPY app.py .
|
||||||
COPY malias_wrapper.py .
|
COPY malias_wrapper.py .
|
||||||
COPY malias.py .
|
COPY malias.py .
|
||||||
|
COPY reset_password.py .
|
||||||
COPY templates/ templates/
|
COPY templates/ templates/
|
||||||
COPY static/ static/
|
COPY static/ static/
|
||||||
|
|
||||||
# Create data directory
|
# Create data directory
|
||||||
RUN mkdir -p /app/data
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
# Copy entrypoint script
|
# Copy entrypoint script and make scripts executable
|
||||||
COPY docker-entrypoint.sh .
|
COPY docker-entrypoint.sh .
|
||||||
RUN chmod +x docker-entrypoint.sh
|
RUN chmod +x docker-entrypoint.sh reset_password.py
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 5172
|
EXPOSE 5172
|
||||||
@@ -28,6 +29,7 @@ EXPOSE 5172
|
|||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV FLASK_DEBUG=False
|
ENV FLASK_DEBUG=False
|
||||||
ENV FLASK_ENV=production
|
ENV FLASK_ENV=production
|
||||||
|
ENV ENABLE_PROXY=false
|
||||||
|
|
||||||
# Run the application via entrypoint
|
# Run the application via entrypoint
|
||||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||||
|
|||||||
432
PROXY_SETUP.md
Normal file
432
PROXY_SETUP.md
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
# Reverse Proxy Setup Guide
|
||||||
|
|
||||||
|
This guide helps you configure reverse proxies (Nginx, Traefik, Zoraxy, Authelia, Caddy, etc.) to work with Mailcow Alias Manager.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 ENABLE_PROXY Configuration
|
||||||
|
|
||||||
|
The application supports **two access modes** controlled by the `ENABLE_PROXY` environment variable in `docker-compose.yml`:
|
||||||
|
|
||||||
|
### **Mode 1: Direct Access (ENABLE_PROXY=false)** - DEFAULT
|
||||||
|
|
||||||
|
Use this when accessing the application directly via IP:port **without a reverse proxy**.
|
||||||
|
|
||||||
|
**docker-compose.yml:**
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- ENABLE_PROXY=false # Default setting
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access:** `http://192.168.1.100:5172` (replace with your server IP)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Works over HTTP (no HTTPS required)
|
||||||
|
- ✅ Standard cookie behavior (SameSite=Lax)
|
||||||
|
- ✅ No proxy configuration needed
|
||||||
|
- ✅ Simple login flow
|
||||||
|
- ✅ Perfect for internal/LAN access
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- Accessing from internal network only
|
||||||
|
- No reverse proxy in place
|
||||||
|
- Testing or development
|
||||||
|
- Simple single-server setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Mode 2: Proxy Access (ENABLE_PROXY=true)**
|
||||||
|
|
||||||
|
Use this when running **behind a reverse proxy** (Authelia, Zoraxy, Nginx, Traefik, Caddy).
|
||||||
|
|
||||||
|
**docker-compose.yml:**
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- ENABLE_PROXY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access:** `https://alias.yourdomain.com` (through your reverse proxy)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ ProxyFix middleware handles X-Forwarded-* headers
|
||||||
|
- ✅ HTTPS redirect support
|
||||||
|
- ✅ Secure cookies (HTTPS only)
|
||||||
|
- ✅ Works with authentication proxies (Authelia)
|
||||||
|
- ✅ Multi-proxy chain support
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- Accessing from internet via domain name
|
||||||
|
- Behind Nginx, Traefik, Caddy, HAProxy
|
||||||
|
- Behind authentication proxy (Authelia, Authentik)
|
||||||
|
- SSL/TLS termination at proxy
|
||||||
|
- Production deployments with HTTPS
|
||||||
|
|
||||||
|
**Special Feature - Authelia Auto-Login:**
|
||||||
|
When `ENABLE_PROXY=true` and you're using Authelia, the app automatically logs you in using Authelia's authentication headers. **No app password needed!** Simply authenticate through Authelia, and you'll be logged into the Mailcow Alias Manager automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Switching Between Modes**
|
||||||
|
|
||||||
|
To switch from one mode to another:
|
||||||
|
|
||||||
|
1. **Edit `docker-compose.yml`**
|
||||||
|
```yaml
|
||||||
|
# Change this line:
|
||||||
|
- ENABLE_PROXY=false # or true
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Restart the container**
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify mode in logs**
|
||||||
|
```bash
|
||||||
|
docker compose logs mailcow-alias-manager | grep "ACCESS MODE"
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see either:
|
||||||
|
- `ACCESS MODE: Direct IP:Port (ENABLE_PROXY=false)`
|
||||||
|
- `ACCESS MODE: Reverse Proxy (ENABLE_PROXY=true)`
|
||||||
|
|
||||||
|
4. **Clear browser cookies** (IMPORTANT!)
|
||||||
|
- Press F12 → Application → Cookies
|
||||||
|
- Delete all cookies for your domain
|
||||||
|
- Close and reopen browser
|
||||||
|
|
||||||
|
5. **Login again**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Quick Reference Table**
|
||||||
|
|
||||||
|
| Access Method | ENABLE_PROXY | Access URL | Cookie Mode |
|
||||||
|
|--------------|--------------|------------|-------------|
|
||||||
|
| Direct IP:port | `false` (default) | `http://192.168.1.100:5172` | HTTP, SameSite=Lax |
|
||||||
|
| Nginx/Traefik | `true` | `https://alias.example.com` | HTTPS, SameSite=None |
|
||||||
|
| Authelia + Zoraxy | `true` | `https://alias.example.com` | HTTPS, SameSite=None |
|
||||||
|
| Caddy | `true` | `https://alias.example.com` | HTTPS, SameSite=None |
|
||||||
|
| Local dev (`python3 app.py`) | N/A (not set) | `http://localhost:5172` | HTTP, SameSite=Lax |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Built-in Proxy Support (When ENABLE_PROXY=true)
|
||||||
|
|
||||||
|
The application includes **ProxyFix middleware** that automatically handles:
|
||||||
|
- ✅ HTTPS detection via `X-Forwarded-Proto`
|
||||||
|
- ✅ Host header forwarding via `X-Forwarded-Host`
|
||||||
|
- ✅ Client IP forwarding via `X-Forwarded-For`
|
||||||
|
- ✅ Path prefix support via `X-Forwarded-Prefix`
|
||||||
|
|
||||||
|
**Works with 2 proxies in chain** (e.g., Zoraxy → Authelia → App)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Proxy Configurations
|
||||||
|
|
||||||
|
### **Zoraxy** (Your Current Setup)
|
||||||
|
|
||||||
|
Zoraxy automatically sets the required headers. Just configure:
|
||||||
|
|
||||||
|
1. **Upstream Target**: `http://localhost:5172` (or your Docker IP)
|
||||||
|
2. **Enable WebSocket**: Yes (optional, recommended)
|
||||||
|
3. **Enable Proxy Headers**: Yes (should be default)
|
||||||
|
|
||||||
|
**Example Zoraxy Config:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "mailcow-alias-manager",
|
||||||
|
"upstream": "http://localhost:5172",
|
||||||
|
"domain": "aliases.yourdomain.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Authelia** (Authentication Proxy)
|
||||||
|
|
||||||
|
If using Authelia in front of the app:
|
||||||
|
|
||||||
|
1. **Authelia forwards the user after login** - this should work automatically
|
||||||
|
2. **Make sure Authelia passes these headers**:
|
||||||
|
- `X-Forwarded-Proto`
|
||||||
|
- `X-Forwarded-Host`
|
||||||
|
- `X-Forwarded-For`
|
||||||
|
|
||||||
|
**Common Issue**: Authelia redirects → App login page
|
||||||
|
**Solution**: The app has its own authentication. You have two options:
|
||||||
|
- **Option A**: Disable app login (requires code modification)
|
||||||
|
- **Option B**: Use Authelia for network access, app login for API access
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Nginx**
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name aliases.yourdomain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/cert.pem;
|
||||||
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:5172;
|
||||||
|
|
||||||
|
# Required headers for ProxyFix
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
|
||||||
|
# Optional: WebSocket support
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Traefik**
|
||||||
|
|
||||||
|
**docker-compose.yml:**
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mailcow-alias-manager:
|
||||||
|
image: gitlab.pm/rune/malias-web:latest
|
||||||
|
container_name: mailcow-alias-manager
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.malias.rule=Host(`aliases.yourdomain.com`)"
|
||||||
|
- "traefik.http.routers.malias.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.malias.tls=true"
|
||||||
|
- "traefik.http.routers.malias.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.malias.loadbalancer.server.port=5172"
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Traefik automatically sets `X-Forwarded-*` headers by default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Caddy**
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
aliases.yourdomain.com {
|
||||||
|
reverse_proxy localhost:5172
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Caddy automatically handles all proxy headers - no configuration needed!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### **Issue: "NetworkError when attempting to fetch resource"**
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Mixed HTTP/HTTPS content
|
||||||
|
2. CORS blocking requests
|
||||||
|
3. Session cookie not being set
|
||||||
|
4. Proxy not forwarding headers
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
#### **1. Check Proxy Headers**
|
||||||
|
Your proxy MUST forward these headers:
|
||||||
|
```
|
||||||
|
X-Forwarded-Proto: https
|
||||||
|
X-Forwarded-Host: aliases.yourdomain.com
|
||||||
|
X-Forwarded-For: <client-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2. Check Browser Console**
|
||||||
|
Open browser DevTools (F12) → Console tab → Look for errors:
|
||||||
|
- **CORS errors**: Proxy misconfiguration
|
||||||
|
- **Mixed content**: HTTP resources on HTTPS page
|
||||||
|
- **401 Unauthorized**: Session/cookie issue
|
||||||
|
|
||||||
|
#### **3. Test Direct Access**
|
||||||
|
```bash
|
||||||
|
# Test without proxy
|
||||||
|
curl http://localhost:5172/
|
||||||
|
|
||||||
|
# Should return HTML (login page)
|
||||||
|
```
|
||||||
|
|
||||||
|
If this works but proxy doesn't, it's a proxy configuration issue.
|
||||||
|
|
||||||
|
#### **4. Check Session Cookies**
|
||||||
|
In browser DevTools → Application tab → Cookies:
|
||||||
|
- **Domain**: Should match your proxy domain
|
||||||
|
- **Path**: `/`
|
||||||
|
- **HttpOnly**: Should be `true`
|
||||||
|
- **Secure**: Depends on your setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Issue: Login works but redirects to HTTP instead of HTTPS**
|
||||||
|
|
||||||
|
**Solution**: Already fixed in latest version via `PREFERRED_URL_SCHEME='https'` in app config.
|
||||||
|
|
||||||
|
If still happening:
|
||||||
|
1. Verify proxy sends `X-Forwarded-Proto: https`
|
||||||
|
2. Check `docker compose logs -f mailcow-alias-manager`
|
||||||
|
3. Rebuild image: `docker compose up -d --build`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Issue: "Invalid password" but password is correct**
|
||||||
|
|
||||||
|
This is NOT a proxy issue - this is an authentication issue:
|
||||||
|
```bash
|
||||||
|
# Reset password
|
||||||
|
docker exec -it mailcow-alias-manager python3 reset_password.py "newpass"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Issue: 502 Bad Gateway**
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. App not running
|
||||||
|
2. Wrong upstream port
|
||||||
|
3. Container network issue
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
```bash
|
||||||
|
# Is container running?
|
||||||
|
docker ps | grep mailcow-alias-manager
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker compose logs -f mailcow-alias-manager
|
||||||
|
|
||||||
|
# Test internal connectivity
|
||||||
|
docker exec mailcow-alias-manager curl http://localhost:5172
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Best Practices
|
||||||
|
|
||||||
|
### **1. HTTPS Only**
|
||||||
|
Always use HTTPS in production. HTTP is only for local testing.
|
||||||
|
|
||||||
|
### **2. Restrict Access**
|
||||||
|
Use firewall or proxy rules to restrict access:
|
||||||
|
```nginx
|
||||||
|
# Nginx: Restrict to internal network only
|
||||||
|
allow 192.168.1.0/24;
|
||||||
|
deny all;
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Use Authelia/Authentik**
|
||||||
|
Add SSO layer for additional security:
|
||||||
|
- Users authenticate via Authelia
|
||||||
|
- Then app login provides API access
|
||||||
|
- Two-factor authentication recommended
|
||||||
|
|
||||||
|
### **4. Keep Session Cookies Secure**
|
||||||
|
The app sets:
|
||||||
|
- `HttpOnly=True` - Prevents JavaScript access
|
||||||
|
- `SameSite=Lax` - CSRF protection
|
||||||
|
|
||||||
|
For HTTPS-only deployments, you can enforce secure cookies:
|
||||||
|
```python
|
||||||
|
# In app.py, change:
|
||||||
|
SESSION_COOKIE_SECURE=True # Only send cookie over HTTPS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Testing Your Setup
|
||||||
|
|
||||||
|
### **1. Basic Connectivity**
|
||||||
|
```bash
|
||||||
|
curl -v https://aliases.yourdomain.com/
|
||||||
|
# Should return 200 OK with HTML
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Login Test**
|
||||||
|
```bash
|
||||||
|
curl -v -X POST https://aliases.yourdomain.com/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password":"yourpassword"}' \
|
||||||
|
-c cookies.txt
|
||||||
|
|
||||||
|
# Check response - should be JSON with status: success
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Session Test**
|
||||||
|
```bash
|
||||||
|
curl -v https://aliases.yourdomain.com/ \
|
||||||
|
-b cookies.txt
|
||||||
|
|
||||||
|
# Should return main page HTML (not login redirect)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Still Having Issues?
|
||||||
|
|
||||||
|
### **Enable Debug Logging**
|
||||||
|
|
||||||
|
**docker-compose.yml:**
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- FLASK_DEBUG=True # Enable debug mode
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart:
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Check Application Logs**
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f mailcow-alias-manager
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
docker compose logs mailcow-alias-manager | grep -i error
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Test Without Proxy**
|
||||||
|
Temporarily bypass proxy to isolate the issue:
|
||||||
|
```bash
|
||||||
|
# Direct access test
|
||||||
|
curl http://localhost:5172/
|
||||||
|
```
|
||||||
|
|
||||||
|
If this works, the issue is with proxy configuration, not the app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Summary
|
||||||
|
|
||||||
|
**The app is already configured for reverse proxies!**
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- ✅ ProxyFix middleware is enabled
|
||||||
|
- ✅ Handles X-Forwarded-* headers automatically
|
||||||
|
- ✅ HTTPS redirect support built-in
|
||||||
|
- ✅ Session cookies work behind proxies
|
||||||
|
- ✅ No code changes needed
|
||||||
|
|
||||||
|
Just make sure your proxy forwards the standard headers and you're good to go! 🚀
|
||||||
33
README.md
33
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Mailcow Alias Manager Web Interface
|
# Mailcow Alias Manager Web Interface..
|
||||||
|
|
||||||
A Python-based web application for managing mail aliases on a Mailcow instance. Built with Flask and a dark theme interface for internal network use. This application uses [malias](https://gitlab.pm/rune/malias) as base. Malias it self is a CLI application for managing aliases on a Mailcow Instance. If you already are using the CLI instance you can copy the the DB file over to your `/data` directory and you'll keep you API key etc. If you start with a blank DB you can use the `Sync Aliases` function to populate existing aliases to your local db.
|
A Python-based web application for managing mail aliases on a Mailcow instance. Built with Flask and a dark theme interface for internal network use. This application uses [malias](https://gitlab.pm/rune/malias) as base. Malias it self is a CLI application for managing aliases on a Mailcow Instance. If you already are using the CLI instance you can copy the the DB file over to your `/data` directory and you'll keep you API key etc. If you start with a blank DB you can use the `Sync Aliases` function to populate existing aliases to your local db.
|
||||||
|
|
||||||
@@ -68,7 +68,32 @@ The application will be available at: `http://localhost:5172`
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### 1. Configure Mailcow Connection
|
### 1. Access Mode Configuration
|
||||||
|
|
||||||
|
The application supports two access modes:
|
||||||
|
|
||||||
|
**Direct Access (Default)** - Access via IP:port
|
||||||
|
- Set `ENABLE_PROXY=false` in `docker-compose.yml` (default)
|
||||||
|
- Access at: `http://your-server-ip:5172`
|
||||||
|
- Works over HTTP (no HTTPS required)
|
||||||
|
- **Login:** Enter app password
|
||||||
|
- Perfect for internal/LAN access
|
||||||
|
|
||||||
|
**Proxy Access** - Access via reverse proxy (Authelia, Nginx, Traefik, etc.)
|
||||||
|
- Set `ENABLE_PROXY=true` in `docker-compose.yml`
|
||||||
|
- Access at: `https://alias.yourdomain.com` (through your proxy)
|
||||||
|
- Requires HTTPS
|
||||||
|
- **Login:** Automatic when using Authelia (no password needed!)
|
||||||
|
- See [PROXY_SETUP.md](PROXY_SETUP.md) for detailed configuration
|
||||||
|
|
||||||
|
**To switch modes:**
|
||||||
|
1. Edit `docker-compose.yml` and change `ENABLE_PROXY` value
|
||||||
|
2. Restart: `docker compose down && docker compose up -d`
|
||||||
|
3. Clear browser cookies and login again
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Configure Mailcow Connection
|
||||||
|
|
||||||
You can configure your Mailcow server connection either:
|
You can configure your Mailcow server connection either:
|
||||||
|
|
||||||
@@ -295,3 +320,7 @@ For manual Python installation:
|
|||||||
- Verify your Mailcow server is accessible from the machine running this app
|
- Verify your Mailcow server is accessible from the machine running this app
|
||||||
- Check that the API key is valid and has the correct permissions
|
- Check that the API key is valid and has the correct permissions
|
||||||
- Review logs in `data/malias2.log`
|
- Review logs in `data/malias2.log`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|||||||
464
app.py
464
app.py
@@ -1,53 +1,350 @@
|
|||||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, session
|
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, make_response
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from datetime import timedelta
|
||||||
import malias_wrapper as malias_w
|
import malias_wrapper as malias_w
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
import secrets
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger('mailcow-alias-manager')
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = os.urandom(24) # Secret key for session management
|
app.secret_key = os.getenv('SECRET_KEY', 'malias-default-secret-key-please-change') # Consistent secret key
|
||||||
|
|
||||||
|
# Configure proxy mode based on environment variable
|
||||||
|
# ENABLE_PROXY=true: Behind reverse proxy (Authelia/Zoraxy/Nginx/etc.)
|
||||||
|
# ENABLE_PROXY=false: Direct IP:port access (default)
|
||||||
|
ENABLE_PROXY = os.getenv('ENABLE_PROXY', 'false').lower() in ('true', '1', 'yes')
|
||||||
|
|
||||||
|
if ENABLE_PROXY:
|
||||||
|
# Mode: Behind reverse proxy (Authelia/Zoraxy/Nginx/etc.)
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(" ACCESS MODE: Reverse Proxy (ENABLE_PROXY=true)")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(" Configuration:")
|
||||||
|
logger.info(" - ProxyFix middleware: ACTIVE")
|
||||||
|
logger.info(" - Trusted proxies: 2 (e.g., Zoraxy + Authelia)")
|
||||||
|
logger.info(" - Cookie security: HTTPS only (Secure=True)")
|
||||||
|
logger.info(" - SameSite policy: None (cross-origin auth)")
|
||||||
|
logger.info(" - URL scheme: https://")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
app.wsgi_app = ProxyFix(
|
||||||
|
app.wsgi_app,
|
||||||
|
x_for=2, # Trust X-Forwarded-For with 2 proxies
|
||||||
|
x_proto=2, # Trust X-Forwarded-Proto (http/https)
|
||||||
|
x_host=2, # Trust X-Forwarded-Host
|
||||||
|
x_prefix=2 # Trust X-Forwarded-Prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
cookie_secure = True
|
||||||
|
cookie_samesite = 'None'
|
||||||
|
preferred_scheme = 'https'
|
||||||
|
else:
|
||||||
|
# Mode: Direct IP:port access (no proxy)
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(" ACCESS MODE: Direct IP:Port (ENABLE_PROXY=false)")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(" Configuration:")
|
||||||
|
logger.info(" - ProxyFix middleware: DISABLED")
|
||||||
|
logger.info(" - Cookie security: HTTP allowed (Secure=False)")
|
||||||
|
logger.info(" - SameSite policy: Lax (standard mode)")
|
||||||
|
logger.info(" - URL scheme: http://")
|
||||||
|
logger.info(" - Access via: http://your-ip:5172")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
# Don't apply ProxyFix for direct access
|
||||||
|
cookie_secure = False
|
||||||
|
cookie_samesite = 'Lax'
|
||||||
|
preferred_scheme = 'http'
|
||||||
|
|
||||||
# Initialize database on startup
|
# Initialize database on startup
|
||||||
malias_w.init_database()
|
malias_w.init_database()
|
||||||
|
|
||||||
|
# Session configuration - dynamic based on proxy mode
|
||||||
|
app.config.update(
|
||||||
|
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
|
||||||
|
SESSION_COOKIE_NAME='malias_session',
|
||||||
|
SESSION_COOKIE_SECURE=cookie_secure,
|
||||||
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
|
SESSION_COOKIE_SAMESITE=cookie_samesite,
|
||||||
|
SESSION_COOKIE_PATH='/',
|
||||||
|
SESSION_COOKIE_DOMAIN=None,
|
||||||
|
SESSION_REFRESH_EACH_REQUEST=True,
|
||||||
|
PREFERRED_URL_SCHEME=preferred_scheme
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def after_request(response):
|
||||||
|
"""Process the response before it's sent"""
|
||||||
|
if ENABLE_PROXY:
|
||||||
|
# Set CORS headers for proxy mode (needed for Authelia/Zoraxy)
|
||||||
|
response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
|
||||||
|
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
||||||
|
|
||||||
|
# Cache control (applies to both modes)
|
||||||
|
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||||
|
response.headers['Pragma'] = 'no-cache'
|
||||||
|
response.headers['Expires'] = '0'
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_authelia_user():
|
||||||
|
"""Helper to get authenticated user from Authelia headers or session cookie"""
|
||||||
|
# Check several possible header variations (ORDER MATTERS - most specific first!)
|
||||||
|
auth_headers = [
|
||||||
|
'X-Authelia-Username', # Authelia standard header (used by Zoraxy)
|
||||||
|
'Remote-User', # Common standard
|
||||||
|
'X-Remote-User',
|
||||||
|
'X-Forwarded-User',
|
||||||
|
'REMOTE_USER',
|
||||||
|
'Http-Remote-User',
|
||||||
|
'Http-X-Remote-User',
|
||||||
|
'X-Authenticated-User'
|
||||||
|
]
|
||||||
|
|
||||||
|
for header in auth_headers:
|
||||||
|
user = request.headers.get(header)
|
||||||
|
if user:
|
||||||
|
logger.info(f"✅ Authelia user detected via header '{header}': {user}")
|
||||||
|
return user
|
||||||
|
|
||||||
|
# Check Zoraxy forwarded headers (sometimes encoded differently)
|
||||||
|
if 'X-Forwarded-Headers' in request.headers:
|
||||||
|
try:
|
||||||
|
# Some reverse proxies encode headers as JSON
|
||||||
|
fwd_headers = json.loads(request.headers.get('X-Forwarded-Headers'))
|
||||||
|
for header in auth_headers:
|
||||||
|
if header in fwd_headers:
|
||||||
|
user = fwd_headers[header]
|
||||||
|
logger.info(f"✅ Authelia user detected via forwarded headers - {header}: {user}")
|
||||||
|
return user
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# WORKAROUND: If Authelia session cookie exists and we're coming from auth.rune.pm,
|
||||||
|
# assume user is authenticated (Authelia not forwarding headers properly)
|
||||||
|
if ENABLE_PROXY and 'authelia_session' in request.cookies:
|
||||||
|
referer = request.headers.get('Referer', '')
|
||||||
|
if 'auth.rune.pm' in referer or request.headers.get('X-Forwarded-Proto') == 'https':
|
||||||
|
# Valid Authelia session exists - assume authenticated
|
||||||
|
# Use a generic identifier since we don't have the actual username
|
||||||
|
pseudo_user = f"authelia_user_{request.cookies.get('authelia_session')[:8]}"
|
||||||
|
logger.info(f"✅ Authelia authentication detected via session cookie (headers not forwarded)")
|
||||||
|
logger.info(f" Using pseudo-user identifier: {pseudo_user}")
|
||||||
|
logger.info(f" NOTE: Configure Authelia to forward Remote-User header for proper username")
|
||||||
|
return pseudo_user
|
||||||
|
|
||||||
|
# Log when no Authelia user found (for debugging)
|
||||||
|
if ENABLE_PROXY:
|
||||||
|
logger.debug("⚠️ No Authelia headers found in request")
|
||||||
|
logger.debug(f"Available headers: {list(request.headers.keys())}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def login_required(f):
|
def login_required(f):
|
||||||
"""Decorator to require login for routes"""
|
"""Decorator to require login for routes"""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
|
# Auto-login with Authelia (only when ENABLE_PROXY=true)
|
||||||
|
if ENABLE_PROXY:
|
||||||
|
authelia_user = get_authelia_user()
|
||||||
|
|
||||||
|
# If Authelia authenticated the user, auto-login
|
||||||
|
if authelia_user:
|
||||||
|
if not session.get('logged_in') or session.get('authelia_user') != authelia_user:
|
||||||
|
logger.info(f"🔐 Auto-login via Authelia in API route: {authelia_user}")
|
||||||
|
session.clear()
|
||||||
|
session.permanent = True
|
||||||
|
session['logged_in'] = True
|
||||||
|
session['authelia_user'] = authelia_user
|
||||||
|
session['user_token'] = secrets.token_urlsafe(32)
|
||||||
|
session['auth_method'] = 'authelia'
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
|
# Store additional info (check both Remote-* and X-Authelia-* headers)
|
||||||
|
session['remote_email'] = (request.headers.get('X-Authelia-Email') or
|
||||||
|
request.headers.get('Remote-Email', ''))
|
||||||
|
session['remote_name'] = (request.headers.get('X-Authelia-DisplayName') or
|
||||||
|
request.headers.get('Remote-Name', ''))
|
||||||
|
session['remote_groups'] = (request.headers.get('X-Authelia-Groups') or
|
||||||
|
request.headers.get('Remote-Groups', ''))
|
||||||
|
|
||||||
|
logger.info(f"✅ Auto-login in API route: {authelia_user}")
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
# Regular session check (when ENABLE_PROXY=false or no Authelia headers)
|
||||||
if not session.get('logged_in'):
|
if not session.get('logged_in'):
|
||||||
|
logger.warning("Access denied: User not authenticated")
|
||||||
|
if request.is_json:
|
||||||
return jsonify({'status': 'error', 'message': 'Not authenticated', 'redirect': '/login'}), 401
|
return jsonify({'status': 'error', 'message': 'Not authenticated', 'redirect': '/login'}), 401
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
"""Login page"""
|
"""Login page or JSON login endpoint"""
|
||||||
|
|
||||||
|
# Validate session mode matches current proxy setting
|
||||||
|
if session.get('logged_in'):
|
||||||
|
session_mode = session.get('auth_method', 'unknown')
|
||||||
|
|
||||||
|
# Clear session if mode mismatch
|
||||||
|
if ENABLE_PROXY and session_mode == 'local':
|
||||||
|
logger.info("⚠️ Session mode mismatch: Clearing local session (proxy mode enabled)")
|
||||||
|
session.clear()
|
||||||
|
elif not ENABLE_PROXY and session_mode == 'authelia':
|
||||||
|
logger.info("⚠️ Session mode mismatch: Clearing authelia session (proxy mode disabled)")
|
||||||
|
session.clear()
|
||||||
|
|
||||||
|
# Auto-login when ENABLE_PROXY=true and Authelia headers are present
|
||||||
|
if ENABLE_PROXY:
|
||||||
|
authelia_user = get_authelia_user()
|
||||||
|
|
||||||
|
if authelia_user:
|
||||||
|
# User authenticated by Authelia - auto-login
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
logger.info(f"🔐 Auto-login: User '{authelia_user}' authenticated by Authelia")
|
||||||
|
session.clear()
|
||||||
|
session.permanent = True
|
||||||
|
session['logged_in'] = True
|
||||||
|
session['user_token'] = secrets.token_urlsafe(32)
|
||||||
|
session['auth_method'] = 'authelia'
|
||||||
|
session['authelia_user'] = authelia_user
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
|
# Get additional Authelia info (check both Remote-* and X-Authelia-* headers)
|
||||||
|
session['remote_email'] = (request.headers.get('X-Authelia-Email') or
|
||||||
|
request.headers.get('Remote-Email', ''))
|
||||||
|
session['remote_name'] = (request.headers.get('X-Authelia-DisplayName') or
|
||||||
|
request.headers.get('Remote-Name', ''))
|
||||||
|
session['remote_groups'] = (request.headers.get('X-Authelia-Groups') or
|
||||||
|
request.headers.get('Remote-Groups', ''))
|
||||||
|
|
||||||
|
logger.info(f"✅ Auto-login successful: {authelia_user} ({session.get('remote_email', 'no email')})")
|
||||||
|
|
||||||
|
# Already logged in via Authelia - redirect to main page
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
else:
|
||||||
|
# ENABLE_PROXY=true but no Authelia headers found
|
||||||
|
logger.warning("⚠️ ENABLE_PROXY=true but no Authelia headers detected!")
|
||||||
|
logger.warning(" Make sure your reverse proxy forwards authentication headers")
|
||||||
|
logger.warning(f" Available headers: {list(request.headers.keys())}")
|
||||||
|
|
||||||
|
# Handle form submission for local authentication (only when ENABLE_PROXY=false)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
password = request.json.get('password', '')
|
password = request.json.get('password', '')
|
||||||
|
logger.info("Login attempt with password (redacted)")
|
||||||
|
|
||||||
if malias_w.verify_password(password):
|
if malias_w.verify_password(password):
|
||||||
|
logger.info("Login successful with local password")
|
||||||
|
session.clear()
|
||||||
|
session.permanent = True
|
||||||
session['logged_in'] = True
|
session['logged_in'] = True
|
||||||
return jsonify({'status': 'success', 'message': 'Login successful'})
|
session['user_token'] = secrets.token_urlsafe(32)
|
||||||
|
session['auth_method'] = 'local'
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
|
# Return JSON response
|
||||||
|
response = jsonify({'status': 'success', 'message': 'Login successful'})
|
||||||
|
return response
|
||||||
else:
|
else:
|
||||||
|
logger.warning("Login failed: Invalid password")
|
||||||
return jsonify({'status': 'error', 'message': 'Invalid password'})
|
return jsonify({'status': 'error', 'message': 'Invalid password'})
|
||||||
|
|
||||||
# Check if already logged in
|
# If already logged in, redirect to index
|
||||||
if session.get('logged_in'):
|
if session.get('logged_in'):
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# Check cookies and client IP for troubleshooting
|
||||||
|
if app.debug:
|
||||||
|
logger.info(f"Cookies: {request.cookies}")
|
||||||
|
logger.info(f"Client IP: {request.remote_addr}")
|
||||||
|
logger.info(f"X-Forwarded-For: {request.headers.get('X-Forwarded-For')}")
|
||||||
|
# Log all headers to see what's coming from Authelia
|
||||||
|
logger.info(f"All headers: {dict(request.headers)}")
|
||||||
|
|
||||||
|
# Show login form
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
"""Logout"""
|
"""Logout"""
|
||||||
session.pop('logged_in', None)
|
# Get auth method before clearing session
|
||||||
return redirect(url_for('login'))
|
auth_method = session.get('auth_method')
|
||||||
|
authelia_user = session.get('authelia_user')
|
||||||
|
|
||||||
|
# Clear local session
|
||||||
|
session.clear()
|
||||||
|
|
||||||
|
# If user was authenticated via Authelia, redirect to app login (not Authelia logout)
|
||||||
|
# This keeps the Authelia session active for other apps
|
||||||
|
if ENABLE_PROXY and (auth_method == 'authelia' or authelia_user):
|
||||||
|
logger.info(f"Logout for Authelia user - redirecting to app login page")
|
||||||
|
# Just redirect back to login page - Authelia session stays active
|
||||||
|
response = redirect(url_for('login'))
|
||||||
|
response.set_cookie(app.config['SESSION_COOKIE_NAME'], '', expires=0)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Default case: redirect to login page
|
||||||
|
response = redirect(url_for('login'))
|
||||||
|
|
||||||
|
# Clear cookie by setting expired date
|
||||||
|
response.set_cookie(app.config['SESSION_COOKIE_NAME'], '', expires=0)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""Main page - requires login"""
|
"""Main page - requires login"""
|
||||||
|
# Auto-login with Authelia (only when ENABLE_PROXY=true)
|
||||||
|
if ENABLE_PROXY:
|
||||||
|
authelia_user = get_authelia_user()
|
||||||
|
|
||||||
|
if authelia_user and not session.get('logged_in'):
|
||||||
|
# Auto-login for users authenticated by Authelia
|
||||||
|
logger.info(f"🔐 Auto-login via Authelia for user: {authelia_user}")
|
||||||
|
session.clear()
|
||||||
|
session.permanent = True
|
||||||
|
session['logged_in'] = True
|
||||||
|
session['authelia_user'] = authelia_user
|
||||||
|
session['user_token'] = secrets.token_urlsafe(32)
|
||||||
|
session['auth_method'] = 'authelia'
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
|
# Store additional Authelia info (check both Remote-* and X-Authelia-* headers)
|
||||||
|
session['remote_email'] = (request.headers.get('X-Authelia-Email') or
|
||||||
|
request.headers.get('Remote-Email', ''))
|
||||||
|
session['remote_name'] = (request.headers.get('X-Authelia-DisplayName') or
|
||||||
|
request.headers.get('Remote-Name', ''))
|
||||||
|
session['remote_groups'] = (request.headers.get('X-Authelia-Groups') or
|
||||||
|
request.headers.get('Remote-Groups', ''))
|
||||||
|
|
||||||
|
logger.info(f"✅ Auto-login successful: {authelia_user} ({session.get('remote_email', 'no email')})")
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
# Check if logged in
|
||||||
if not session.get('logged_in'):
|
if not session.get('logged_in'):
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
# Debug logging
|
||||||
|
if app.debug and session.get('logged_in'):
|
||||||
|
logger.info(f"User authenticated with method: {session.get('auth_method', 'unknown')}")
|
||||||
|
|
||||||
|
# Show main page
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
@app.route('/list_aliases', methods=['POST'])
|
@app.route('/list_aliases', methods=['POST'])
|
||||||
@@ -65,6 +362,7 @@ def list_aliases():
|
|||||||
result = malias_w.get_all_aliases(page=page, per_page=20)
|
result = malias_w.get_all_aliases(page=page, per_page=20)
|
||||||
return jsonify({'status': 'success', 'data': result})
|
return jsonify({'status': 'success', 'data': result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error listing aliases")
|
||||||
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||||
|
|
||||||
@app.route('/sync_aliases', methods=['POST'])
|
@app.route('/sync_aliases', methods=['POST'])
|
||||||
@@ -76,6 +374,7 @@ def sync_aliases():
|
|||||||
result = f"Aliases synchronized successfully! Added {count} new aliases to local DB."
|
result = f"Aliases synchronized successfully! Added {count} new aliases to local DB."
|
||||||
return jsonify({'status': 'success', 'message': result})
|
return jsonify({'status': 'success', 'message': result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error syncing aliases")
|
||||||
return jsonify({'status': 'error', 'message': f"Error syncing: {str(e)}"})
|
return jsonify({'status': 'error', 'message': f"Error syncing: {str(e)}"})
|
||||||
|
|
||||||
@app.route('/get_domains', methods=['POST'])
|
@app.route('/get_domains', methods=['POST'])
|
||||||
@@ -88,6 +387,7 @@ def get_domains():
|
|||||||
result = f"Domains: {', '.join(domain_list)}"
|
result = f"Domains: {', '.join(domain_list)}"
|
||||||
return jsonify({'status': 'success', 'message': result, 'domains': domain_list})
|
return jsonify({'status': 'success', 'message': result, 'domains': domain_list})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error getting domains")
|
||||||
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||||
|
|
||||||
@app.route('/create_alias', methods=['POST'])
|
@app.route('/create_alias', methods=['POST'])
|
||||||
@@ -106,6 +406,7 @@ def create_alias():
|
|||||||
result = f"Alias {alias} created successfully for {goto}"
|
result = f"Alias {alias} created successfully for {goto}"
|
||||||
return jsonify({'status': 'success', 'message': result})
|
return jsonify({'status': 'success', 'message': result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error creating alias")
|
||||||
return jsonify({'status': 'error', 'message': f"Error creating alias: {str(e)}"})
|
return jsonify({'status': 'error', 'message': f"Error creating alias: {str(e)}"})
|
||||||
|
|
||||||
@app.route('/delete_alias', methods=['POST'])
|
@app.route('/delete_alias', methods=['POST'])
|
||||||
@@ -123,6 +424,7 @@ def delete_alias_route():
|
|||||||
result = f"Alias {alias} deleted successfully"
|
result = f"Alias {alias} deleted successfully"
|
||||||
return jsonify({'status': 'success', 'message': result})
|
return jsonify({'status': 'success', 'message': result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error deleting alias")
|
||||||
return jsonify({'status': 'error', 'message': f"Error deleting alias: {str(e)}"})
|
return jsonify({'status': 'error', 'message': f"Error deleting alias: {str(e)}"})
|
||||||
|
|
||||||
@app.route('/delete_aliases_bulk', methods=['POST'])
|
@app.route('/delete_aliases_bulk', methods=['POST'])
|
||||||
@@ -177,6 +479,7 @@ def delete_aliases_bulk():
|
|||||||
'details': result
|
'details': result
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error deleting aliases bulk")
|
||||||
return jsonify({'status': 'error', 'message': f"Error deleting aliases: {str(e)}"})
|
return jsonify({'status': 'error', 'message': f"Error deleting aliases: {str(e)}"})
|
||||||
|
|
||||||
@app.route('/create_timed_alias', methods=['POST'])
|
@app.route('/create_timed_alias', methods=['POST'])
|
||||||
@@ -195,6 +498,7 @@ def create_timed_alias():
|
|||||||
result = f"Timed alias created for {username} on domain {domain}"
|
result = f"Timed alias created for {username} on domain {domain}"
|
||||||
return jsonify({'status': 'success', 'message': result})
|
return jsonify({'status': 'success', 'message': result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error creating timed alias")
|
||||||
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||||
|
|
||||||
@app.route('/search', methods=['POST'])
|
@app.route('/search', methods=['POST'])
|
||||||
@@ -217,6 +521,7 @@ def search():
|
|||||||
|
|
||||||
return jsonify({'status': 'success', 'message': message, 'query': query})
|
return jsonify({'status': 'success', 'message': message, 'query': query})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error searching aliases")
|
||||||
return jsonify({'status': 'error', 'message': f"Search error: {str(e)}"})
|
return jsonify({'status': 'error', 'message': f"Search error: {str(e)}"})
|
||||||
|
|
||||||
@app.route('/config', methods=['GET', 'POST'])
|
@app.route('/config', methods=['GET', 'POST'])
|
||||||
@@ -262,8 +567,148 @@ def change_password_route():
|
|||||||
malias_w.change_password(old_password, new_password)
|
malias_w.change_password(old_password, new_password)
|
||||||
return jsonify({'status': 'success', 'message': 'Password changed successfully'})
|
return jsonify({'status': 'success', 'message': 'Password changed successfully'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("Error changing password")
|
||||||
return jsonify({'status': 'error', 'message': str(e)})
|
return jsonify({'status': 'error', 'message': str(e)})
|
||||||
|
|
||||||
|
# Add a health check endpoint for proxies
|
||||||
|
@app.route('/health')
|
||||||
|
def health_check():
|
||||||
|
"""Health check endpoint for proxies"""
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'authenticated': session.get('logged_in', False),
|
||||||
|
'authelia_user': session.get('authelia_user', None),
|
||||||
|
'auth_method': session.get('auth_method'),
|
||||||
|
'version': '1.0.2',
|
||||||
|
'cookies': {k: '***' for k in request.cookies.keys()}, # Only show cookie names
|
||||||
|
'authelia_headers_present': get_authelia_user() is not None,
|
||||||
|
'zoraxy_headers_present': 'X-Forwarded-Server' in request.headers
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add a debugging endpoint
|
||||||
|
@app.route('/debug')
|
||||||
|
def debug_info():
|
||||||
|
"""Show debug information about the current request"""
|
||||||
|
# Always allow this in production to help with troubleshooting
|
||||||
|
debug_data = {
|
||||||
|
'headers': dict(request.headers),
|
||||||
|
'cookies': {k: '***' for k in request.cookies.keys()}, # Don't expose cookie values
|
||||||
|
'session': {k: ('***' if k in ['user_token', 'csrf_token'] else v) for k, v in session.items()} if session else {},
|
||||||
|
'remote_addr': request.remote_addr,
|
||||||
|
'scheme': request.scheme,
|
||||||
|
'host': request.host,
|
||||||
|
'path': request.path,
|
||||||
|
'is_secure': request.is_secure,
|
||||||
|
'x_forwarded_for': request.headers.get('X-Forwarded-For'),
|
||||||
|
'x_forwarded_proto': request.headers.get('X-Forwarded-Proto'),
|
||||||
|
'x_forwarded_host': request.headers.get('X-Forwarded-Host'),
|
||||||
|
'x_forwarded_prefix': request.headers.get('X-Forwarded-Prefix'),
|
||||||
|
'remote_user': get_authelia_user(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional server information
|
||||||
|
debug_data['server_info'] = {
|
||||||
|
'cookie_settings': {
|
||||||
|
'SESSION_COOKIE_NAME': app.config['SESSION_COOKIE_NAME'],
|
||||||
|
'SESSION_COOKIE_SECURE': app.config['SESSION_COOKIE_SECURE'],
|
||||||
|
'SESSION_COOKIE_HTTPONLY': app.config['SESSION_COOKIE_HTTPONLY'],
|
||||||
|
'SESSION_COOKIE_SAMESITE': app.config['SESSION_COOKIE_SAMESITE'],
|
||||||
|
'SESSION_COOKIE_PATH': app.config['SESSION_COOKIE_PATH'],
|
||||||
|
},
|
||||||
|
'app_config': {
|
||||||
|
'DEBUG': app.debug,
|
||||||
|
'PREFERRED_URL_SCHEME': app.config['PREFERRED_URL_SCHEME'],
|
||||||
|
'ENABLE_PROXY': ENABLE_PROXY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = jsonify(debug_data)
|
||||||
|
response.headers['Cache-Control'] = 'no-store, must-revalidate'
|
||||||
|
response.headers['Pragma'] = 'no-cache'
|
||||||
|
response.headers['Expires'] = '0'
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Dedicated endpoint for header diagnostics
|
||||||
|
@app.route('/headers')
|
||||||
|
def show_headers():
|
||||||
|
"""Show all request headers - useful for debugging proxies"""
|
||||||
|
# Log headers to help diagnose issues with Zoraxy/Authelia
|
||||||
|
logger.info(f"Headers endpoint: All headers received: {dict(request.headers)}")
|
||||||
|
return jsonify({
|
||||||
|
'headers': dict(request.headers),
|
||||||
|
'remote_addr': request.remote_addr,
|
||||||
|
'authelia_user': get_authelia_user()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Self-test endpoint for cookies
|
||||||
|
@app.route('/cookie-test')
|
||||||
|
def cookie_test():
|
||||||
|
"""Test cookie handling"""
|
||||||
|
# Clear any existing cookie
|
||||||
|
resp = make_response(jsonify({'status': 'ok', 'message': 'Cookie set test'}))
|
||||||
|
|
||||||
|
# Set a test cookie with the same attributes as session
|
||||||
|
resp.set_cookie(
|
||||||
|
'malias_test_cookie',
|
||||||
|
'test-value',
|
||||||
|
max_age=3600,
|
||||||
|
secure=app.config['SESSION_COOKIE_SECURE'],
|
||||||
|
httponly=app.config['SESSION_COOKIE_HTTPONLY'],
|
||||||
|
samesite='None',
|
||||||
|
path=app.config['SESSION_COOKIE_PATH']
|
||||||
|
)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
# Endpoint to check if test cookie is set
|
||||||
|
@app.route('/cookie-check')
|
||||||
|
def cookie_check():
|
||||||
|
"""Check if test cookie was properly set"""
|
||||||
|
test_cookie = request.cookies.get('malias_test_cookie')
|
||||||
|
return jsonify({
|
||||||
|
'test_cookie_present': test_cookie is not None,
|
||||||
|
'test_cookie_value': test_cookie if test_cookie else None,
|
||||||
|
'all_cookies': {k: '***' for k in request.cookies.keys()}
|
||||||
|
})
|
||||||
|
|
||||||
|
# New endpoint to test Zoraxy auth configuration
|
||||||
|
@app.route('/authelia-test')
|
||||||
|
def authelia_test():
|
||||||
|
"""Test if Authelia headers are correctly passed through Zoraxy"""
|
||||||
|
all_headers = dict(request.headers)
|
||||||
|
authelia_headers = {}
|
||||||
|
|
||||||
|
# Check for common Authelia-related headers
|
||||||
|
auth_related_headers = [
|
||||||
|
'Remote-User', 'X-Remote-User', 'Remote-Groups', 'X-Remote-Groups',
|
||||||
|
'Remote-Name', 'X-Remote-Name', 'Remote-Email', 'X-Remote-Email',
|
||||||
|
'X-Authelia-URL', 'X-Original-URL', 'X-Forwarded-Proto'
|
||||||
|
]
|
||||||
|
|
||||||
|
for header in auth_related_headers:
|
||||||
|
if header.lower() in [h.lower() for h in all_headers.keys()]:
|
||||||
|
for actual_header in all_headers.keys():
|
||||||
|
if header.lower() == actual_header.lower():
|
||||||
|
authelia_headers[actual_header] = all_headers[actual_header]
|
||||||
|
|
||||||
|
# Check for auth cookies
|
||||||
|
auth_cookies = {}
|
||||||
|
for cookie_name in request.cookies:
|
||||||
|
if 'auth' in cookie_name.lower():
|
||||||
|
auth_cookies[cookie_name] = '***' # Hide actual value
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'request_host': request.host,
|
||||||
|
'authelia_user_detected': get_authelia_user() is not None,
|
||||||
|
'authelia_user': get_authelia_user(),
|
||||||
|
'authelia_headers': authelia_headers,
|
||||||
|
'auth_cookies': auth_cookies,
|
||||||
|
'all_headers_count': len(all_headers),
|
||||||
|
'zoraxy_detected': any('zoraxy' in h.lower() for h in all_headers.keys()) or 'X-Forwarded-Server' in all_headers,
|
||||||
|
'host_header': request.headers.get('Host'),
|
||||||
|
'referer': request.headers.get('Referer'),
|
||||||
|
})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Parse command-line arguments
|
# Parse command-line arguments
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
@@ -314,6 +759,9 @@ Environment Variables (Docker):
|
|||||||
else:
|
else:
|
||||||
debug_mode = os.getenv('FLASK_DEBUG', 'False').lower() in ('true', '1', 'yes')
|
debug_mode = os.getenv('FLASK_DEBUG', 'False').lower() in ('true', '1', 'yes')
|
||||||
|
|
||||||
|
# Set log level based on debug mode
|
||||||
|
logger.setLevel(logging.DEBUG if debug_mode else logging.INFO)
|
||||||
|
|
||||||
# Port
|
# Port
|
||||||
if args.port is not None:
|
if args.port is not None:
|
||||||
port = args.port
|
port = args.port
|
||||||
@@ -343,4 +791,6 @@ Environment Variables (Docker):
|
|||||||
print('=' * 60)
|
print('=' * 60)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
logger.info(f"Starting Mailcow Alias Manager - Debug: {debug_mode}, Host: {host}, Port: {port}")
|
||||||
|
|
||||||
app.run(debug=debug_mode, host=host, port=port)
|
app.run(debug=debug_mode, host=host, port=port)
|
||||||
@@ -8,6 +8,12 @@
|
|||||||
# Recommended Settings:
|
# Recommended Settings:
|
||||||
# - Production: FLASK_ENV=production, FLASK_DEBUG=False (default)
|
# - Production: FLASK_ENV=production, FLASK_DEBUG=False (default)
|
||||||
# - Development: FLASK_ENV=development, FLASK_DEBUG=True
|
# - Development: FLASK_ENV=development, FLASK_DEBUG=True
|
||||||
|
#
|
||||||
|
# Reverse Proxy Support:
|
||||||
|
# ----------------------
|
||||||
|
# This application is configured to work behind reverse proxies (Nginx, Traefik, Zoraxy, Authelia, etc.)
|
||||||
|
# The ProxyFix middleware automatically handles X-Forwarded-* headers for HTTPS detection
|
||||||
|
# No additional configuration needed for most standard proxy setups
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mailcow-alias-manager:
|
mailcow-alias-manager:
|
||||||
@@ -33,3 +39,28 @@ services:
|
|||||||
|
|
||||||
# Host binding (default: 0.0.0.0 for Docker)
|
# Host binding (default: 0.0.0.0 for Docker)
|
||||||
- FLASK_HOST=0.0.0.0
|
- FLASK_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Secret key for sessions - CRITICAL for multi-worker Gunicorn!
|
||||||
|
# All workers must use the SAME secret key to decode session cookies
|
||||||
|
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
# IMPORATNT! The key below is just an example. GENERATE YOUR OWN KEY WITH COMMAND ABOVE!
|
||||||
|
- SECRET_KEY=dca6920115b8bbc13c346c75d668a49849590c77d9baaf903296582f24ff816a
|
||||||
|
|
||||||
|
# Reverse Proxy Mode
|
||||||
|
# ------------------
|
||||||
|
# Set to 'true' when accessing through reverse proxy (Authelia, Zoraxy, Nginx, etc.)
|
||||||
|
# Set to 'false' for direct IP:port access (e.g., http://192.168.1.100:5172)
|
||||||
|
#
|
||||||
|
# ENABLE_PROXY=true (Proxy Mode):
|
||||||
|
# - ProxyFix middleware enabled (handles X-Forwarded-* headers)
|
||||||
|
# - Secure cookies required (HTTPS only)
|
||||||
|
# - SameSite=None (allows cross-origin authentication)
|
||||||
|
# - Access via: https://alias.yourdomain.com
|
||||||
|
#
|
||||||
|
# ENABLE_PROXY=false (Direct Mode) - DEFAULT:
|
||||||
|
# - ProxyFix middleware disabled
|
||||||
|
# - Standard cookies (HTTP allowed)
|
||||||
|
# - SameSite=Lax (standard browser behavior)
|
||||||
|
# - Access via: http://your-server-ip:5172
|
||||||
|
#
|
||||||
|
- ENABLE_PROXY=false
|
||||||
103
reset_password.py
Executable file
103
reset_password.py
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Password Reset Script for Mailcow Alias Manager
|
||||||
|
Usage: python3 reset_password.py [new_password]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
import bcrypt
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Database location
|
||||||
|
project_dir = Path(__file__).parent
|
||||||
|
filepath = project_dir.joinpath('data')
|
||||||
|
database = filepath.joinpath('malias2.db')
|
||||||
|
|
||||||
|
def reset_password(new_password):
|
||||||
|
"""Reset the admin password"""
|
||||||
|
|
||||||
|
if not database.exists():
|
||||||
|
print(f"❌ Error: Database not found at {database}")
|
||||||
|
print(" Make sure the application has been run at least once to create the database.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Hash the new password
|
||||||
|
password_hash = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
|
||||||
|
|
||||||
|
# Update database
|
||||||
|
conn = sqlite3.connect(database)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if auth table exists
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='auth'")
|
||||||
|
if not cursor.fetchone():
|
||||||
|
print("❌ Error: Auth table not found in database")
|
||||||
|
conn.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Update password
|
||||||
|
cursor.execute('UPDATE auth SET password_hash = ? WHERE id = 0', (password_hash.decode('utf-8'),))
|
||||||
|
|
||||||
|
# Verify update
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
print("❌ Error: No password record found. Creating new one...")
|
||||||
|
cursor.execute('INSERT INTO auth VALUES (?, ?)', (0, password_hash.decode('utf-8')))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(" ✅ Password Reset Successful!")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" New password: {new_password}")
|
||||||
|
print(f" Database: {database}")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("You can now login with the new password.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error resetting password: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Mailcow Alias Manager - Password Reset")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Get new password
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
new_password = sys.argv[1]
|
||||||
|
else:
|
||||||
|
print("Enter new password (or press Ctrl+C to cancel):")
|
||||||
|
new_password = input("New password: ").strip()
|
||||||
|
|
||||||
|
if not new_password:
|
||||||
|
print("❌ Error: Password cannot be empty")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if len(new_password) < 6:
|
||||||
|
print("⚠️ Warning: Password is less than 6 characters")
|
||||||
|
response = input("Continue anyway? (y/n): ").strip().lower()
|
||||||
|
if response != 'y':
|
||||||
|
print("Password reset cancelled.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print()
|
||||||
|
if reset_password(new_password):
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nPassword reset cancelled.")
|
||||||
|
sys.exit(0)
|
||||||
@@ -16,6 +16,12 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: 16px !important;
|
||||||
|
color: #707170FF;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
@@ -273,6 +279,13 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #707170FF;
|
||||||
|
align: center;
|
||||||
|
padding-top: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile Responsive Styles */
|
/* Mobile Responsive Styles */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
body {
|
body {
|
||||||
@@ -942,6 +955,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/list_aliases', {
|
const response = await fetch('/list_aliases', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -970,6 +984,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`/${actionName}`, {
|
const response = await fetch(`/${actionName}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
@@ -995,6 +1010,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/search', {
|
const response = await fetch('/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -1019,7 +1035,9 @@
|
|||||||
|
|
||||||
async function openConfig() {
|
async function openConfig() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/config');
|
const response = await fetch('/config', {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
const config = await response.json();
|
const config = await response.json();
|
||||||
|
|
||||||
document.getElementById('mailcowServer').value = config.mailcow_server || '';
|
document.getElementById('mailcowServer').value = config.mailcow_server || '';
|
||||||
@@ -1047,6 +1065,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/config', {
|
const response = await fetch('/config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -1081,6 +1100,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/change_password', {
|
const response = await fetch('/change_password', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -1128,6 +1148,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/create_alias', {
|
const response = await fetch('/create_alias', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -1176,6 +1197,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/search', {
|
const response = await fetch('/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -1368,6 +1390,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/delete_aliases_bulk', {
|
const response = await fetch('/delete_aliases_bulk', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -1401,6 +1424,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/delete_alias', {
|
const response = await fetch('/delete_alias', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -1463,6 +1487,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/create_timed_alias', {
|
const response = await fetch('/create_timed_alias', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
@@ -1498,5 +1523,9 @@
|
|||||||
if (e.target === this) closeTimedAlias();
|
if (e.target === this) closeTimedAlias();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
<div class="footer">
|
||||||
|
<p style="padding-top:2%">© <a href="https://opensource.org/license/mit">MIT</a> 2026 Rune Olsen <a href="https://blog.rune.pm">Blog</a> — <a href="https://gitlab.pm/rune/malias-web">Source Code</a></p>
|
||||||
|
</div>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -192,6 +192,7 @@
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
|
credentials: 'include', // CRITICAL: Include cookies in request
|
||||||
body: JSON.stringify({ password: password })
|
body: JSON.stringify({ password: password })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user