modified: web/blueprints/auth.py

modified:   web/blueprints/group_admin.py
	modified:   web/config.py
	modified:   web/panel_db.py
	new file:   web/templates/auth/accept_invite.html
	modified:   web/templates/group_admin/base.html
	modified:   web/templates/group_admin/members.html
This commit is contained in:
simon
2026-04-13 10:26:47 +02:00
parent 484687a076
commit 6b13ea5c22
7 changed files with 404 additions and 12 deletions

View File

@@ -4,6 +4,8 @@ Verwaltet Nutzer, Gruppen, Mitgliedschaften (PANEL_DB)
und verschlüsselte MC-DB-Zugangsdaten (CREDS_DB).
"""
import json
import secrets
from datetime import datetime, timedelta
import pymysql
import pymysql.cursors
from config import Config
@@ -98,6 +100,23 @@ PANEL_SCHEMA = [
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
"""CREATE TABLE IF NOT EXISTS group_invites (
id INT AUTO_INCREMENT PRIMARY KEY,
group_id INT NOT NULL,
invited_username VARCHAR(50) NOT NULL,
invited_email VARCHAR(255) NOT NULL,
role ENUM('admin','member') DEFAULT 'member',
token VARCHAR(128) UNIQUE NOT NULL,
created_by_user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
accepted_at DATETIME NULL,
revoked_at DATETIME NULL,
UNIQUE KEY uq_group_pending_invite_email (group_id, invited_email, revoked_at, accepted_at),
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
]
CREDS_SCHEMA = [
@@ -146,6 +165,130 @@ def create_user(username: str, email: str, password: str, is_site_admin: bool =
)
def create_user_for_group(username: str, email: str, password: str, group_id: int, role: str = "member") -> int:
"""Create a non-site-admin user and assign them to a group atomically."""
permissions = Config.DEFAULT_PERMISSIONS
salt = generate_salt()
pw_hash = hash_password(password, salt)
conn = get_panel_db()
conn.autocommit(False)
try:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO users (username, email, password_hash, salt, is_site_admin) VALUES (%s,%s,%s,%s,%s)",
(username, email, pw_hash, salt, 0),
)
user_id = cur.lastrowid
cur.execute(
"INSERT INTO group_members (user_id, group_id, role, permissions) VALUES (%s,%s,%s,%s)",
(user_id, group_id, role, json.dumps(permissions)),
)
conn.commit()
return user_id
except Exception:
conn.rollback()
raise
finally:
conn.close()
def create_group_invite(group_id: int, username: str, email: str, role: str, created_by_user_id: int) -> str:
expires_at = datetime.utcnow() + timedelta(hours=Config.INVITE_EXPIRY_HOURS)
token = secrets.token_urlsafe(32)
_panel_query(
"INSERT INTO group_invites (group_id, invited_username, invited_email, role, token, created_by_user_id, expires_at) "
"VALUES (%s,%s,%s,%s,%s,%s,%s)",
(group_id, username, email, role, token, created_by_user_id, expires_at),
write=True,
)
return token
def list_active_group_invites(group_id: int):
return _panel_query(
"SELECT gi.*, u.username AS created_by_username "
"FROM group_invites gi "
"JOIN users u ON u.id = gi.created_by_user_id "
"WHERE gi.group_id=%s AND gi.accepted_at IS NULL AND gi.revoked_at IS NULL AND gi.expires_at > UTC_TIMESTAMP() "
"ORDER BY gi.created_at DESC",
(group_id,),
)
def get_active_invite_by_email(group_id: int, email: str):
return _panel_query(
"SELECT * FROM group_invites WHERE group_id=%s AND invited_email=%s "
"AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > UTC_TIMESTAMP()",
(group_id, email),
fetchone=True,
)
def get_invite_by_token(token: str):
return _panel_query(
"SELECT gi.*, g.name AS group_name, u.username AS created_by_username "
"FROM group_invites gi "
"JOIN user_groups g ON g.id = gi.group_id "
"JOIN users u ON u.id = gi.created_by_user_id "
"WHERE gi.token=%s",
(token,),
fetchone=True,
)
def revoke_group_invite(invite_id: int, group_id: int):
_panel_query(
"UPDATE group_invites SET revoked_at=UTC_TIMESTAMP() WHERE id=%s AND group_id=%s AND accepted_at IS NULL AND revoked_at IS NULL",
(invite_id, group_id),
write=True,
)
def accept_group_invite(token: str, password: str) -> dict | None:
invite = get_invite_by_token(token)
if not invite:
return None
if invite.get("accepted_at") or invite.get("revoked_at"):
return None
if invite["expires_at"] <= datetime.utcnow():
return None
permissions = Config.DEFAULT_PERMISSIONS
salt = generate_salt()
pw_hash = hash_password(password, salt)
conn = get_panel_db()
conn.autocommit(False)
try:
with conn.cursor() as cur:
cur.execute("SELECT id FROM users WHERE username=%s OR email=%s", (invite["invited_username"], invite["invited_email"]))
if cur.fetchone():
conn.rollback()
return {"error": "username_or_email_taken"}
cur.execute(
"INSERT INTO users (username, email, password_hash, salt, is_site_admin) VALUES (%s,%s,%s,%s,%s)",
(invite["invited_username"], invite["invited_email"], pw_hash, salt, 0),
)
user_id = cur.lastrowid
cur.execute(
"INSERT INTO group_members (user_id, group_id, role, permissions) VALUES (%s,%s,%s,%s)",
(user_id, invite["group_id"], invite["role"], json.dumps(permissions)),
)
cur.execute(
"UPDATE group_invites SET accepted_at=UTC_TIMESTAMP() WHERE id=%s AND accepted_at IS NULL AND revoked_at IS NULL",
(invite["id"],),
)
conn.commit()
return {"user_id": user_id, "group_id": invite["group_id"]}
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_user_by_username(username: str):
return _panel_query("SELECT * FROM users WHERE username=%s", (username,), fetchone=True)