From 9963c3e7fef45de41a4763f31cf363899abc0e90 Mon Sep 17 00:00:00 2001 From: Cian Hughes Date: Thu, 13 Aug 2026 14:03:03 +0100 Subject: [PATCH] Added user login and account creation to admin panels --- invenio_theme_iform/administration/views.py | 165 ++++++++++++++++-- invenio_theme_iform/ext.py | 30 +++- .../accounts/header_login.html | 2 +- .../accounts/registration_disabled.html | 42 +++++ .../administration/registration_settings.html | 61 +++++++ .../administration/user_create.html | 58 ++++++ pyproject.toml | 2 + 7 files changed, 346 insertions(+), 14 deletions(-) create mode 100644 invenio_theme_iform/templates/invenio_theme_iform/accounts/registration_disabled.html create mode 100644 invenio_theme_iform/templates/invenio_theme_iform/administration/registration_settings.html create mode 100644 invenio_theme_iform/templates/invenio_theme_iform/administration/user_create.html diff --git a/invenio_theme_iform/administration/views.py b/invenio_theme_iform/administration/views.py index f13eca6..059e54c 100644 --- a/invenio_theme_iform/administration/views.py +++ b/invenio_theme_iform/administration/views.py @@ -6,9 +6,13 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. -"""I-Form role management administration views.""" +"""I-Form administration views.""" -from flask import flash, redirect, request, url_for +import json +import os +from datetime import datetime + +from flask import current_app, flash, redirect, request, url_for from invenio_accounts.models import Role, User from invenio_accounts.proxies import current_accounts from invenio_administration.views.base import AdminView @@ -16,6 +20,32 @@ from invenio_db import db from invenio_i18n import lazy_gettext as _ +def get_site_settings_path(): + """Get the path to site_settings.json.""" + app_data_dir = os.path.join(current_app.instance_path, "app_data") + os.makedirs(app_data_dir, exist_ok=True) + return os.path.join(app_data_dir, "site_settings.json") + + +def get_site_settings(): + """Load site settings from JSON file.""" + path = get_site_settings_path() + if not os.path.exists(path): + return {"registration_enabled": True} + try: + with open(path, "r") as f: + return json.load(f) + except Exception: + return {"registration_enabled": True} + + +def set_site_settings(settings): + """Save site settings to JSON file.""" + path = get_site_settings_path() + with open(path, "w") as f: + json.dump(settings, f, indent=2) + + class RoleManagementView(AdminView): """Admin view for managing user roles.""" @@ -30,14 +60,14 @@ class RoleManagementView(AdminView): """Render the role management template.""" roles = Role.query.order_by(Role.name).all() selected_role_id = request.args.get("role_id") - + users = [] selected_role = None if selected_role_id: selected_role = Role.query.get(selected_role_id) if selected_role: users = User.query.order_by(User.email).all() - + return self.render( roles=roles, selected_role=selected_role, @@ -49,30 +79,141 @@ class RoleManagementView(AdminView): user_id = request.form.get("user_id") role_id = request.form.get("role_id") action = request.form.get("action") - + if not (user_id and role_id and action): flash(_("Missing required fields."), "error") return redirect(url_for("administration.roles", role_id=role_id)) - + try: user = User.query.get(int(user_id)) except ValueError: user = None - + role = Role.query.get(role_id) - + if not user or not role: flash(_("User or role not found."), "error") return redirect(url_for("administration.roles", role_id=role_id)) - + if action == "enable": if role not in user.roles: current_accounts.datastore.add_role_to_user(user, role) - flash(_("Role {role} enabled for {email}.").format(role=role.name, email=user.email), "success") + flash( + _("Role {role} enabled for {email}.").format( + role=role.name, email=user.email + ), + "success", + ) elif action == "disable": if role in user.roles: current_accounts.datastore.remove_role_from_user(user, role) - flash(_("Role {role} disabled for {email}.").format(role=role.name, email=user.email), "success") - + flash( + _("Role {role} disabled for {email}.").format( + role=role.name, email=user.email + ), + "success", + ) + db.session.commit() return redirect(url_for("administration.roles", role_id=role_id)) + + +class RegistrationSettingsView(AdminView): + """Admin view for controlling site registration settings.""" + + name = "registration-settings" + category = "Site Settings" + template = "invenio_theme_iform/administration/registration_settings.html" + url = "/registration-settings" + menu_label = _("Registration Settings") + icon = "setting" + + def get(self): + """Render registration settings page.""" + settings = get_site_settings() + return self.render( + registration_enabled=settings.get("registration_enabled", True) + ) + + def post(self): + """Handle registration settings update.""" + enabled = request.form.get("registration_enabled") == "true" + settings = get_site_settings() + settings["registration_enabled"] = enabled + set_site_settings(settings) + + status_str = _("enabled") if enabled else _("disabled") + flash( + _("Public user registration has been {status}.").format( + status=status_str + ), + "success", + ) + return redirect(url_for("administration.registration-settings")) + + +class UserCreateView(AdminView): + """Admin view for creating users.""" + + name = "create-user" + category = "User management" + template = "invenio_theme_iform/administration/user_create.html" + url = "/create-user" + menu_label = _("Create User") + icon = "user plus" + + def get(self): + """Render user creation page.""" + return self.render() + + def post(self): + """Handle manual user creation.""" + from flask_security.utils import hash_password + from invenio_access.permissions import system_identity + from invenio_users_resources.proxies import current_users_service + + email = request.form.get("email", "").strip() + password = request.form.get("password", "").strip() + confirm_email = request.form.get("confirm_email") == "true" + + if not email or not password: + flash(_("Email and Password are required."), "error") + return redirect(url_for("administration.create-user")) + + if User.query.filter_by(email=email).first(): + flash(_("A user with this email already exists."), "error") + return redirect(url_for("administration.create-user")) + + try: + user_kwargs = { + "email": email, + "password": hash_password(password), + "active": True, + } + if confirm_email: + user_kwargs["confirmed_at"] = datetime.utcnow() + + user = current_accounts.datastore.create_user(**user_kwargs) + db.session.commit() + + # Index the newly created user in search engine + try: + from invenio_search import current_search_client + current_users_service.reindex(system_identity, uids=[user.id]) + current_users_service.indexer.process_bulk_queue() + current_search_client.indices.refresh(index="*") + except Exception as ie: + current_app.logger.warning("Failed to index user %s: %s", user.id, str(ie)) + + flash( + _("User {email} created successfully.").format(email=email), + "success", + ) + except Exception as e: + db.session.rollback() + flash( + _("Failed to create user: {error}").format(error=str(e)), + "error", + ) + + return redirect(url_for("administration.create-user")) diff --git a/invenio_theme_iform/ext.py b/invenio_theme_iform/ext.py index 9a203bc..c76ca2e 100644 --- a/invenio_theme_iform/ext.py +++ b/invenio_theme_iform/ext.py @@ -8,7 +8,7 @@ """invenio module for I-Form theme.""" -from flask import g, has_request_context +from flask import g, has_request_context, request from flask_login import login_required from . import config @@ -32,8 +32,36 @@ class InvenioThemeIform(object): app.register_error_handler(423, locked) + @app.before_request + def block_registration_if_disabled(): + if request.endpoint == "security.register": + from flask import current_app, render_template + from .administration.views import get_site_settings + settings = get_site_settings() + if not settings.get("registration_enabled", True): + admin_email = ( + current_app.config.get("APP_RDM_ADMIN_EMAIL_RECIPIENT") + or current_app.config.get("SUPPORT_EMAIL") + or current_app.config.get("THEME_SUPPORT_EMAIL") + or current_app.config.get("SECURITY_EMAIL_SENDER") + or "info@repo.i-form.ie" + ) + return ( + render_template( + "invenio_theme_iform/accounts/registration_disabled.html", + admin_email=admin_email, + ), + 403, + ) + app.extensions["invenio-theme-iform"] = self + @app.context_processor + def inject_site_settings(): + from .administration.views import get_site_settings + settings = get_site_settings() + return dict(registration_enabled=settings.get("registration_enabled", True)) + def init_config(self, app): """Initialize configuration.""" for k in dir(config): diff --git a/invenio_theme_iform/templates/invenio_theme_iform/accounts/header_login.html b/invenio_theme_iform/templates/invenio_theme_iform/accounts/header_login.html index 62eb2bb..1bc81cc 100644 --- a/invenio_theme_iform/templates/invenio_theme_iform/accounts/header_login.html +++ b/invenio_theme_iform/templates/invenio_theme_iform/accounts/header_login.html @@ -2,7 +2,7 @@ {%- if not current_user.is_authenticated %} - {%- if config.SECURITY_REGISTERABLE %} + {%- if config.SECURITY_REGISTERABLE and registration_enabled %}
{{ _ ("Sign up") }} diff --git a/invenio_theme_iform/templates/invenio_theme_iform/accounts/registration_disabled.html b/invenio_theme_iform/templates/invenio_theme_iform/accounts/registration_disabled.html new file mode 100644 index 0000000..f063975 --- /dev/null +++ b/invenio_theme_iform/templates/invenio_theme_iform/accounts/registration_disabled.html @@ -0,0 +1,42 @@ +{# + Copyright (C) 2024 I-Form. + + invenio-theme-iform is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} +{% extends config.THEME_ERROR_TEMPLATE %} + +{% block message %} + +{% endblock message %} diff --git a/invenio_theme_iform/templates/invenio_theme_iform/administration/registration_settings.html b/invenio_theme_iform/templates/invenio_theme_iform/administration/registration_settings.html new file mode 100644 index 0000000..76a4f1d --- /dev/null +++ b/invenio_theme_iform/templates/invenio_theme_iform/administration/registration_settings.html @@ -0,0 +1,61 @@ +{# + Copyright (C) 2024 I-Form. + + invenio-theme-iform is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} +{% extends "invenio_administration/base.html" %} + +{% block admin_page_content %} +
+
+
+

+ +
+ {{ _("Registration Settings") }} +
{{ _("Enable or disable public user registration on the website.") }}
+
+

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+

{{ _("Public Sign-up Status") }}

+ +

+ {% if registration_enabled %} + {{ _("Public Sign-ups ENABLED") }} + {% else %} + {{ _("Public Sign-ups DISABLED") }} + {% endif %} +

+ +
+ +
+ {% if registration_enabled %} + + + {% else %} + + + {% endif %} +
+
+
+
+
+{% endblock %} diff --git a/invenio_theme_iform/templates/invenio_theme_iform/administration/user_create.html b/invenio_theme_iform/templates/invenio_theme_iform/administration/user_create.html new file mode 100644 index 0000000..2e21fe9 --- /dev/null +++ b/invenio_theme_iform/templates/invenio_theme_iform/administration/user_create.html @@ -0,0 +1,58 @@ +{# + Copyright (C) 2024 I-Form. + + invenio-theme-iform is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} +{% extends "invenio_administration/base.html" %} + +{% block admin_page_content %} +
+
+
+

+ +
+ {{ _("Create User") }} +
{{ _("Manually add a new user account.") }}
+
+

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+
+
+{% endblock %} diff --git a/pyproject.toml b/pyproject.toml index 773353b..b6a45a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,8 @@ invenio_theme_iform = "invenio_theme_iform.ext:finalize_app" [project.entry-points."invenio_administration.views"] invenio_theme_iform_roles = "invenio_theme_iform.administration.views:RoleManagementView" +invenio_theme_iform_registration_settings = "invenio_theme_iform.administration.views:RegistrationSettingsView" +invenio_theme_iform_user_create = "invenio_theme_iform.administration.views:UserCreateView" [dependency-groups]