Successful logins, user creation and deletion, password changes and the enabled and admin toggles are logged with actor, target and IP. So are failed logins, a wrong current password and denied access. Values are JSON encoded, so client input cannot break the line or add fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
3.8 KiB
Python
135 lines
3.8 KiB
Python
from dataclasses import dataclass, fields
|
|
|
|
from .common import NoteLinesMixin, create_email
|
|
from .connection import with_cursor
|
|
|
|
|
|
@dataclass
|
|
class User(NoteLinesMixin):
|
|
id: int
|
|
email_id: int
|
|
email: str
|
|
passwordhash: str
|
|
enabled: bool
|
|
is_admin: bool
|
|
can_use_different_alias_domain: bool
|
|
note: str
|
|
|
|
|
|
select_fields = ", ".join(
|
|
map(
|
|
lambda field: f"{'u' if field.name != 'email' else 'e'}.{field.name}",
|
|
fields(User),
|
|
)
|
|
)
|
|
base_user_query = (
|
|
f"SELECT {select_fields} FROM users u INNER JOIN emails e ON u.email_id=e.id"
|
|
)
|
|
|
|
|
|
@with_cursor
|
|
def get_user_by_email(cur, email):
|
|
return get_user(cur, f"{base_user_query} WHERE e.email=lower(%s)", email)
|
|
|
|
|
|
@with_cursor
|
|
def get_user_by_user_id(cur, user_id):
|
|
return get_user(cur, f"{base_user_query} WHERE u.id=%s", user_id)
|
|
|
|
|
|
def get_user(cur, query, param):
|
|
cur.execute(query, [param])
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return None
|
|
return User(*row)
|
|
|
|
|
|
@with_cursor
|
|
def get_users(cur):
|
|
cur.execute(f"{base_user_query} ORDER BY e.email ASC")
|
|
return [User(*row) for row in cur.fetchall()]
|
|
|
|
|
|
@with_cursor
|
|
def update_user(cur, id, **fields):
|
|
"""only pwhash, enabled, is_admin, can_use_different_alias_domain, note"""
|
|
cur.execute(
|
|
"UPDATE users SET "
|
|
+ ", ".join(f"{field}=%s" for field in fields.keys())
|
|
+ " WHERE id=%s",
|
|
list(fields.values()) + [id],
|
|
)
|
|
|
|
|
|
@with_cursor
|
|
def create_user(
|
|
cur, email, passwordhash, enabled, is_admin, can_use_different_alias_domain, note
|
|
):
|
|
"""Returns the id of the new user"""
|
|
email_id = create_email(cur, email)
|
|
cur.execute(
|
|
"""\
|
|
INSERT INTO users
|
|
(email_id, passwordhash, enabled, is_admin, can_use_different_alias_domain, note)
|
|
VALUES (%s, %s, %s, %s, %s, %s) RETURNING id""",
|
|
[
|
|
email_id,
|
|
passwordhash,
|
|
enabled,
|
|
is_admin,
|
|
can_use_different_alias_domain,
|
|
note,
|
|
],
|
|
)
|
|
return cur.fetchone()[0]
|
|
|
|
|
|
@with_cursor
|
|
def delete_user(cur, user):
|
|
cur.execute("DELETE FROM users WHERE id=%s", [user.id])
|
|
cur.execute("DELETE FROM emails WHERE id=%s", [user.email_id])
|
|
|
|
|
|
@with_cursor
|
|
def reset_or_create_user(cur, email, hash, is_admin):
|
|
# does the email exists?
|
|
cur.execute("SELECT id FROM emails WHERE email=%s", [email])
|
|
email_id = cur.fetchone()
|
|
if email_id is not None:
|
|
email_id = email_id[0]
|
|
# alias or user?
|
|
cur.execute("SELECT id FROM users WHERE email_id=%s", [email_id])
|
|
user_id = cur.fetchone()
|
|
if user_id is not None:
|
|
user_id = user_id[0]
|
|
# update User
|
|
cur.execute(
|
|
"UPDATE users SET passwordhash=%s, is_admin=%s, enabled=%s WHERE id=%s",
|
|
[hash, is_admin, True, user_id],
|
|
)
|
|
print(
|
|
f"reset password for {email}. Set admin={is_admin} and enabled the user."
|
|
)
|
|
else:
|
|
print(f"{email} is used as an alias. Please specify another one.")
|
|
else:
|
|
# user does not exists. create him.
|
|
cur.execute("INSERT INTO emails (email) VALUES (%s) RETURNING id", [email])
|
|
email_id = cur.fetchone()[0]
|
|
cur.execute(
|
|
"INSERT INTO users (email_id, passwordhash, enabled, is_admin, can_use_different_alias_domain, note) VALUES (%s, %s, %s, %s, %s, %s)", # noqa
|
|
[email_id, hash, True, is_admin, is_admin, "Created by commandline"],
|
|
)
|
|
user_type = "admin" if is_admin else "user"
|
|
print(f"Created {user_type} {email}")
|
|
|
|
|
|
@with_cursor
|
|
def is_last_enabled_admin(cur, user_id):
|
|
cur.execute(
|
|
"SELECT EXISTS (SELECT 1 FROM users u WHERE u.id != %s AND u.is_admin AND u.enabled)",
|
|
[user_id],
|
|
)
|
|
return not cur.fetchone()[0]
|