feat: implement Phase 0 auth system, setup wizard, and Docker config
- Add users table to migration, User ORM model - Implement JWT+TOTP auth API (login, refresh, me, change-password) - Add first-run setup wizard with rate-limited admin creation, TOTP QR enrollment (secret saved only after verification), and NAS path verification — served as Jinja2 single-page HTML - Add setup redirect middleware (bypasses /health, /docs, /openapi.json) - Mount config.yaml, scripts, logs volumes in docker-compose - Route API vs frontend traffic in Caddyfile - Include admin seed script as CLI fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
78
scripts/seed_admin.py
Normal file
78
scripts/seed_admin.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""초기 관리자 계정 생성 스크립트
|
||||
|
||||
사용법:
|
||||
# Docker 컨테이너 내부에서 실행
|
||||
docker compose exec fastapi python /app/scripts/seed_admin.py
|
||||
|
||||
# 로컬에서 실행 (DATABASE_URL 환경변수 필요)
|
||||
python scripts/seed_admin.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 프로젝트 루트의 app/ 디렉토리를 경로에 추가
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))
|
||||
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from core.auth import hash_password
|
||||
|
||||
|
||||
async def seed_admin():
|
||||
database_url = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql+asyncpg://pkm:pkm@localhost:5432/pkm",
|
||||
)
|
||||
|
||||
engine = create_async_engine(database_url)
|
||||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
print("=== hyungi_Document_Server 관리자 계정 생성 ===\n")
|
||||
|
||||
username = input("관리자 아이디 [admin]: ").strip() or "admin"
|
||||
password = getpass.getpass("비밀번호: ")
|
||||
if not password:
|
||||
print("비밀번호는 필수입니다.")
|
||||
return
|
||||
|
||||
password_confirm = getpass.getpass("비밀번호 확인: ")
|
||||
if password != password_confirm:
|
||||
print("비밀번호가 일치하지 않습니다.")
|
||||
return
|
||||
|
||||
password_hash = hash_password(password)
|
||||
|
||||
async with async_session() as session:
|
||||
# 이미 존재하는지 확인
|
||||
result = await session.execute(
|
||||
text("SELECT id FROM users WHERE username = :username"),
|
||||
{"username": username},
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
print(f"'{username}' 계정이 이미 존재합니다. 비밀번호를 업데이트합니다.")
|
||||
await session.execute(
|
||||
text("UPDATE users SET password_hash = :hash WHERE username = :username"),
|
||||
{"hash": password_hash, "username": username},
|
||||
)
|
||||
else:
|
||||
await session.execute(
|
||||
text(
|
||||
"INSERT INTO users (username, password_hash, is_active) "
|
||||
"VALUES (:username, :hash, TRUE)"
|
||||
),
|
||||
{"username": username, "hash": password_hash},
|
||||
)
|
||||
print(f"'{username}' 계정이 생성되었습니다.")
|
||||
|
||||
await session.commit()
|
||||
|
||||
await engine.dispose()
|
||||
print("\n완료!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_admin())
|
||||
Reference in New Issue
Block a user