REMOVE LDAP
This commit is contained in:
144
utils/__init__.py
Normal file
144
utils/__init__.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Utility helpers used across the application."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
from email.header import Header
|
||||
from email.mime.text import MIMEText
|
||||
from functools import wraps
|
||||
from typing import Callable, Iterable, List
|
||||
|
||||
from flask import abort, current_app
|
||||
from flask_login import current_user
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Access control decorators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _require_roles(roles: Iterable[str]) -> Callable:
|
||||
"""Abort with 403 when the current user does not own one of the roles."""
|
||||
|
||||
def decorator(view_func: Callable) -> Callable:
|
||||
@wraps(view_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not current_user.is_authenticated or current_user.role not in roles:
|
||||
abort(403)
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def admin_required(view_func: Callable) -> Callable:
|
||||
"""Limit a view to administrator accounts only."""
|
||||
|
||||
return _require_roles({"admin"})(view_func)
|
||||
|
||||
|
||||
def editor_or_admin_required(view_func: Callable) -> Callable:
|
||||
"""Limit a view to editor or administrator accounts."""
|
||||
|
||||
return _require_roles({"editor", "admin"})(view_func)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# History helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def add_history_log(spec_id: int, action: str, details: str = "") -> None:
|
||||
"""Persist a history record for an action executed on a spec."""
|
||||
|
||||
from models import SpecHistory, db
|
||||
entry = SpecHistory(
|
||||
spec_id=spec_id,
|
||||
user_id=current_user.id if current_user.is_authenticated else None,
|
||||
action=action,
|
||||
details=details,
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notification helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def process_recipients(recipients_str: str | None) -> List[str]:
|
||||
"""Convert a semicolon separated string into a list of unique addresses."""
|
||||
|
||||
if not recipients_str:
|
||||
return []
|
||||
|
||||
normalized = recipients_str.replace("\r", ";").replace("\n", ";")
|
||||
candidates = [item.strip() for item in normalized.split(";") if item.strip()]
|
||||
|
||||
seen: set[str] = set()
|
||||
result: List[str] = []
|
||||
for email in candidates:
|
||||
if email not in seen:
|
||||
seen.add(email)
|
||||
result.append(email)
|
||||
return result
|
||||
|
||||
|
||||
def send_email(to_addrs: Iterable[str], subject: str, body: str) -> bool:
|
||||
"""Send an HTML email using the SMTP configuration in Flask config."""
|
||||
|
||||
recipients = list(to_addrs)
|
||||
if not recipients:
|
||||
logger.info("send_email skipped: no recipients provided")
|
||||
return False
|
||||
|
||||
cfg = current_app.config
|
||||
smtp_server = cfg.get("SMTP_SERVER")
|
||||
smtp_port = int(cfg.get("SMTP_PORT", 25))
|
||||
use_tls = bool(cfg.get("SMTP_USE_TLS", False))
|
||||
use_ssl = bool(cfg.get("SMTP_USE_SSL", False))
|
||||
sender = cfg.get("SMTP_SENDER_EMAIL")
|
||||
password = cfg.get("SMTP_SENDER_PASSWORD", "")
|
||||
auth_required = bool(cfg.get("SMTP_AUTH_REQUIRED", False))
|
||||
|
||||
logger.debug(
|
||||
"Preparing email: subject=%s recipients=%s server=%s:%s use_tls=%s use_ssl=%s auth_required=%s",
|
||||
subject,
|
||||
recipients,
|
||||
smtp_server,
|
||||
smtp_port,
|
||||
use_tls,
|
||||
use_ssl,
|
||||
auth_required,
|
||||
)
|
||||
|
||||
message = MIMEText(body, "html", "utf-8")
|
||||
message["Subject"] = Header(subject, "utf-8")
|
||||
message["From"] = sender
|
||||
message["To"] = ", ".join(recipients)
|
||||
|
||||
try:
|
||||
if use_ssl and smtp_port == 465:
|
||||
server = smtplib.SMTP_SSL(smtp_server, smtp_port)
|
||||
else:
|
||||
server = smtplib.SMTP(smtp_server, smtp_port)
|
||||
|
||||
if use_tls and smtp_port == 587:
|
||||
server.starttls()
|
||||
|
||||
if auth_required and password:
|
||||
server.login(sender, password)
|
||||
|
||||
server.sendmail(sender, recipients, message.as_string())
|
||||
server.quit()
|
||||
logger.info("Email sent to %s (subject=%s)", recipients, subject)
|
||||
return True
|
||||
|
||||
except smtplib.SMTPException as exc:
|
||||
logger.error("SMTP error while sending email: %s", exc)
|
||||
except Exception as exc: # pragma: no cover
|
||||
logger.exception("Unexpected error while sending email: %s", exc)
|
||||
|
||||
return False
|
60
utils/timezone.py
Normal file
60
utils/timezone.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Timezone utilities used by the temp spec application."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, date, time, timedelta, timezone
|
||||
|
||||
TAIWAN_TZ = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def now_taiwan() -> datetime:
|
||||
"""Return current datetime in Taiwan time (aware)."""
|
||||
return datetime.now(TAIWAN_TZ)
|
||||
|
||||
|
||||
def now_utc() -> datetime:
|
||||
"""Return current datetime in UTC (aware)."""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def taiwan_now() -> datetime:
|
||||
"""Return current datetime in Taiwan time (naive)."""
|
||||
return now_taiwan().replace(tzinfo=None)
|
||||
|
||||
|
||||
def to_taiwan_time(dt: datetime | date | None) -> datetime | None:
|
||||
"""Convert a datetime/date (naive or aware) to Taiwan time (aware)."""
|
||||
if dt is None:
|
||||
return None
|
||||
# Promote date to datetime
|
||||
if isinstance(dt, date) and not isinstance(dt, datetime):
|
||||
dt = datetime.combine(dt, time.min)
|
||||
# Treat naive timestamps as UTC
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(TAIWAN_TZ)
|
||||
|
||||
|
||||
def to_utc_time(dt: datetime | date | None) -> datetime | None:
|
||||
"""Convert a datetime/date (naive or aware) to UTC (aware)."""
|
||||
if dt is None:
|
||||
return None
|
||||
if isinstance(dt, date) and not isinstance(dt, datetime):
|
||||
dt = datetime.combine(dt, time.min).replace(tzinfo=TAIWAN_TZ)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=TAIWAN_TZ)
|
||||
return dt.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def format_taiwan_time(dt: datetime | date | None, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
|
||||
"""Format a datetime/date using Taiwan time."""
|
||||
if dt is None:
|
||||
return ""
|
||||
tpe_dt = to_taiwan_time(dt)
|
||||
return tpe_dt.strftime(format_str)
|
||||
|
||||
|
||||
def parse_taiwan_time(time_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> datetime:
|
||||
"""Parse a string as Taiwan time (aware)."""
|
||||
naive_dt = datetime.strptime(time_str, format_str)
|
||||
return naive_dt.replace(tzinfo=TAIWAN_TZ)
|
Reference in New Issue
Block a user