FastAPI 포럼 만들기: 7단계 - 권한
James Reed
Infrastructure Engineer · Leapcell

이전 글에서 포럼에 댓글과 답글 기능을 구현하여 커뮤니티 상호작용을 크게 향상시켰습니다.
하지만 상호작용이 늘어나면 필연적으로 갈등이 발생할 수 있습니다. 상호작용이 증가함에 따라 커뮤니티 관리는 우리가 직면해야 하는 문제가 됩니다. 누군가 악의적인 콘텐츠를 게시하면 어떻게 될까요?
이 글에서는 기본적인 권한 관리 시스템을 소개합니다. "관리자" 역할을 설정하고 관리자에게 커뮤니티 질서를 유지하기 위해 사용자를 "차단"할 수 있는 기능을 부여할 것입니다.
1단계: 데이터베이스 모델 업데이트
사용자 테이블(users)에 두 개의 필드를 추가해야 합니다. 하나는 누가 관리자인지 식별하는 용도이고, 다른 하나는 누가 "차단"되었는지 표시하는 용도입니다.
models.py를 열고 User 모델을 수정합니다.
models.py (User 모델 업데이트)
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean from sqlalchemy.orm import relationship from database import Base class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) hashed_password = Column(String) # --- 새로운 필드 --- is_admin = Column(Boolean, default=False) is_banned = Column(Boolean, default=False) # --------------- posts = relationship("Post", back_populates="owner", cascade="all, delete-orphan") comments = relationship("Comment", back_populates="owner", cascade="all, delete-orphan") # ... Post 및 Comment 모델은 변경되지 않습니다 ...
is_admin과 is_banned 두 개의 필드를 추가했습니다. 기존 사용자에게 영향을 주지 않도록 두 필드 모두 default=False로 설정했습니다.
모델을 업데이트한 후에는 데이터베이스 테이블 구조를 수동으로 업데이트해야 합니다. 해당 SQL 문은 다음과 같습니다.
-- users 테이블에 is_admin 컬럼 추가 ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE; -- users 테이블에 is_banned 컬럼 추가 ALTER TABLE users ADD COLUMN is_banned BOOLEAN DEFAULT FALSE;
데이터베이스를 Leapcell을 사용하여 생성한 경우,
해당 SQL 문을 웹 기반 작업 패널에서 직접 실행할 수 있습니다.

2단계: 관리자 수동 임명
아직 관리자를 임명할 "관리자 백엔드"가 없습니다. 관리자 생성은 빈번하지 않은 요구 사항이므로, 데이터베이스를 직접 조작하여 사용자를 관리자로 수동 설정할 수 있습니다.
데이터베이스에서 다음 명령을 실행합니다.
-- 사용자 이름 'your_username'을 가진 사용자를 관리자로 설정 UPDATE users SET is_admin = TRUE WHERE username = 'your_username';
your_username을 등록한 사용자 이름으로 바꾸는 것을 잊지 마세요.
3단계: 관리자 패널 페이지 생성
관리자만 액세스할 수 있는 페이지가 필요하며, 이 페이지는 모든 사용자를 표시하고 작업 버튼을 제공합니다.
templates 폴더에 admin.html이라는 새 파일을 만듭니다.
templates/admin.html
<!DOCTYPE html> <html> <head> <title>관리자 패널 - 내 FastAPI 포럼</title> <style> body { font-family: sans-serif; margin: 2em; } li { margin-bottom: 10px; } button { margin-left: 10px; padding: 5px; } </style> </head> <body> <h1>관리자 패널 - 사용자 관리</h1> <a href="/posts">홈으로 돌아가기</a> <hr /> <ul> {% for user in users %} <li> <strong>{{ user.username }}</strong> <span>(관리자: {{ user.is_admin }}, 차단됨: {{ user.is_banned }})</span> {% if not user.is_admin %} {% if user.is_banned %} <form action="/admin/unban/{{ user.id }}" method="post" style="display: inline;"> <button type="submit" style="background-color: #28a745; color: white;">차단 해제</button> </form> {% else %} <form action="/admin/ban/{{ user.id }}" method="post" style="display: inline;"> <button type="submit" style="background-color: #dc3545; color: white;">차단</button> </form> {% endif %} {% endif %} </li> {% endfor %} </ul> </body> </html>
이 페이지는 모든 사용자를 반복합니다. 사용자가 관리자가 아니면 해당 사용자 옆에 "차단" 또는 "차단 해제" 버튼이 표시됩니다. 이 버튼들은 POST 요청을 통해 생성할 API 라우트를 가리킵니다.
4단계: 관리자 백엔드 라우트 구현
이제 main.py에 관리자 패널 로직을 처리할 새 라우트를 추가해야 합니다.
main.py (새 라우트 및 의존성 추가)
# ... (이전 임포트들은 변경되지 않았습니다) ... # --- 의존성 --- # ... (get_current_user는 변경되지 않았습니다) ... # 1. 관리자 권한을 확인하는 새 의존성 추가 async def get_admin_user( current_user: Optional[models.User] = Depends(get_current_user) ) -> models.User: if not current_user: raise HTTPException( status_code=status.HTTP_302_FOUND, detail="인증되지 않음", headers={"Location": "/login"} ) if not current_user.is_admin: # 관리자가 아니면 403 오류 발생 raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="이 리소스에 액세스할 권한이 없습니다." ) return current_user # --- 라우트 --- # ... (이전 라우트 /, /posts, /api/posts 등은 변경되지 않았습니다) ... # 2. 관리자 패널 라우트 추가 @app.get("/admin", response_class=HTMLResponse) async def view_admin_panel( request: Request, db: AsyncSession = Depends(get_db), admin_user: models.User = Depends(get_admin_user) ): # 모든 사용자 쿼리 result = await db.execute(select(models.User).order_by(models.User.id)) users = result.scalars().all() return templates.TemplateResponse("admin.html", { "request": request, "users": users }) # 3. 사용자 차단 라우트 @app.post("/admin/ban/{user_id}") async def ban_user( user_id: int, db: AsyncSession = Depends(get_db), admin_user: models.User = Depends(get_admin_user) ): result = await db.execute(select(models.User).where(models.User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다") # 관리자는 다른 관리자를 차단할 수 없습니다 if user.is_admin: raise HTTPException(status_code=403, detail="관리자를 차단할 수 없습니다") user.is_banned = True await db.commit() return RedirectResponse(url="/admin", status_code=status.HTTP_303_SEE_OTHER) # 4. 사용자 차단 해제 라우트 @app.post("/admin/unban/{user_id}") async def unban_user( user_id: int, db: AsyncSession = Depends(get_db), admin_user: models.User = Depends(get_admin_user) ): result = await db.execute(select(models.User).where(models.User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다") user.is_banned = False await db.commit() return RedirectResponse(url="/admin", status_code=status.HTTP_303_SEE_OTHER) # ... (이후 라우트 /posts/{post_id}, /posts/{post_id}/comments 등은 변경되지 않았습니다) ...
여기에는 다음과 같은 주요 변경 사항이 포함됩니다.
get_current_user를 기반으로 하며current_user.is_admin이True인지 추가로 확인하는 새 의존성get_admin_user를 생성했습니다.- 모든 사용자를 쿼리하고
admin.html템플릿을 렌더링하는GET /admin라우트를 생성했습니다. 이 라우트는 관리자만 액세스할 수 있도록Depends(get_admin_user)로 보호됩니다. - 특정 사용자를 차단/차단 해제하기 위한
POST /admin/ban/{user_id}및POST /admin/unban/{user_id}라우트를 생성했습니다.
5단계: 차단 강제 적용 (게시물 작성 방지)
이제 "차단됨"으로 표시될 수 있지만 사용자의 작업은 아직 영향을 받지 않습니다. 차단된 사용자는 여전히 게시물과 댓글을 작성할 수 있습니다.
create_post 및 create_comment 라우트를 수정하여 해당 작업을 수행하기 전에 사용자의 상태를 확인해야 합니다.
main.py (create_post 및 create_comment 업데이트)
# ... (이전 코드) ... @app.post("/api/posts") async def create_post( title: str = Form(...), content: str = Form(...), db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): if not current_user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) # --- 확인 추가 --- if current_user.is_banned: raise HTTPException(status_code=403, detail="귀하는 차단되었으므로 게시물을 작성할 수 없습니다.") # --------------- new_post = models.Post(title=title, content=content, owner_id=current_user.id) # ... (이후 코드는 변경되지 않았습니다) ... db.add(new_post) await db.commit() await db.refresh(new_post) return RedirectResponse(url="/posts", status_code=status.HTTP_303_SEE_OTHER) # ... (기타 라우트) ... @app.post("/posts/{post_id}/comments") async def create_comment( post_id: int, content: str = Form(...), parent_id: Optional[int] = Form(None), db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): if not current_user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) # --- 확인 추가 --- if current_user.is_banned: raise HTTPException(status_code=403, detail="귀하는 차단되었으므로 댓글을 작성할 수 없습니다.") # --------------- new_comment = models.Comment( content=content, post_id=post_id, owner_id=current_user.id, parent_id=parent_id ) # ... (이후 코드는 변경되지 않았습니다) ... db.add(new_comment) await db.commit() return RedirectResponse(url=f"/posts/{post_id}", status_code=status.HTTP_303_SEE_OTHER) # ... (이후 라우트 /posts/{post_id}/edit, /register, /login, /logout 등은 변경되지 않았습니다) ...
이제 차단된 사용자가 게시물 또는 댓글 양식을 제출하려고 하면 백엔드가 요청을 거부하고 403 오류를 반환합니다.
6단계: 프론트엔드 UI 업데이트
백엔드는 이제 안전하지만 사용자 경험 관점에서 프론트엔드에서 게시 및 댓글 양식을 숨기고 관리자에게 백엔드 진입점을 제공해야 합니다.
templates/posts.html (업데이트)
... (헤더 및 스타일은 변경되지 않았습니다) ... <body> <header> <h1>내 포럼에 오신 것을 환영합니다</h1> <div class="auth-links"> {% if current_user %} <span>안녕하세요, {{ current_user.username }}님!</span> {% if current_user.is_admin %} <a href="/admin" style="color: red; font-weight: bold;">[관리자 패널]</a> {% endif %} <a href="/logout">로그아웃</a> {% else %} <a href="/login">로그인</a> | <a href="/register">회원가입</a> {% endif %} </div> </header> {% if current_user and not current_user.is_banned %} <h2>새 게시물 작성</h2> <form action="/api/posts" method="post"> <input type="text" name="title" placeholder="게시물 제목" required /><br /> <textarea name="content" rows="4" placeholder="게시물 내용" required></textarea><br /> <button type="submit">게시</button> </form> {% elif current_user and current_user.is_banned %} <p style="color: red; font-weight: bold;">귀하는 차단되었으므로 새 게시물을 작성할 수 없습니다.</p> {% else %} <p><a href="/login">로그인</a>하여 새 게시물을 작성하세요.</p> {% endif %} <hr /> ... (게시물 목록 섹션은 변경되지 않았습니다) ... </body> </html>
templates/post_detail.html (업데이트)
... (헤더 및 스타일은 변경되지 않았습니다) ... <body> ... (게시물 상세 섹션은 변경되지 않았습니다) ... <hr /> <div class="comment-form"> <h3>댓글 작성</h3> {% if current_user and not current_user.is_banned %} <form action="/posts/{{ post.id }}/comments" method="post"> <textarea name="content" rows="4" style="width:100%;" placeholder="댓글을 작성하세요..." required></textarea><br /> <button type="submit">제출</button> </form> {% elif current_user and current_user.is_banned %} <p style="color: red; font-weight: bold;">귀하는 차단되었으므로 댓글을 작성할 수 없습니다.</p> {% else %} <p><a href="/login">로그인</a>하여 댓글을 작성하세요.</p> {% endif %} </div> ... (댓글 섹션은 변경되지 않았습니다) ... </body> </html>
여기에는 두 가지 주요 변경 사항이 포함됩니다.
posts.html헤더에서 현재 사용자가 관리자인 경우(current_user.is_admin), "관리자 패널" 링크가 표시됩니다.posts.html과post_detail.html에서 원래{% if current_user %}조건이{% if current_user and not current_user.is_banned %}로 변경되어, 차단되지 않은 사용자만 양식을 볼 수 있도록 했습니다.
실행 및 확인
uvicorn 서버를 다시 시작합니다.
uvicorn main:app --reload
관리자 계정으로 로그인합니다. 오른쪽 상단에 "관리자 패널" 링크가 보여야 합니다.

클릭하여 /admin 페이지로 이동합니다. 모든 사용자 목록이 표시되며 다른 사용자를 차단할 수 있습니다.

test_user를 차단합니다. test_user로 로그인한 것처럼 전환하면 "새 게시물 작성" 양식이 사라지고 "귀하는 차단되었습니다"라는 메시지가 표시되는 것을 발견할 수 있습니다.

결론
포럼에 기본적인 관리 기능을 추가했습니다. is_admin과 is_banned 필드를 사용하여 사용자 역할 차별화와 권한 제어를 지원했습니다.
이 프레임워크를 기반으로 사용자에게 섀도우 차단 또는 로그인 금지와 같은 더 많은 관리 기능을 확장할 수 있습니다.
포럼 콘텐츠가 증가함에 따라 사용자는 관심 있는 오래된 게시물을 찾기 어렵게 될 수 있습니다.
이 문제를 해결하기 위해 다음 글에서는 포럼에 검색 기능을 추가할 것입니다.

