0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FastAPIで独自のフォーラムを構築:ステップ7 - パーミッション

Posted at

Cover

前の記事では、フォーラムにコメントと返信を実装し、コミュニティのインタラクションを大幅に強化しました。

しかし、インタラクションが増えれば、必然的に対立が生じます。インタラクションが増えるにつれて、コミュニティ管理は私たちが直面しなければならない問題となります。悪意のあるコンテンツが投稿された場合はどうなるでしょうか?

この記事では、基本的なパーミッション管理システムを紹介します。私たちは「管理者」ロールを確立し、コミュニティの秩序を維持するために管理者にユーザーを「禁止」する能力を与えます。

ステップ1:データベースモデルの更新

ユーザーテーブル(users)に2つのフィールドを追加する必要があります。1つは管理者かどうかを識別するため、もう1つは「禁止」されているかどうかをマークするためです。

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_adminis_bannedの2つのフィールドを追加しました。既存のユーザーに影響を与えないように、両方とも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を使用している場合、

ImageLc

これらのSQLステートメントをWebベースの操作パネルで直接実行できます。

ImageDb

ステップ2:管理者の手動任命

私たちのフォーラムには、管理者Appointmentsするための「管理者バックエンド」がまだありません。管理者を作成することはまれな要件であるため、データベースを直接操作して、ユーザーを管理者に手動で設定することができます。

データベースで次のコマンドを実行します。

-- ユーザー名 '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>Admin Panel - My FastAPI Forum</title>
    <style>
      body {
        font-family: sans-serif;
        margin: 2em;
      }
      li {
        margin-bottom: 10px;
      }
      button {
        margin-left: 10px;
        padding: 5px;
      }
    </style>
  </head>
  <body>
    <h1>Admin Panel - User Management</h1>
    <a href="/posts">Back to Home</a>
    <hr />
    <ul>
      {% for user in users %}
      <li>
        <strong>{{ user.username }}</strong>
        <span>(Admin: {{ user.is_admin }}, Banned: {{ 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;">Unban</button>
        </form>
        {% else %}
        <form action="/admin/ban/{{ user.id }}" method="post" style="display: inline;">
          <button type="submit" style="background-color: #dc3545; color: white;">Ban</button>
        </form>
        {% endif %} {% endif %}
      </li>
      {% endfor %}
    </ul>
  </body>
</html>

このページは、すべてのユーザーを反復処理します。ユーザーが管理者でない場合、その横に「禁止」または「禁止解除」ボタンが表示されます。これらのボタンは、これから作成するAPIルートにPOSTリクエスト経由で接続されます。

ステップ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="Not authenticated",
            headers={"Location": "/login"}
        )
    if not current_user.is_admin:
        # 管理者でない場合は、403エラーを発生させる
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="You do not have permission to access this resource."
        )
    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="User not found")

    # 管理者は他の管理者を禁止できない
    if user.is_admin:
        raise HTTPException(status_code=403, detail="Cannot ban an admin")

    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 not found")

    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_adminTrueであるかを確認する新しい依存関係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.pycreate_postcreate_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="You are banned and cannot create posts.")
    # --------------- 

    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="You are banned and cannot create comments.")
    # --------------- 

    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>Welcome to My Forum</h1>

      <div class="auth-links">
        {% if current_user %}
        <span>Welcome, {{ current_user.username }}!</span>
        {% if current_user.is_admin %}
        <a href="/admin" style="color: red; font-weight: bold;">[Admin Panel]</a>
        {% endif %}
        <a href="/logout">Logout</a>
        {% else %}
        <a href="/login">Login</a> |
        <a href="/register">Register</a>
        {% endif %}
      </div>
    </header>

    {% if current_user and not current_user.is_banned %}
    <h2>Create a New Post</h2>
    <form action="/api/posts" method="post">
      <input type="text" name="title" placeholder="Post Title" required /><br />
      <textarea name="content" rows="4" placeholder="Post Content" required></textarea><br />
      <button type="submit">Post</button>
    </form>
    {% elif current_user and current_user.is_banned %}
    <p style="color: red; font-weight: bold;">You have been banned and cannot create new posts.</p>
    {% else %}
    <p><a href="/login">Login</a> to create a new post.</p>
    {% endif %}

    <hr />
    ... (投稿リストセクションは変更なし) ...
</body>
</html>

templates/post_detail.html(更新)

... (ヘッダーとスタイルは変更なし) ...
<body>
    ... (投稿詳細セクションは変更なし) ...
    <hr />
    <div class="comment-form">
      <h3>Post a Comment</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="Write your comment..." required></textarea><br />
        <button type="submit">Submit</button>
      </form>
      {% elif current_user and current_user.is_banned %}
      <p style="color: red; font-weight: bold;">You have been banned and cannot post comments.</p>
      {% else %}
      <p><a href="/login">Log in</a> to post a comment.</p>
      {% endif %}
    </div>
    ... (コメントセクションは変更なし) ...
</body>
</html>

これには2つの主な変更が含まれます。

  • 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

管理者アカウントにログインします。右上隅に「管理者パネル」リンクが表示されるはずです。

ImageP1

それをクリックして/adminページに移動します。すべてのユーザーのリストが表示され、他のユーザーを禁止できます。

ImageP2

test_userを禁止します。test_userとしてログインしている状態に切り替えると、「新規投稿の作成」フォームが消え、「禁止されています」というメッセージに置き換えられていることに気づくでしょう。

ImageP3

結論

フォーラムに基本的な管理機能を追加しました。is_adminおよびis_bannedフィールドの使用により、ユーザーロールの区別とパーミッション管理をサポートしました。

このフレームワークに基づいて、シャドウバンやログイン禁止などの、より多くの管理機能をさらに拡張できます。

フォーラムのコンテンツが増えるにつれて、ユーザーは関心のある古い投稿を見つけるのに苦労するかもしれません。

これを解決するために、次の記事ではフォーラムに検索機能を追加します。


Xでフォローする:@LeapcellJapan


本シリーズの他の記事を読む

関連記事:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?