Files
Community_Nursing_Atlas/server/auth.py
T
jsahn 4e943700fc feat: add common utilities and feedback handling scripts
- Introduced common.js for shared functions including API calls, error reporting, and UI helpers.
- Implemented feedback.js for the feedback reporting page, including form handling and displaying feedback status.
- Added layout.js for managing galaxy and section layouts in the visualization.
- Created login.js for user authentication with session management.
- Developed search.js for term searching based on user input and synonyms.
- Implemented signup.js for user registration with validation and immediate login.
2026-10-06 23:14:45 +09:00

179 lines
5.6 KiB
Python

"""인증.
- 교수자(owner): admins 테이블의 아이디·비밀번호 계정. 용어·관계·문제 수정, 회원·반 관리, 설정.
처음 실행할 때 아이디 ATLAS_OWNER_USERNAME(기본 professor), 비밀번호 ATLAS_OWNER_PASSWORD 또는
data/.owner_password 로 계정을 만든다. 이후 비밀번호는 DB에 해시로만 저장된다.
- 회원(member): 학생 계정. 교수자가 승인하면 사례·문제를 볼 수 있다. 학생·방문자는 오류 신고만 할 수 있다.
모두 서명된 쿠키 세션을 쓴다.
"""
import base64
import hashlib
import hmac
import os
import secrets
import time
from fastapi import HTTPException, Request
from .db import DB_PATH
DATA = DB_PATH.parent
COOKIE = "atlas_admin"
MEMBER_COOKIE = "atlas_member"
TTL = 60 * 60 * 24 * 14
def _read_or_create(path, factory):
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists():
return path.read_text().strip()
v = factory()
path.write_text(v)
try:
os.chmod(path, 0o600)
except OSError:
pass
return v
def secret() -> bytes:
return _read_or_create(DATA / ".secret", lambda: secrets.token_hex(32)).encode()
def owner_password() -> str:
env = os.environ.get("ATLAS_OWNER_PASSWORD")
if env:
return env
return _read_or_create(DATA / ".owner_password", lambda: secrets.token_urlsafe(12))
# ---------------------------------------------------------------- 서명 토큰
def sign(payload: str) -> str:
raw = payload.encode()
sig = hmac.new(secret(), raw, hashlib.sha256).hexdigest()
return base64.urlsafe_b64encode(raw).decode() + "." + sig
def unsign(token: str | None) -> str | None:
if not token or "." not in token:
return None
b64, sig = token.rsplit(".", 1)
try:
raw = base64.urlsafe_b64decode(b64.encode())
except Exception:
return None
if not hmac.compare_digest(hmac.new(secret(), raw, hashlib.sha256).hexdigest(), sig):
return None
return raw.decode()
def make_token(name: str, role: str = "owner") -> str:
return sign(f"{name}|{role}|{int(time.time()) + TTL}")
def read_session(token: str | None) -> tuple[str, str] | None:
"""(이름, 역할) 또는 None. 예전 형식(이름|만료)은 검수자로 본다."""
p = unsign(token)
if not p:
return None
parts = p.rsplit("|", 2)
if len(parts) == 3:
name, role, exp = parts
else:
name, exp = p.rsplit("|", 1)
role = "reviewer"
if not exp.isdigit() or int(exp) < time.time():
return None
return name, role
def read_token(token: str | None) -> str | None:
s = read_session(token)
return s[0] if s else None
def ensure_owner_account(con) -> dict | None:
"""교수자 계정이 없으면 만든다. 새로 만들었으면 {username, password} 를 돌려준다."""
if con.execute("SELECT 1 FROM admins").fetchone():
return None
username = os.environ.get("ATLAS_OWNER_USERNAME", "professor")
pw = owner_password()
with con:
con.execute("INSERT INTO admins(username,password_hash,name,created_at) VALUES(?,?,?,?)",
(username, hash_password(pw), "교수자", time.strftime("%Y-%m-%dT%H:%M:%S")))
# 평문 비밀번호 파일은 계정을 만든 뒤 지운다 (이후엔 DB의 해시만 사용)
f = DATA / ".owner_password"
if not os.environ.get("ATLAS_OWNER_PASSWORD") and f.exists():
f.unlink()
return {"username": username, "password": pw}
def check_login(con, username: str, pw: str) -> dict | None:
"""교수자 아이디·비밀번호가 맞으면 계정 행"""
r = con.execute("SELECT * FROM admins WHERE username=?", (username.strip(),)).fetchone()
if r and verify_password(pw, r["password_hash"]):
return dict(r)
return None
def current_admin(request: Request) -> str | None:
"""교수자로 로그인했으면 이름 (예전 검수자 세션은 인정하지 않음)."""
s = read_session(request.cookies.get(COOKIE))
return s[0] if s and s[1] == "owner" else None
def current_role(request: Request) -> str | None:
s = read_session(request.cookies.get(COOKIE))
return s[1] if s else None
def require_admin(request: Request) -> str:
name = current_admin(request)
if not name:
raise HTTPException(401, "교수자 로그인이 필요합니다.")
return name
def require_owner(request: Request) -> str:
s = read_session(request.cookies.get(COOKIE))
if not s:
raise HTTPException(401, "관리자 로그인이 필요합니다.")
if s[1] != "owner":
raise HTTPException(401, "교수자 로그인이 필요합니다.")
return s[0]
# ---------------------------------------------------------------- 회원 비밀번호·세션
def hash_password(pw: str) -> str:
salt = os.urandom(16)
h = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt, 200_000)
return f"pbkdf2${salt.hex()}${h.hex()}"
def verify_password(pw: str, stored: str | None) -> bool:
try:
_, salt, h = (stored or "").split("$")
except ValueError:
return False
calc = hashlib.pbkdf2_hmac("sha256", pw.encode(), bytes.fromhex(salt), 200_000).hex()
return hmac.compare_digest(calc, h)
def make_member_token(user_id: int) -> str:
return sign(f"m|{user_id}|{int(time.time()) + TTL}")
def member_id(request: Request) -> int | None:
p = unsign(request.cookies.get(MEMBER_COOKIE))
if not p:
return None
try:
tag, uid, exp = p.split("|")
except ValueError:
return None
if tag != "m" or int(exp) < time.time():
return None
return int(uid)