Compare commits

...

7 Commits

6 changed files with 123 additions and 10 deletions
+32 -1
View File
@@ -50,6 +50,7 @@ import socket
import io
import html
import logging
from logging.handlers import RotatingFileHandler
import secrets
import importlib
try:
@@ -76,10 +77,19 @@ from data_protection import (
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
import settings as cfg
from settings import MongoClient
from tenant import get_tenant_context
app = Flask(__name__, static_folder='static') # Correctly set static folder
app.logger.setLevel(logging.WARNING)
app.logger.setLevel(logging.INFO)
if not os.path.exists(cfg.LOGS_FOLDER):
os.makedirs(cfg.LOGS_FOLDER, exist_ok=True)
log_file_path = os.path.join(cfg.LOGS_FOLDER, 'application.log')
file_handler = RotatingFileHandler(log_file_path, maxBytes=10 * 1024 * 1024, backupCount=5, encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s'))
app.logger.handlers = []
app.logger.addHandler(file_handler)
app.secret_key = cfg.SECRET_KEY
app.debug = cfg.DEBUG
app.config['UPLOAD_FOLDER'] = cfg.UPLOAD_FOLDER
@@ -1092,6 +1102,15 @@ def inject_version():
client.close()
current_module = _get_current_module(request.path)
current_tenant_db = MONGODB_DB
current_tenant_id = None
try:
ctx = get_tenant_context()
if ctx:
current_tenant_db = ctx.db_name or current_tenant_db
current_tenant_id = ctx.tenant_id
except Exception:
current_tenant_db = MONGODB_DB
return {
'APP_VERSION': APP_VERSION,
@@ -1104,6 +1123,8 @@ def inject_version():
'is_admin': is_admin,
'unread_notification_count': unread_notification_count,
'current_permissions': current_permissions,
'current_tenant_db': current_tenant_db,
'current_tenant_id': current_tenant_id,
'permission_action_options': PERMISSION_ACTION_OPTIONS,
'permission_page_options': PERMISSION_PAGE_OPTIONS,
'permission_presets': us.get_permission_preset_definitions(),
@@ -4036,13 +4057,22 @@ def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
ctx = get_tenant_context()
current_tenant_id = ctx.tenant_id if ctx else None
current_tenant_db = ctx.db_name if ctx else cfg.MONGODB_DB
app.logger.info(f"Login attempt: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
app.logger.info(f"Debug login context: headers={dict(request.headers)} tenant_config={ctx.config if ctx else None} remote_addr={request.remote_addr} host={request.host}")
app.logger.info(f"Raw login payload: username={username!r} password={password!r}")
app.logger.info(f"Active MongoDB config: uri={getattr(cfg, 'MONGODB_URI', None)!r} host={cfg.MONGODB_HOST!r} port={cfg.MONGODB_PORT!r} default_db={cfg.MONGODB_DB!r}")
if not username or not password:
app.logger.warning(f"Login blocked: missing credentials tenant={current_tenant_id or 'default'} host={request.host} ip={request.remote_addr}")
flash('Bitte alle Felder ausfüllen', 'error')
return redirect(url_for('login'))
user = us.check_nm_pwd(username, password)
if user:
app.logger.info(f"Login success: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
session['username'] = username
is_admin_user = bool(user.get('Admin', False))
session['admin'] = is_admin_user
@@ -4063,6 +4093,7 @@ def login():
else:
return redirect(url_for('home'))
else:
app.logger.warning(f"Login failed: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
flash('Ungültige Anmeldedaten', 'error')
get_flashed_messages()
return render_template('login.html')
+7 -1
View File
@@ -131,11 +131,15 @@ PORT = _get(_conf, ['port'], DEFAULTS['port'])
MONGODB_HOST = _get(_conf, ['mongodb', 'host'], DEFAULTS['mongodb']['host'])
MONGODB_PORT = _get(_conf, ['mongodb', 'port'], DEFAULTS['mongodb']['port'])
MONGODB_DB = _get(_conf, ['mongodb', 'db'], DEFAULTS['mongodb']['db'])
MONGODB_URI = _get(_conf, ['mongodb', 'uri'], '')
# Optional environment overrides for containerized/runtime deployments.
MONGODB_HOST = os.getenv('INVENTAR_MONGODB_HOST', MONGODB_HOST)
MONGODB_PORT = int(os.getenv('INVENTAR_MONGODB_PORT', str(MONGODB_PORT)))
MONGODB_DB = os.getenv('INVENTAR_MONGODB_DB', MONGODB_DB)
MONGODB_URI = os.getenv('INVENTAR_MONGODB_URI', os.getenv('MONGO_URI', MONGODB_URI))
if isinstance(MONGODB_URI, str):
MONGODB_URI = MONGODB_URI.strip() or None
MONGODB_MAX_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MAX_POOL_SIZE', 20)
MONGODB_MIN_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MIN_POOL_SIZE', 0)
MONGODB_MAX_IDLE_TIME_MS = _get_int_env('INVENTAR_MONGODB_MAX_IDLE_TIME_MS', 300000)
@@ -318,7 +322,9 @@ def MongoClient(*args, **kwargs):
}
client_kwargs.update(kwargs)
if len(args) >= 2 and not explicit_host and not explicit_port:
if MONGODB_URI and len(args) == 0 and not explicit_host and not explicit_port:
mongo_args = (MONGODB_URI,)
elif len(args) >= 2 and not explicit_host and not explicit_port:
mongo_args = args
else:
mongo_args = (host, port)
+15 -1
View File
@@ -191,6 +191,17 @@
white-space: nowrap;
flex-shrink: 0;
}
.navbar-text.tenant-badge {
font-size: 0.82rem;
color: rgba(255,255,255,0.9);
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.18);
padding: 0.25rem 0.55rem;
border-radius: 999px;
white-space: nowrap;
margin-right: 0.75rem;
}
.navbar-brand::before {
content: attr(data-icon);
@@ -1113,7 +1124,10 @@
<button class="function-search-btn" type="submit">Los</button>
</form>
</div>
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
{% if current_tenant_db %}
<span class="navbar-text tenant-badge" title="Aktive Tenant-Datenbank">{{ current_tenant_db }}</span>
{% endif %}
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
<div class="dropdown me-2 user-menu-wrap">
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="invUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
👤
+9
View File
@@ -14,6 +14,15 @@
<div class="row justify-content-center">
<div class="col-md-6">
<h2 class="text-center">Login</h2>
{% if current_tenant_id or current_tenant_db %}
<div class="alert alert-info text-center small mb-4" role="alert">
{% if current_tenant_id %}
Aktiver Tenant: <strong>{{ current_tenant_id }}</strong> &middot; Datenbank: <strong>{{ current_tenant_db }}</strong>
{% else %}
Aktive Datenbank: <strong>{{ current_tenant_db }}</strong>
{% endif %}
</div>
{% endif %}
<form method="POST" action="{{ url_for('login') }}">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
+9
View File
@@ -123,12 +123,17 @@ class TenantContext:
host = request.host.lower()
_, port = _parse_port_from_host(host)
self.port = port
logger.info(f"Tenant resolution start: request.host={host} request.headers={dict(request.headers)}")
if port:
tenant_from_port = _tenant_id_for_port(port)
if tenant_from_port:
self.tenant_id = tenant_from_port
self.config = get_tenant_config(tenant_from_port)
logger.info(
f"Tenant resolution by port: host={host} port={port} tenant={tenant_from_port} config={self.config}"
)
return self._get_db_name(tenant_from_port)
logger.info(f"Tenant port not mapped: host={host} port={port}")
# Priority 3: Subdomain extraction
parts = host.split('.')
@@ -144,7 +149,11 @@ class TenantContext:
self.subdomain = potential_subdomain
self.tenant_id = potential_subdomain
self.config = get_tenant_config(potential_subdomain)
logger.info(
f"Tenant resolution by subdomain: host={host} tenant={potential_subdomain} config={self.config}"
)
return self._get_db_name(potential_subdomain)
logger.info(f"Tenant subdomain ignored: {potential_subdomain}")
# Fallback to default tenant if no tenant identifier found.
# If no explicit 'default' tenant config exists, use configured MongoDB DB.
+51 -7
View File
@@ -12,6 +12,7 @@ Provides methods for creating, validating, and retrieving user information.
'''
import hashlib
import copy
import logging
import re
import secrets
import string
@@ -19,6 +20,9 @@ from bson.objectid import ObjectId
import settings as cfg
from settings import MongoClient
logger = logging.getLogger('app')
logger.setLevel(logging.INFO)
def normalize_student_card_id(card_id):
"""Normalize student card IDs for reliable lookup."""
@@ -421,25 +425,65 @@ def check_nm_pwd(username, password):
"""
db_name = cfg.MONGODB_DB
tenant_db = None
ctx = None
try:
from tenant import get_tenant_context
ctx = get_tenant_context()
if ctx and ctx.tenant_id:
tenant_db = ctx.db_name or ctx.resolve_tenant()
db_name = tenant_db
except Exception:
pass
except Exception as exc:
logger.exception(f"Failed to resolve tenant context in check_nm_pwd: {exc}")
logger.info(
"check_nm_pwd start: username=%r tenant=%r db=%r host=%r port=%r uri=%r",
username,
ctx.tenant_id if ctx else None,
db_name,
cfg.MONGODB_HOST,
cfg.MONGODB_PORT,
getattr(cfg, 'MONGODB_URI', None),
)
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try:
hashed_password = hashing(password)
logger.info("check_nm_pwd password hash for username=%r: %s", username, hashed_password)
def find_user_in_db(database_name):
db = client[database_name]
users = db['users']
return users.find_one({'Username': username, 'Password': hashed_password}) or users.find_one({'username': username, 'Password': hashed_password})
db = client[db_name]
users = db['users']
user_record = users.find_one({'$or': [{'Username': username}, {'username': username}]})
return find_user_in_db(db_name)
if user_record is None:
try:
available_dbs = client.list_database_names()
logger.warning(
"No user document in tenant database %r for username=%r. Available databases=%s",
db_name,
username,
available_dbs,
)
except Exception as exc:
logger.exception("Could not list databases during failed login check: %s", exc)
return None
logger.info("Found user document for username=%r in db=%r: %s", username, db_name, user_record)
stored_password = user_record.get('Password') or user_record.get('password')
if stored_password is None:
logger.warning("User document for username=%r in db=%r has no password field", username, db_name)
return None
if stored_password != hashed_password:
logger.warning(
"Password mismatch for username=%r in db=%r: provided_hash=%s stored_hash=%s",
username,
db_name,
hashed_password,
stored_password,
)
return None
return user_record
finally:
client.close()