Initial commit
This commit is contained in:
180
backend/routes/auth.py
Normal file
180
backend/routes/auth.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# backend/routes/auth.py
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
redirect,
|
||||
url_for,
|
||||
flash,
|
||||
request,
|
||||
session,
|
||||
current_app,
|
||||
)
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
|
||||
from backend.forms.auth_forms import RegistrationForm, LoginForm
|
||||
from backend.models.user import User, db
|
||||
|
||||
# ── (선택) Telegram: 미설정이면 조용히 패스
|
||||
try:
|
||||
from telegram import Bot
|
||||
from telegram.constants import ParseMode
|
||||
except Exception: # 라이브러리 미설치/미사용 환경
|
||||
Bot = None
|
||||
ParseMode = None
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 유틸
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
def _is_safe_url(target: str) -> bool:
|
||||
"""로그인 후 next 파라미터의 안전성 확인(동일 호스트만 허용)."""
|
||||
ref = urlparse(request.host_url)
|
||||
test = urlparse(urljoin(request.host_url, target))
|
||||
return (test.scheme in ("http", "https")) and (ref.netloc == test.netloc)
|
||||
|
||||
|
||||
def _notify(text: str) -> None:
|
||||
"""텔레그램 알림 (설정 없으면 바로 return)."""
|
||||
token = (current_app.config.get("TELEGRAM_BOT_TOKEN") or "").strip()
|
||||
chat_id = (current_app.config.get("TELEGRAM_CHAT_ID") or "").strip()
|
||||
if not (token and chat_id and Bot and ParseMode):
|
||||
return
|
||||
|
||||
def _send():
|
||||
try:
|
||||
bot = Bot(token=token)
|
||||
bot.send_message(chat_id=chat_id, text=text, parse_mode=ParseMode.HTML)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("Telegram send failed: %s", e)
|
||||
|
||||
threading.Thread(target=_send, daemon=True).start()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Blueprint 등록 훅
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
def register_auth_routes(app):
|
||||
"""app.py에서 register_routes(app, socketio) 호출 시 사용."""
|
||||
app.register_blueprint(auth_bp)
|
||||
|
||||
@app.before_request
|
||||
def _touch_session():
|
||||
# 요청마다 세션 갱신(만료 슬라이딩) + 로그아웃 플래그 정리
|
||||
session.modified = True
|
||||
if current_user.is_authenticated and session.get("just_logged_out"):
|
||||
session.pop("just_logged_out", None)
|
||||
flash("세션이 만료되어 자동 로그아웃 되었습니다.", "info")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 회원가입
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@auth_bp.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
current_app.logger.info("REGISTER: already auth → /index")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
# 모델 내부에서 email/username 정규화됨(find_by_*)
|
||||
if User.find_by_email(form.email.data):
|
||||
flash("이미 등록된 이메일입니다.", "warning")
|
||||
current_app.logger.info("REGISTER: dup email %s", form.email.data)
|
||||
return render_template("register.html", form=form)
|
||||
|
||||
if User.find_by_username(form.username.data):
|
||||
flash("이미 사용 중인 사용자명입니다.", "warning")
|
||||
current_app.logger.info("REGISTER: dup username %s", form.username.data)
|
||||
return render_template("register.html", form=form)
|
||||
|
||||
user = User(username=form.username.data, email=form.email.data, is_active=False)
|
||||
user.set_password(form.password.data) # passlib: 기본 Argon2id
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
_notify(
|
||||
f"🆕 <b>신규 가입 요청</b>\n"
|
||||
f"📛 사용자: <code>{user.username}</code>\n"
|
||||
f"📧 이메일: <code>{user.email}</code>"
|
||||
)
|
||||
current_app.logger.info("REGISTER: created id=%s email=%s", user.id, user.email)
|
||||
flash("회원가입이 완료되었습니다. 관리자의 승인을 기다려주세요.", "success")
|
||||
return redirect(url_for("auth.login"))
|
||||
else:
|
||||
if request.method == "POST":
|
||||
current_app.logger.info("REGISTER: form errors=%s", form.errors)
|
||||
|
||||
return render_template("register.html", form=form)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 로그인
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
current_app.logger.info("LOGIN: already auth → /index")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
current_app.logger.info("LOGIN: form ok email=%s", form.email.data)
|
||||
user: Optional[User] = User.find_by_email(form.email.data)
|
||||
|
||||
if not user:
|
||||
flash("이메일 또는 비밀번호가 올바르지 않습니다.", "danger")
|
||||
current_app.logger.info("LOGIN: user not found")
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
pass_ok = user.check_password(form.password.data) # passlib verify(+자동 재해시)
|
||||
current_app.logger.info(
|
||||
"LOGIN: found id=%s active=%s pass_ok=%s",
|
||||
user.id, user.is_active, pass_ok
|
||||
)
|
||||
|
||||
if not pass_ok:
|
||||
flash("이메일 또는 비밀번호가 올바르지 않습니다.", "danger")
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
if not user.is_active:
|
||||
flash("계정이 아직 승인되지 않았습니다.", "warning")
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
# 성공
|
||||
login_user(user, remember=form.remember.data)
|
||||
session.permanent = True
|
||||
_notify(f"🔐 <b>로그인 성공</b>\n👤 <code>{user.username}</code>")
|
||||
current_app.logger.info("LOGIN: SUCCESS → redirect")
|
||||
|
||||
nxt = request.args.get("next")
|
||||
if nxt and _is_safe_url(nxt):
|
||||
return redirect(nxt)
|
||||
return redirect(url_for("main.index"))
|
||||
else:
|
||||
if request.method == "POST":
|
||||
current_app.logger.info("LOGIN: form errors=%s", form.errors)
|
||||
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 로그아웃
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@auth_bp.route("/logout", methods=["GET"])
|
||||
@login_required
|
||||
def logout():
|
||||
if current_user.is_authenticated:
|
||||
current_app.logger.info("LOGOUT: user=%s", current_user.username)
|
||||
logout_user()
|
||||
session["just_logged_out"] = True
|
||||
return redirect(url_for("auth.login"))
|
||||
Reference in New Issue
Block a user