From 06b2152331429aa7f16dd143096187cf75e4e3bc Mon Sep 17 00:00:00 2001 From: ske087 Date: Wed, 8 Jul 2026 21:54:08 +0300 Subject: [PATCH] IT Assets: add portal sync button + wire env vars in start-dev.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Portal: add /api/internal/itassets-users endpoint (returns all portal users with itassets access + their effective role) - IT Assets: add PORTAL_INTERNAL_URL config key - IT Assets: add POST /settings/sync-from-portal route (admin only) — pulls users from the portal API and upserts them locally - Settings page: 'Sync from Portal' button next to the users table header - start-dev.sh: pass INTERNAL_SYNC_SECRET and ITASSETS_INTERNAL_URL to portal; pass PORTAL_INTERNAL_URL and INTERNAL_SYNC_SECRET to itassets - Also includes 'Back to Portal' icon button added to sidebar footer --- IT_asset_management/app/routes/settings.py | 61 +++++++++++++++++++ IT_asset_management/app/templates/base.html | 5 +- .../app/templates/settings/index.html | 11 +++- IT_asset_management/config.py | 3 + portal/app/routes/api.py | 32 ++++++++++ start-dev.sh | 4 ++ 6 files changed, 113 insertions(+), 3 deletions(-) diff --git a/IT_asset_management/app/routes/settings.py b/IT_asset_management/app/routes/settings.py index c5e31e2..07188cf 100644 --- a/IT_asset_management/app/routes/settings.py +++ b/IT_asset_management/app/routes/settings.py @@ -3,6 +3,7 @@ from flask_login import login_required, current_user from app.extensions import db from app.models.admin_user import AdminUser from app.utils.decorators import admin_required +from app.utils.portal_sso import _portal_role_to_local bp = Blueprint('settings', __name__, url_prefix='/settings') @@ -104,3 +105,63 @@ def delete_admin(admin_id): db.session.commit() flash(f'User "{username}" deleted.', 'success') return redirect(url_for('settings.index')) + + +@bp.route('/sync-from-portal', methods=['POST']) +@login_required +@admin_required +def sync_from_portal(): + """Pull all portal users with itassets access and upsert them locally.""" + import urllib.request + import json as _json + + portal_url = current_app.config.get('PORTAL_INTERNAL_URL', 'http://localhost:5001') + secret = current_app.config.get('INTERNAL_SYNC_SECRET', '') + url = portal_url.rstrip('/') + '/api/internal/itassets-users' + + try: + req = urllib.request.Request( + url, + headers={'X-Internal-Token': secret}, + method='GET', + ) + with urllib.request.urlopen(req, timeout=5) as resp: + users = _json.loads(resp.read()) + except Exception as exc: + flash(f'Could not reach portal: {exc}', 'danger') + return redirect(url_for('settings.index')) + + created = updated = 0 + for u in users: + username = u.get('username', '').strip() + email = u.get('email', '') or f'{username}@portal.local' + local_role = _portal_role_to_local(u.get('app_role', 'standard')) + if not username: + continue + existing = AdminUser.query.filter_by(username=username).first() + if existing: + changed = False + if existing.role != local_role: + existing.role = local_role + changed = True + if existing.email != email: + existing.email = email + changed = True + if changed: + updated += 1 + else: + import secrets as _sec + new_user = AdminUser( + username=username, + email=email, + full_name=username, + role=local_role, + is_active=True, + ) + new_user.set_password(_sec.token_hex(32)) + db.session.add(new_user) + created += 1 + + db.session.commit() + flash(f'Portal sync complete — {created} created, {updated} updated.', 'success') + return redirect(url_for('settings.index')) diff --git a/IT_asset_management/app/templates/base.html b/IT_asset_management/app/templates/base.html index f418508..34df11f 100644 --- a/IT_asset_management/app/templates/base.html +++ b/IT_asset_management/app/templates/base.html @@ -190,7 +190,10 @@ {{ current_user.role }} - + + + + diff --git a/IT_asset_management/app/templates/settings/index.html b/IT_asset_management/app/templates/settings/index.html index c74dbff..a6aa78f 100644 --- a/IT_asset_management/app/templates/settings/index.html +++ b/IT_asset_management/app/templates/settings/index.html @@ -14,8 +14,15 @@
-
- Application Users +
+ Application Users + {% if current_user.is_admin %} +
+ +
+ {% endif %}
diff --git a/IT_asset_management/config.py b/IT_asset_management/config.py index 49a6b73..cf9e423 100644 --- a/IT_asset_management/config.py +++ b/IT_asset_management/config.py @@ -57,6 +57,9 @@ class Config: # Internal service-to-service sync secret — must match portal's INTERNAL_SYNC_SECRET INTERNAL_SYNC_SECRET = os.environ.get('INTERNAL_SYNC_SECRET', 'change-this-internal-secret') + # URL to reach the portal directly (bypassing nginx) for internal sync pulls + PORTAL_INTERNAL_URL = os.environ.get('PORTAL_INTERNAL_URL', 'http://localhost:5001') + # Pagination ITEMS_PER_PAGE = int(os.environ.get('ITEMS_PER_PAGE', 25)) diff --git a/portal/app/routes/api.py b/portal/app/routes/api.py index 981efeb..6dbd2ee 100644 --- a/portal/app/routes/api.py +++ b/portal/app/routes/api.py @@ -106,3 +106,35 @@ def nv_users_internal(): 'nv_role': nv_access.app_role or u.role, # per-app override or portal role }) return jsonify(result) + + +@bp.route('/internal/itassets-users') +def itassets_users_internal(): + """ + Internal endpoint for the IT Asset Management app to pull all portal users + that have itassets access, with their effective role. + Protected by X-Internal-Token header (shared INTERNAL_SYNC_SECRET). + """ + secret = current_app.config.get('INTERNAL_SYNC_SECRET', '') + provided = request.headers.get('X-Internal-Token', '') + if not secret or not provided or not _secrets.compare_digest(provided, secret): + return jsonify({'error': 'forbidden'}), 403 + + from app.models.user import PortalUser + from app.models.app_access import AppAccess + + users = PortalUser.query.filter_by(is_active=True).order_by(PortalUser.username).all() + result = [] + for u in users: + access = AppAccess.query.filter_by( + user_id=u.id, app_name='itassets', is_active=True + ).first() + if access is None: + continue + result.append({ + 'username': u.username, + 'email': u.email, + 'portal_role': u.role, + 'app_role': access.app_role or u.role, + }) + return jsonify(result) diff --git a/start-dev.sh b/start-dev.sh index f75e3ba..ccfe4a2 100755 --- a/start-dev.sh +++ b/start-dev.sh @@ -383,6 +383,8 @@ start_bg "portal" \ env FLASK_ENV=development \ DATA_DIR="$ROOT/portal/data" \ PORT=$PORTAL_PORT \ + INTERNAL_SYNC_SECRET="${INTERNAL_SYNC_SECRET:-change-this-internal-secret}" \ + ITASSETS_INTERNAL_URL="http://127.0.0.1:${ITASSETS_PORT}" \ "$ROOT/portal/.venv/bin/python" "$ROOT/portal/run.py" # DigiServer @@ -414,6 +416,8 @@ if module_enabled "itassets"; then PORTAL_JWT_SECRET=change-this-jwt-secret-in-production \ PORTAL_LOGIN_URL="http://localhost:${NGINX_PORT}/login" \ PORTAL_LOGOUT_URL="http://localhost:${NGINX_PORT}/logout" \ + PORTAL_INTERNAL_URL="http://127.0.0.1:${PORTAL_PORT}" \ + INTERNAL_SYNC_SECRET="${INTERNAL_SYNC_SECRET:-change-this-internal-secret}" \ FLASK_APP=run.py \ "$ROOT/IT_asset_management/.venv/bin/python" "$ROOT/IT_asset_management/run.py" else