IT Assets: add role-based auth system and portal user sync

Auth:
- Fix local login (was redirecting to portal; now authenticates AdminUser directly)
- Portal SSO still takes priority in production via nginx headers

Role system (admin | editor | readonly):
- New app/utils/decorators.py with editor_required and admin_required decorators
- All write routes protected with editor_required (create/edit/delete/import/mask)
- Settings user management protected with admin_required
- Sidebar hides write-only links for readonly users
- Dashboard quick actions and list page buttons hidden for readonly

Settings page:
- Role colour badges (admin=red, editor=blue, readonly=grey)
- Inline role changer per user (dropdown auto-submit)
- Reset password modal per user
- Delete user button with confirmation
- Add user form includes role selector with legend

Portal user sync:
- New /internal/sync-user endpoint receives user pre-creation from portal
- INTERNAL_SYNC_SECRET added to config
- portal/config.py: added internal_url for itassets app so _sync_user_to_app works
This commit is contained in:
ske087
2026-07-08 21:35:16 +03:00
parent 6034a62b08
commit 7d24e7f527
17 changed files with 320 additions and 29 deletions
@@ -4,6 +4,7 @@ from flask import (Blueprint, render_template, redirect, url_for,
flash, request, current_app, send_from_directory, jsonify)
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from app.utils.decorators import editor_required
from app.extensions import db
from app.models.document_template import DocumentTemplate
from app.models.paperwork import DOC_TYPES
@@ -54,6 +55,7 @@ def index():
# ------------------------------------------------------------------
@bp.route('/upload', methods=['GET', 'POST'])
@login_required
@editor_required
def upload():
if request.method == 'POST':
name = request.form.get('name', '').strip()
@@ -134,6 +136,7 @@ def download(tpl_id):
# ------------------------------------------------------------------
@bp.route('/<int:tpl_id>/rescan', methods=['POST'])
@login_required
@editor_required
def rescan(tpl_id):
tpl = DocumentTemplate.query.get_or_404(tpl_id)
folder = _template_folder(current_app)
@@ -153,6 +156,7 @@ def rescan(tpl_id):
# ------------------------------------------------------------------
@bp.route('/<int:tpl_id>/edit', methods=['GET', 'POST'])
@login_required
@editor_required
def edit(tpl_id):
tpl = DocumentTemplate.query.get_or_404(tpl_id)
if request.method == 'POST':
@@ -170,6 +174,7 @@ def edit(tpl_id):
# ------------------------------------------------------------------
@bp.route('/<int:tpl_id>/delete', methods=['POST'])
@login_required
@editor_required
def delete(tpl_id):
tpl = DocumentTemplate.query.get_or_404(tpl_id)
# Check if any documents were generated from this template