mirror of
https://github.com/theoleuthardt/homelab-docker-compose.git
synced 2026-06-05 23:41:07 +00:00
feat: invite web browser
This commit is contained in:
parent
a3af85cca1
commit
4981d63ef5
7 changed files with 186 additions and 85 deletions
1
matrix/.env
Normal file
1
matrix/.env
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
REGISTRATION_SHARED_SECRET=5c388ec3c5727803d2f44b6da96e91ca038e9557a5b9cff8c5dc5ace37d6cf83
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Erstellt einen Matrix-Einladungslink für einen neuen User.
|
|
||||||
Nutzung: python create_invite.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
# Nur für den generierten Link – API-Calls gehen direkt über localhost
|
|
||||||
PUBLIC_URL = "https://matrix.theocloud.dev"
|
|
||||||
HOMESERVER = "http://192.168.12.151:8008"
|
|
||||||
ADMIN_USER = "admin"
|
|
||||||
|
|
||||||
|
|
||||||
def login(password: str) -> str:
|
|
||||||
data = json.dumps({
|
|
||||||
"type": "m.login.password",
|
|
||||||
"user": ADMIN_USER,
|
|
||||||
"password": password,
|
|
||||||
}).encode()
|
|
||||||
|
|
||||||
req = urllib.request.Request(
|
|
||||||
f"{HOMESERVER}/_matrix/client/v3/login",
|
|
||||||
data=data,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
)
|
|
||||||
with urllib.request.urlopen(req) as resp:
|
|
||||||
return json.loads(resp.read())["access_token"]
|
|
||||||
|
|
||||||
|
|
||||||
def create_token(access_token: str, uses: int, expires_in_hours: int | None) -> str:
|
|
||||||
payload: dict = {"uses_allowed": uses}
|
|
||||||
|
|
||||||
if expires_in_hours:
|
|
||||||
expiry = datetime.now() + timedelta(hours=expires_in_hours)
|
|
||||||
payload["expiry_time"] = int(expiry.timestamp() * 1000)
|
|
||||||
|
|
||||||
data = json.dumps(payload).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
f"{HOMESERVER}/_synapse/admin/v1/registration_tokens/new",
|
|
||||||
data=data,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {access_token}",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
with urllib.request.urlopen(req) as resp:
|
|
||||||
return json.loads(resp.read())["token"]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
password = input(f"Admin-Passwort für '{ADMIN_USER}': ")
|
|
||||||
|
|
||||||
uses_input = input("Wie oft soll der Link nutzbar sein? [1]: ").strip()
|
|
||||||
uses = int(uses_input) if uses_input else 1
|
|
||||||
|
|
||||||
expires_input = input("Ablauf in Stunden? (leer = kein Ablauf): ").strip()
|
|
||||||
expires_in_hours = int(expires_input) if expires_input else None
|
|
||||||
|
|
||||||
print("\nErstelle Token...")
|
|
||||||
try:
|
|
||||||
access_token = login(password)
|
|
||||||
token = create_token(access_token, uses, expires_in_hours)
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
print(f"Fehler: {e.status} – {e.read().decode()}")
|
|
||||||
return
|
|
||||||
|
|
||||||
link = f"{PUBLIC_URL}/#/register?token={token}"
|
|
||||||
print(f"\nEinladungslink:\n{link}")
|
|
||||||
if expires_in_hours:
|
|
||||||
expires_at = datetime.now() + timedelta(hours=expires_in_hours)
|
|
||||||
print(f"Gültig bis: {expires_at.strftime('%d.%m.%Y %H:%M')}")
|
|
||||||
print(f"Nutzungen: {uses}x")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -41,30 +41,46 @@ services:
|
||||||
if grep -q "^enable_registration:" /data/homeserver.yaml; then
|
if grep -q "^enable_registration:" /data/homeserver.yaml; then
|
||||||
sed -i 's/^enable_registration:.*/enable_registration: true/' /data/homeserver.yaml
|
sed -i 's/^enable_registration:.*/enable_registration: true/' /data/homeserver.yaml
|
||||||
else
|
else
|
||||||
echo "enable_registration: true" >> /data/homeserver.yaml
|
printf '\nenable_registration: true\n' >> /data/homeserver.yaml
|
||||||
fi
|
fi
|
||||||
# Nur per Token registrieren erlauben
|
# Nur per Token registrieren erlauben
|
||||||
if grep -q "^registration_requires_token:" /data/homeserver.yaml; then
|
if grep -q "^registration_requires_token:" /data/homeserver.yaml; then
|
||||||
sed -i 's/^registration_requires_token:.*/registration_requires_token: true/' /data/homeserver.yaml
|
sed -i 's/^registration_requires_token:.*/registration_requires_token: true/' /data/homeserver.yaml
|
||||||
else
|
else
|
||||||
echo "registration_requires_token: true" >> /data/homeserver.yaml
|
printf '\nregistration_requires_token: true\n' >> /data/homeserver.yaml
|
||||||
fi
|
fi
|
||||||
# Shared Secret für register_new_matrix_user CLI
|
# Shared Secret für register_new_matrix_user CLI
|
||||||
if grep -q "^registration_shared_secret:" /data/homeserver.yaml; then
|
if grep -q "^registration_shared_secret:" /data/homeserver.yaml; then
|
||||||
sed -i "s/^registration_shared_secret:.*/registration_shared_secret: ${REGISTRATION_SHARED_SECRET}/" /data/homeserver.yaml
|
sed -i "s/^registration_shared_secret:.*/registration_shared_secret: ${REGISTRATION_SHARED_SECRET}/" /data/homeserver.yaml
|
||||||
else
|
else
|
||||||
echo "registration_shared_secret: ${REGISTRATION_SHARED_SECRET}" >> /data/homeserver.yaml
|
printf '\nregistration_shared_secret: %s\n' "${REGISTRATION_SHARED_SECRET}" >> /data/homeserver.yaml
|
||||||
fi
|
fi
|
||||||
# Email-Verifizierung deaktiviert (kein SMTP nötig)
|
# Email-Verifizierung deaktiviert (kein SMTP nötig)
|
||||||
if grep -q "^enable_registration_without_verification:" /data/homeserver.yaml; then
|
if grep -q "^enable_registration_without_verification:" /data/homeserver.yaml; then
|
||||||
sed -i 's/^enable_registration_without_verification:.*/enable_registration_without_verification: true/' /data/homeserver.yaml
|
sed -i 's/^enable_registration_without_verification:.*/enable_registration_without_verification: true/' /data/homeserver.yaml
|
||||||
else
|
else
|
||||||
echo "enable_registration_without_verification: true" >> /data/homeserver.yaml
|
printf '\nenable_registration_without_verification: true\n' >> /data/homeserver.yaml
|
||||||
fi
|
fi
|
||||||
echo "Starting Synapse..."
|
echo "Starting Synapse..."
|
||||||
exec /start.py
|
exec /start.py
|
||||||
volumes:
|
volumes:
|
||||||
- synapse_data:/data
|
- synapse_data:/data
|
||||||
|
|
||||||
|
well-known:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: matrix-well-known
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 8070:80
|
||||||
|
volumes:
|
||||||
|
- ./nginx/well-known.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
|
||||||
|
invite-app:
|
||||||
|
build: ./invite-app
|
||||||
|
container_name: matrix-invite
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 8050:8090
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
synapse_data:
|
synapse_data:
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ Einmalig nach dem ersten Start:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec -it synapse register_new_matrix_user \
|
docker exec -it synapse register_new_matrix_user \
|
||||||
-c /data/homeserver.yaml \
|
|
||||||
-u admin \
|
-u admin \
|
||||||
-p SICHERESPASSWORT \
|
-p SICHERESPASSWORT \
|
||||||
--admin \
|
--admin \
|
||||||
|
|
|
||||||
4
matrix/invite-app/Dockerfile
Normal file
4
matrix/invite-app/Dockerfile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY app.py .
|
||||||
|
CMD ["python", "app.py"]
|
||||||
139
matrix/invite-app/app.py
Normal file
139
matrix/invite-app/app.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
|
HOMESERVER = "http://192.168.12.151:8008"
|
||||||
|
PUBLIC_URL = "https://matrix.theocloud.dev"
|
||||||
|
ADMIN_USER = "admin"
|
||||||
|
|
||||||
|
HTML = """<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Matrix Einladung</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: system-ui, sans-serif; background: #0d1117; color: #e6edf3; display: flex; justify-content: center; align-items: center; min-height: 100vh; }}
|
||||||
|
.card {{ background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 2rem; width: 100%; max-width: 420px; }}
|
||||||
|
h1 {{ font-size: 1.25rem; margin-bottom: 1.5rem; color: #58a6ff; }}
|
||||||
|
label {{ display: block; font-size: 0.85rem; color: #8b949e; margin-bottom: 0.4rem; margin-top: 1rem; }}
|
||||||
|
input {{ width: 100%; padding: 0.6rem 0.8rem; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; font-size: 1rem; }}
|
||||||
|
input:focus {{ outline: none; border-color: #58a6ff; }}
|
||||||
|
button {{ margin-top: 1.5rem; width: 100%; padding: 0.7rem; background: #238636; border: none; border-radius: 6px; color: #fff; font-size: 1rem; cursor: pointer; }}
|
||||||
|
button:hover {{ background: #2ea043; }}
|
||||||
|
.result {{ margin-top: 1.5rem; padding: 1rem; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; word-break: break-all; }}
|
||||||
|
.result a {{ color: #58a6ff; }}
|
||||||
|
.error {{ margin-top: 1.5rem; padding: 1rem; background: #2d1117; border: 1px solid #f85149; border-radius: 6px; color: #f85149; font-size: 0.9rem; }}
|
||||||
|
.meta {{ font-size: 0.8rem; color: #8b949e; margin-top: 0.5rem; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Matrix Einladungslink</h1>
|
||||||
|
<form method="POST">
|
||||||
|
<label>Admin-Passwort</label>
|
||||||
|
<input type="password" name="password" required autofocus>
|
||||||
|
<label>Nutzungen</label>
|
||||||
|
<input type="number" name="uses" value="1" min="1">
|
||||||
|
<label>Ablauf in Stunden (leer = kein Ablauf)</label>
|
||||||
|
<input type="number" name="expires" min="1" placeholder="z.B. 24">
|
||||||
|
<button type="submit">Link generieren</button>
|
||||||
|
</form>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def login(password: str) -> str:
|
||||||
|
data = json.dumps({
|
||||||
|
"type": "m.login.password",
|
||||||
|
"user": ADMIN_USER,
|
||||||
|
"password": password,
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{HOMESERVER}/_matrix/client/v3/login",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
return json.loads(resp.read())["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def create_token(access_token: str, uses: int, expires_in_hours: int | None) -> str:
|
||||||
|
payload: dict = {"uses_allowed": uses}
|
||||||
|
if expires_in_hours:
|
||||||
|
expiry = datetime.now() + timedelta(hours=expires_in_hours)
|
||||||
|
payload["expiry_time"] = int(expiry.timestamp() * 1000)
|
||||||
|
data = json.dumps(payload).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{HOMESERVER}/_synapse/admin/v1/registration_tokens/new",
|
||||||
|
data=data,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
return json.loads(resp.read())["token"]
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_html(self, content: str, status: int = 200):
|
||||||
|
body = HTML.format(content=content).encode()
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", len(body))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self.send_html("")
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(length).decode()
|
||||||
|
params = parse_qs(body)
|
||||||
|
|
||||||
|
password = params.get("password", [""])[0]
|
||||||
|
uses = int(params.get("uses", ["1"])[0] or 1)
|
||||||
|
expires_raw = params.get("expires", [""])[0].strip()
|
||||||
|
expires_in_hours = int(expires_raw) if expires_raw else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = login(password)
|
||||||
|
token = create_token(access_token, uses, expires_in_hours)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
error_body = e.read().decode()
|
||||||
|
try:
|
||||||
|
msg = json.loads(error_body).get("error", error_body)
|
||||||
|
except Exception:
|
||||||
|
msg = error_body
|
||||||
|
self.send_html(f'<div class="error">{msg}</div>')
|
||||||
|
return
|
||||||
|
|
||||||
|
link = f"{PUBLIC_URL}/#/register?token={token}"
|
||||||
|
meta_parts = [f"{uses}x nutzbar"]
|
||||||
|
if expires_in_hours:
|
||||||
|
expires_at = datetime.now() + timedelta(hours=expires_in_hours)
|
||||||
|
meta_parts.append(f"gültig bis {expires_at.strftime('%d.%m.%Y %H:%M')}")
|
||||||
|
|
||||||
|
self.send_html(f"""
|
||||||
|
<div class="result">
|
||||||
|
<a href="{link}" target="_blank">{link}</a>
|
||||||
|
<div class="meta">{' · '.join(meta_parts)}</div>
|
||||||
|
</div>""")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = HTTPServer(("0.0.0.0", 8090), Handler)
|
||||||
|
print("Invite app running on http://0.0.0.0:8090")
|
||||||
|
server.serve_forever()
|
||||||
22
matrix/nginx/well-known.conf
Normal file
22
matrix/nginx/well-known.conf
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location /.well-known/matrix/server {
|
||||||
|
default_type application/json;
|
||||||
|
return 200 '{"m.server":"matrix.theocloud.dev:443"}';
|
||||||
|
}
|
||||||
|
|
||||||
|
location /.well-known/matrix/client {
|
||||||
|
default_type application/json;
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
return 200 '{"m.homeserver":{"base_url":"https://matrix.theocloud.dev"}}';
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://192.168.12.151:8008;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue