관리자부분 추가
This commit is contained in:
parent
1e7d742b58
commit
28dfc7ad69
20
app/api/admin_auth.py
Normal file
20
app/api/admin_auth.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security_admin import create_admin_access_token, verify_password
|
||||||
|
from app.models.admin_user import AdminUser
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/login")
|
||||||
|
def admin_login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||||
|
# username/password로만 사용 (scope 미사용)
|
||||||
|
admin = db.query(AdminUser).filter(AdminUser.username == form.username).first()
|
||||||
|
if not admin or not admin.is_active or not verify_password(form.password, admin.password_hash):
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||||
|
token = create_admin_access_token(sub=admin.username, minutes=60)
|
||||||
|
admin.last_login_at = datetime.utcnow()
|
||||||
|
db.commit()
|
||||||
|
return {"access_token": token, "token_type": "bearer"}
|
||||||
27
app/api/admin_board.py
Normal file
27
app/api/admin_board.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security_admin import get_current_admin
|
||||||
|
from app.models.board import Board
|
||||||
|
from app.schemas.board import BoardCreate, BoardOut
|
||||||
|
from app.models.admin_user import AdminUser
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("", response_model=list[BoardOut])
|
||||||
|
def list_boards(db: Session = Depends(get_db), _: AdminUser = Depends(get_current_admin)):
|
||||||
|
return db.query(Board).order_by(Board.id.desc()).all()
|
||||||
|
|
||||||
|
@router.post("", response_model=BoardOut, status_code=201)
|
||||||
|
def create_board(payload: BoardCreate, db: Session = Depends(get_db), _: AdminUser = Depends(get_current_admin)):
|
||||||
|
if db.query(Board).filter(Board.code == payload.code).first():
|
||||||
|
raise HTTPException(409, "Board code already exists")
|
||||||
|
b = Board(**payload.model_dump())
|
||||||
|
db.add(b); db.commit(); db.refresh(b)
|
||||||
|
return b
|
||||||
|
|
||||||
|
@router.delete("/{board_id}", status_code=204)
|
||||||
|
def delete_board(board_id: int, db: Session = Depends(get_db), _: AdminUser = Depends(get_current_admin)):
|
||||||
|
b = db.query(Board).get(board_id)
|
||||||
|
if not b: return
|
||||||
|
db.delete(b); db.commit()
|
||||||
37
app/api/admin_members.py
Normal file
37
app/api/admin_members.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security_admin import get_current_admin
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.models.user import User # 기존 일반 사용자 모델
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
class MemberOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
email: EmailStr | None = None
|
||||||
|
created_at: datetime | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
last_login_at: datetime | None = None
|
||||||
|
|
||||||
|
# Pydantic v2: ORM에서 읽기
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
@router.get("", response_model=list[MemberOut])
|
||||||
|
def list_members(db: Session = Depends(get_db), _: User = Depends(get_current_admin)):
|
||||||
|
users = db.query(User).order_by(User.id.desc()).limit(200).all()
|
||||||
|
# 모델 필드가 없을 수 있는 값들은 getattr로 안전하게 매핑
|
||||||
|
result = []
|
||||||
|
for u in users:
|
||||||
|
result.append(MemberOut(
|
||||||
|
id=u.id,
|
||||||
|
username=getattr(u, "username", ""),
|
||||||
|
email=getattr(u, "email", None),
|
||||||
|
created_at=getattr(u, "created_at", None),
|
||||||
|
is_active=getattr(u, "is_active", None),
|
||||||
|
last_login_at=getattr(u, "last_login_at", None),
|
||||||
|
))
|
||||||
|
return result
|
||||||
50
app/api/admin_users.py
Normal file
50
app/api/admin_users.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security_admin import get_current_admin, hash_password
|
||||||
|
from app.models.admin_user import AdminUser
|
||||||
|
from app.schemas.admin_user import AdminUserCreate, AdminUserUpdate, AdminUserOut
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("", response_model=list[AdminUserOut])
|
||||||
|
def list_admin_users(db: Session = Depends(get_db), _: AdminUser = Depends(get_current_admin)):
|
||||||
|
return db.query(AdminUser).order_by(AdminUser.id.desc()).all()
|
||||||
|
|
||||||
|
@router.post("", response_model=AdminUserOut, status_code=201)
|
||||||
|
def create_admin_user(payload: AdminUserCreate, db: Session = Depends(get_db), me: AdminUser = Depends(get_current_admin)):
|
||||||
|
# 슈퍼관리자만 생성 허용
|
||||||
|
if not me.is_superadmin:
|
||||||
|
raise HTTPException(status_code=403, detail="Superadmin required")
|
||||||
|
if db.query(AdminUser).filter(AdminUser.username == payload.username).first():
|
||||||
|
raise HTTPException(status_code=409, detail="Username already exists")
|
||||||
|
user = AdminUser(
|
||||||
|
username=payload.username,
|
||||||
|
password_hash=hash_password(payload.password),
|
||||||
|
name=payload.name,
|
||||||
|
email=payload.email,
|
||||||
|
is_superadmin=payload.is_superadmin,
|
||||||
|
is_active=payload.is_active,
|
||||||
|
)
|
||||||
|
db.add(user); db.commit(); db.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@router.patch("/{user_id}", response_model=AdminUserOut)
|
||||||
|
def update_admin_user(user_id: int, payload: AdminUserUpdate, db: Session = Depends(get_db), me: AdminUser = Depends(get_current_admin)):
|
||||||
|
user = db.query(AdminUser).get(user_id)
|
||||||
|
if not user: raise HTTPException(404, "Not found")
|
||||||
|
if payload.password: user.password_hash = hash_password(payload.password)
|
||||||
|
for f in ("name","email","is_active","is_superadmin"):
|
||||||
|
v = getattr(payload, f)
|
||||||
|
if v is not None:
|
||||||
|
setattr(user, f, v)
|
||||||
|
db.commit(); db.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@router.delete("/{user_id}", status_code=204)
|
||||||
|
def delete_admin_user(user_id: int, db: Session = Depends(get_db), me: AdminUser = Depends(get_current_admin)):
|
||||||
|
if not me.is_superadmin:
|
||||||
|
raise HTTPException(403, "Superadmin required")
|
||||||
|
user = db.query(AdminUser).get(user_id)
|
||||||
|
if not user: return
|
||||||
|
db.delete(user); db.commit()
|
||||||
@ -1,6 +1,8 @@
|
|||||||
# app/core/config.py
|
# app/core/config.py
|
||||||
|
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
database_url: str
|
database_url: str
|
||||||
@ -8,8 +10,13 @@ class Settings(BaseSettings):
|
|||||||
algorithm: str
|
algorithm: str
|
||||||
access_token_expire_minutes: int
|
access_token_expire_minutes: int
|
||||||
|
|
||||||
class Config:
|
admin_api_prefix: str = "/admin"
|
||||||
env_file = ".env"
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_ignore_case=True,
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
|||||||
38
app/core/security_admin.py
Normal file
38
app/core/security_admin.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Annotated
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from jose import jwt, JWTError
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.models.admin_user import AdminUser
|
||||||
|
|
||||||
|
pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
oauth2_scheme_admin = OAuth2PasswordBearer(tokenUrl=f"{settings.admin_api_prefix}/auth/login")
|
||||||
|
|
||||||
|
def verify_password(plain, hashed): return pwd_ctx.verify(plain, hashed)
|
||||||
|
def hash_password(pw): return pwd_ctx.hash(pw)
|
||||||
|
|
||||||
|
def create_admin_access_token(sub: str, minutes: int = 60):
|
||||||
|
exp = datetime.now(timezone.utc) + timedelta(minutes=minutes)
|
||||||
|
payload = {"sub": sub, "role": "admin", "exp": exp}
|
||||||
|
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
|
||||||
|
|
||||||
|
def get_current_admin(
|
||||||
|
token: Annotated[str, Depends(oauth2_scheme_admin)],
|
||||||
|
db: Annotated[Session, Depends(get_db)],
|
||||||
|
) -> AdminUser:
|
||||||
|
cred_exc = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid admin token")
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
||||||
|
if payload.get("role") != "admin":
|
||||||
|
raise cred_exc
|
||||||
|
username = payload.get("sub")
|
||||||
|
except JWTError:
|
||||||
|
raise cred_exc
|
||||||
|
user = db.query(AdminUser).filter(AdminUser.username == username, AdminUser.is_active == True).first()
|
||||||
|
if not user:
|
||||||
|
raise cred_exc
|
||||||
|
return user
|
||||||
37
app/main.py
37
app/main.py
@ -1,33 +1,36 @@
|
|||||||
#pp/main.py
|
#pp/main.py
|
||||||
from app.models import User, Character, HomeworkType, Friend, FriendRequest # 👈 명시적 import!
|
from app.models import User, Character, HomeworkType, Friend, FriendRequest
|
||||||
from fastapi import FastAPI, Request, Depends
|
from fastapi import FastAPI, Request, Depends
|
||||||
from fastapi.openapi.docs import get_swagger_ui_html
|
from fastapi.openapi.docs import get_swagger_ui_html
|
||||||
from fastapi.openapi.utils import get_openapi
|
from fastapi.openapi.utils import get_openapi
|
||||||
from app.core.deps import get_current_user
|
from app.core.deps import get_current_user
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from app.api import admin_auth, admin_users, admin_board
|
||||||
|
from app.api import admin_members
|
||||||
|
from app.core.config import settings
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from app.api import user, auth, character, homework, character_homework, dashboard, friend
|
from app.api import user, auth, character, homework, character_homework, dashboard, friend
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
||||||
|
|
||||||
# app = FastAPI(
|
|
||||||
# title="숙제노기 API",
|
|
||||||
# description="마비노기 모바일 숙제 관리용 백엔드 API",
|
|
||||||
# version="0.1.0",
|
|
||||||
# docs_url="/docs",
|
|
||||||
# redoc_url=None,
|
|
||||||
# openapi_url="/openapi.json",
|
|
||||||
# root_path="/api"
|
|
||||||
# )
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
docs_url=None,
|
title="숙제노기 API",
|
||||||
|
description="마비노기 모바일 숙제 관리용 백엔드 API",
|
||||||
|
version="0.1.0",
|
||||||
|
docs_url="/docs",
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
openapi_url=None
|
openapi_url="/openapi.json",
|
||||||
|
root_path="/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# app = FastAPI(
|
||||||
|
# docs_url=None,
|
||||||
|
# redoc_url=None,
|
||||||
|
# openapi_url=None
|
||||||
|
# )
|
||||||
|
|
||||||
@app.get("/docs", include_in_schema=False)
|
@app.get("/docs", include_in_schema=False)
|
||||||
def custom_docs(user=Depends(get_current_user)):
|
def custom_docs(user=Depends(get_current_user)):
|
||||||
return get_swagger_ui_html(openapi_url="/openapi.json", title="Sukjenogi API Docs")
|
return get_swagger_ui_html(openapi_url="/openapi.json", title="Sukjenogi API Docs")
|
||||||
@ -58,8 +61,8 @@ origins = [
|
|||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=origins,
|
# allow_origins=origins,
|
||||||
# allow_origins=["*"],
|
allow_origins=["*"],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
@ -72,6 +75,10 @@ app.include_router(homework.router, prefix="/homeworks", tags=["Homeworks"])
|
|||||||
app.include_router(character_homework.router, prefix="/characterHomework", tags=["Character Homeworks"])
|
app.include_router(character_homework.router, prefix="/characterHomework", tags=["Character Homeworks"])
|
||||||
app.include_router(dashboard.router, prefix="/dashboard", tags=["Dashboard"])
|
app.include_router(dashboard.router, prefix="/dashboard", tags=["Dashboard"])
|
||||||
app.include_router(friend.router, prefix="/friends", tags=["Friends"])
|
app.include_router(friend.router, prefix="/friends", tags=["Friends"])
|
||||||
|
app.include_router(admin_auth.router, prefix=f"{settings.admin_api_prefix}/auth", tags=["admin-auth"])
|
||||||
|
app.include_router(admin_users.router, prefix=f"{settings.admin_api_prefix}/users", tags=["admin-users"])
|
||||||
|
app.include_router(admin_board.router, prefix=f"{settings.admin_api_prefix}/boards", tags=["admin-boards"])
|
||||||
|
app.include_router(admin_members.router, prefix=f"{settings.admin_api_prefix}/members",tags=["admin-members"])
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def read_root():
|
def read_root():
|
||||||
|
|||||||
@ -3,3 +3,5 @@ from app.models.user import User
|
|||||||
from app.models.character import Character
|
from app.models.character import Character
|
||||||
from app.models.homework import HomeworkType
|
from app.models.homework import HomeworkType
|
||||||
from app.models.friend import Friend, FriendRequest
|
from app.models.friend import Friend, FriendRequest
|
||||||
|
from app.models.admin_user import AdminUser
|
||||||
|
from app.models.board import Board, BoardPost, BoardComment
|
||||||
18
app/models/admin_user.py
Normal file
18
app/models/admin_user.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from sqlalchemy import Integer, String, Boolean, DateTime
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
class AdminUser(Base):
|
||||||
|
__tablename__ = "admin_user"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
username: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
|
||||||
|
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
email: Mapped[str] = mapped_column(String(120), unique=True, nullable=True)
|
||||||
|
is_superadmin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
last_login_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
41
app/models/board.py
Normal file
41
app/models/board.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy import Integer, String, Boolean, DateTime, Text, ForeignKey
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
class Board(Base):
|
||||||
|
__tablename__ = "board"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
code: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False) # 예: "notices", "free"
|
||||||
|
name: Mapped[str] = mapped_column(String(100), nullable=False) # 예: "공지사항"
|
||||||
|
description: Mapped[str | None] = mapped_column(String(255))
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, default=True) # 공개 여부
|
||||||
|
allow_comment: Mapped[bool] = mapped_column(Boolean, default=True) # 댓글 허용
|
||||||
|
use_attachment: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
|
class BoardPost(Base):
|
||||||
|
__tablename__ = "board_post"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
board_id: Mapped[int] = mapped_column(ForeignKey("board.id", ondelete="CASCADE"), index=True, nullable=False)
|
||||||
|
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||||
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
author_id: Mapped[int | None] = mapped_column(Integer, nullable=True) # 일반 유저 id(선택)
|
||||||
|
is_notice: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
is_pinned: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
view_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
|
class BoardComment(Base):
|
||||||
|
__tablename__ = "board_comment"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
post_id: Mapped[int] = mapped_column(ForeignKey("board_post.id", ondelete="CASCADE"), index=True, nullable=False)
|
||||||
|
author_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||||
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
25
app/schemas/admin_user.py
Normal file
25
app/schemas/admin_user.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class AdminUserBase(BaseModel):
|
||||||
|
username: str
|
||||||
|
name: str
|
||||||
|
email: EmailStr | None = None
|
||||||
|
is_superadmin: bool = False
|
||||||
|
is_active: bool = True
|
||||||
|
|
||||||
|
class AdminUserCreate(AdminUserBase):
|
||||||
|
password: str
|
||||||
|
|
||||||
|
class AdminUserUpdate(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
email: EmailStr | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
is_superadmin: bool | None = None
|
||||||
|
password: str | None = None
|
||||||
|
|
||||||
|
class AdminUserOut(AdminUserBase):
|
||||||
|
id: int
|
||||||
|
last_login_at: datetime | None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
28
app/schemas/board.py
Normal file
28
app/schemas/board.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class BoardCreate(BaseModel):
|
||||||
|
code: str
|
||||||
|
name: str
|
||||||
|
description: str | None = None
|
||||||
|
is_public: bool = True
|
||||||
|
allow_comment: bool = True
|
||||||
|
use_attachment: bool = True
|
||||||
|
|
||||||
|
class BoardOut(BoardCreate):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class PostCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
content: str
|
||||||
|
is_notice: bool = False
|
||||||
|
is_pinned: bool = False
|
||||||
|
|
||||||
|
class PostOut(PostCreate):
|
||||||
|
id: int
|
||||||
|
board_id: int
|
||||||
|
view_count: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
24
app/scripts/create_admin.py
Normal file
24
app/scripts/create_admin.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.database import SessionLocal, Base, engine
|
||||||
|
from app.core.security_admin import hash_password
|
||||||
|
from app.models.admin_user import AdminUser
|
||||||
|
|
||||||
|
def main():
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
db: Session = SessionLocal()
|
||||||
|
if not db.query(AdminUser).filter(AdminUser.username=="admin").first():
|
||||||
|
user = AdminUser(
|
||||||
|
username="admin",
|
||||||
|
password_hash=hash_password("admin1234"),
|
||||||
|
name="관리자",
|
||||||
|
email=None,
|
||||||
|
is_superadmin=True,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db.add(user); db.commit()
|
||||||
|
print("✅ admin / admin1234 created")
|
||||||
|
else:
|
||||||
|
print("ℹ️ admin already exists")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -8,3 +8,4 @@ passlib[bcrypt]
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
pydantic[email]
|
pydantic[email]
|
||||||
pydantic-settings
|
pydantic-settings
|
||||||
|
email-validator>=2.1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user