REMOVE LDAP

This commit is contained in:
beabigegg
2025-09-25 08:44:44 +08:00
commit 333a640a3b
53 changed files with 4231 additions and 0 deletions

144
utils/__init__.py Normal file
View 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
View 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)