From fe2a579fc4be23fd096a862d5ee45ccebb4969f9 Mon Sep 17 00:00:00 2001 From: "georg.sinn-schirwitz" Date: Sat, 30 Aug 2025 18:33:08 +0200 Subject: [PATCH] completing user administration --- web/app.py | 35 ++++++ web/db.py | 41 +++++- web/templates/admin/user.html | 114 +++++++++++++++++ web/templates/admin/users.html | 219 ++++++++++++++------------------- web/templates/index.html | 3 +- 5 files changed, 276 insertions(+), 136 deletions(-) create mode 100644 web/templates/admin/user.html diff --git a/web/app.py b/web/app.py index fd996e2..154ed61 100644 --- a/web/app.py +++ b/web/app.py @@ -6,6 +6,7 @@ from typing import Dict, List from web.craigslist import scraper from web.db import ( db_init, + delete_user_by_id, get_all_jobs, mark_favorite, record_visit, @@ -13,6 +14,7 @@ from web.db import ( create_or_update_user, verify_user_credentials, get_user, + get_user_by_id, get_user_regions, get_user_keywords, set_user_regions, @@ -342,6 +344,39 @@ def admin_users(): return render_template('admin/users.html', users=users, title='Users') +@app.route('/admin/user/', methods=['GET', 'POST']) +def admin_user(user_id): + if not require_admin(): + return redirect(url_for('login')) + user = get_user_by_id(user_id) + if request.method == 'POST': + data = request.form + username = (data.get('username') or '').strip() + password = data.get('new_password') + is_admin = bool(data.get('is_admin')) + is_active = bool(data.get('is_active')) if data.get( + 'is_active') is not None else True + try: + create_or_update_user( + username, password=password, is_admin=is_admin, is_active=is_active) + flash('User saved') + except Exception as e: + flash(f'Error: {e}') + return redirect(url_for('admin_users')) + return render_template('admin/user.html', user=user, title='User') + + +@app.route('/admin/user//delete', methods=['POST']) +def admin_user_delete(user_id): + if not require_admin(): + return redirect(url_for('login')) + if delete_user_by_id(user_id): + flash('User deleted') + else: + flash('Error deleting user') + return redirect(url_for('admin_users')) + + # ---------------- User settings (regions/keywords) ------------------------- @app.route('/settings', methods=['GET', 'POST']) diff --git a/web/db.py b/web/db.py index 265f69d..e8a75d0 100644 --- a/web/db.py +++ b/web/db.py @@ -534,7 +534,8 @@ def remove_job(url): def get_or_create_user(username: str) -> int: """Return user_id for username, creating if missing.""" - created_at = datetime.now(UTC).isoformat() + # 2025-08-30T16:04:29.660245+00:00 is wrong. should be 2025-08-30T16:04:29 + created_at = datetime.now(UTC).isoformat().split('.')[0] with _ensure_session() as session: row = session.execute( text("SELECT user_id FROM users WHERE username = :u"), { @@ -654,22 +655,50 @@ def get_user(username: str) -> Optional[Dict[str, Any]]: """Return single user dict or None.""" with _ensure_session() as session: row = session.execute(text( - "SELECT user_id, username, is_admin, is_active, password_hash, last_login, created_at FROM users WHERE username = :u" + "SELECT user_id, username, created_at, is_admin, is_active, last_login, (password_hash IS NOT NULL) AS has_pw FROM users WHERE username = :u" ), {"u": username}).fetchone() if not row: return None return { "user_id": int(row[0]), "username": row[1], - "is_admin": bool(row[2]), - "is_active": bool(row[3]), - "password_hash": row[4], + "created_at": row[2].isoformat() if isinstance(row[2], datetime) else (row[2] or None), + "is_admin": bool(row[3]), + "is_active": bool(row[4]), "last_login": row[5].isoformat() if row[5] else None, - "created_at": row[6].isoformat() if isinstance(row[6], datetime) else (row[6] or None), + "has_password": bool(row[6]), } +def get_user_by_id(user_id: int) -> Optional[Dict[str, Any]]: + """Return single user dict or None.""" + with _ensure_session() as session: + row = session.execute(text( + "SELECT user_id, username, created_at, is_admin, is_active, last_login, (password_hash IS NOT NULL) AS has_pw FROM users WHERE user_id = :u" + ), {"u": user_id}).fetchone() + if not row: + return None + return { + "user_id": int(row[0]), + "username": row[1], + "created_at": row[2].isoformat() if isinstance(row[2], datetime) else (row[2] or None), + "is_admin": bool(row[3]), + "is_active": bool(row[4]), + "last_login": row[5].isoformat() if row[5] else None, + "has_password": bool(row[6]), + } + + +def delete_user_by_id(user_id: int) -> bool: + with _ensure_session() as session: + result = session.execute( + text("DELETE FROM users WHERE user_id = :u"), {"u": user_id}) + session.commit() + return result.rowcount > 0 + # ---------------- Regions/Keywords helpers --------------------------------- + + def upsert_region(name: str) -> int: """Get or create a region by name; return region_id.""" name = (name or "").strip() diff --git a/web/templates/admin/user.html b/web/templates/admin/user.html new file mode 100644 index 0000000..56669d2 --- /dev/null +++ b/web/templates/admin/user.html @@ -0,0 +1,114 @@ +{% extends 'base.html' %} {% block content %} +
+ {% if not user %} +

Create new user

+
+ +
+

+ Username: + +

+

+ Password: + +

+

+ Admin: + +

+

+ Active: + +

+ +
+
+ {% else %} +

User {{ user.username }}

+
+ + + +
+

ID: {{ user.user_id }}

+

Username: {{ user.username }}

+

Created At: {{ user.created_at }}

+

Last Login: {{ user.last_login }}

+

+ Admin: + +

+

+ Active: + +

+

+ Has Password: {{ '✅' if user.has_password else '❌' }} +

+

+ New Password: + +

+ +
+
+
+ + +{% endif %} {% endblock %} {% block footer_scripts %} {% endblock %} diff --git a/web/templates/admin/users.html b/web/templates/admin/users.html index 6696cdd..8651f59 100644 --- a/web/templates/admin/users.html +++ b/web/templates/admin/users.html @@ -1,139 +1,100 @@ {% extends 'base.html' %} {% block content %}

Users

-
- - - - - - - - - - - - - - - - {% for u in users %} - - - - - - - - - - - - {% endfor %} - -
IDUsernameAdminActivePasswordCreatedLast Login
- {{ u.user_id }} - - - - - - - {{ '✅' if u.has_password else '❌' }}{{ u.created_at }}{{ u.last_login or 'never' }} - -
-
-
-

Create / Update User

-
- - - - - -
+ + + + + + + + + + + + + + + + + {% for u in users %} + + + + + + + + + + + + {% endfor %} + +
IDUsernameAdminActivePasswordCreatedLast LoginEditDelete
{{ u.user_id }} + {{ u.username }} + {{ '✅' if u.is_admin else '❌' }}{{ '✅' if u.is_active else '❌' }}{{ '✅' if u.has_password else '❌' }}{{ u.created_at }}{{ u.last_login or 'never' }} + + + +
+ +

Create New User

+Create User {% endblock %} {% block footer_scripts %} {% endblock %} diff --git a/web/templates/index.html b/web/templates/index.html index 183acde..a69261f 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -44,7 +44,8 @@
{% for job in jobs %}
-

{{ job['title'] }}

+ +

{{ job['title'] }}

{{ job['posted_time'] }}

{{ job['region'] }} {{ job['keyword'] }}