# backend/routes/admin.py from __future__ import annotations import logging from functools import wraps from typing import Callable from flask import ( Blueprint, render_template, redirect, url_for, flash, abort, request, current_app, ) from flask_login import login_required, current_user from backend.models.user import User, db admin_bp = Blueprint("admin", __name__) # Blueprint 등록 def register_admin_routes(app): app.register_blueprint(admin_bp) # 관리자 권한 데코레이터 def admin_required(view_func: Callable): @wraps(view_func) def wrapper(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for("auth.login")) if not getattr(current_user, "is_admin", False): flash("관리자 권한이 필요합니다.", "danger") return redirect(url_for("main.index")) return view_func(*args, **kwargs) return wrapper # 관리자 대시보드 @admin_bp.route("/admin", methods=["GET"]) @login_required @admin_required def admin_panel(): users = db.session.query(User).order_by(User.id.asc()).all() return render_template("admin.html", users=users) # 사용자 승인 @admin_bp.route("/admin/approve/", methods=["GET"]) @login_required @admin_required def approve_user(user_id: int): user = db.session.get(User, user_id) if not user: abort(404) user.is_active = True db.session.commit() flash("사용자가 승인되었습니다.", "success") logging.info("✅ 승인된 사용자: %s (id=%s)", user.username, user.id) return redirect(url_for("admin.admin_panel")) # 사용자 삭제 @admin_bp.route("/admin/delete/", methods=["GET"]) @login_required @admin_required def delete_user(user_id: int): user = db.session.get(User, user_id) if not user: abort(404) username = user.username db.session.delete(user) db.session.commit() flash("사용자가 삭제되었습니다.", "success") logging.info("🗑 삭제된 사용자: %s (id=%s)", username, user_id) return redirect(url_for("admin.admin_panel")) # ▼▼▼ 사용자 비밀번호 변경(관리자용) ▼▼▼ @admin_bp.route("/admin/users//reset_password", methods=["POST"]) @login_required @admin_required def reset_password(user_id: int): """ admin.html에서 각 사용자 행 아래 폼으로부터 POST: - name="new_password" - name="confirm_password" CSRF는 템플릿에서 {{ csrf_token() }} 또는 {{ form.hidden_tag() }}로 포함되어야 합니다. """ new_pw = (request.form.get("new_password") or "").strip() confirm = (request.form.get("confirm_password") or "").strip() # 서버측 검증 if not new_pw or not confirm: flash("비밀번호와 확인 값을 모두 입력하세요.", "warning") return redirect(url_for("admin.admin_panel")) if new_pw != confirm: flash("비밀번호 확인이 일치하지 않습니다.", "warning") return redirect(url_for("admin.admin_panel")) if len(new_pw) < 8: flash("비밀번호는 최소 8자 이상이어야 합니다.", "warning") return redirect(url_for("admin.admin_panel")) user = db.session.get(User, user_id) if not user: abort(404) try: # passlib(Argon2id) 기반 set_password 사용 (models.user에 구현됨) user.set_password(new_pw) db.session.commit() flash(f"사용자(ID={user.id}) 비밀번호를 변경했습니다.", "success") current_app.logger.info( "ADMIN: reset password for user_id=%s by admin_id=%s", user.id, current_user.id ) except Exception as e: db.session.rollback() current_app.logger.exception("ADMIN: reset password failed: %s", e) flash("비밀번호 변경 중 오류가 발생했습니다.", "danger") return redirect(url_for("admin.admin_panel"))