127 lines
3.9 KiB
Python
127 lines
3.9 KiB
Python
# 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/<int:user_id>", 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/<int:user_id>", 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/<int:user_id>/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"))
|