Build Your Own Forum with FastAPI: Step 7 - Permissions
James Reed
Infrastructure Engineer · Leapcell

In the previous article, we implemented comments and replies for our forum, which greatly enhanced community interaction.
Interaction, however, can inevitably lead to conflict. As interaction increases, community management becomes a problem we must face. What if someone posts malicious content?
In this article, we will introduce a basic permission management system. We will establish an "Admin" role and give administrators the ability to "ban" users to maintain community order.
Step 1: Update the Database Model
We need to add two fields to the user table (users): one to identify who is an admin, and another to mark who has been "banned."
Open models.py and modify the User model:
models.py (Update User model)
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) # --- New fields --- 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 and Comment models remain unchanged ...
We've added two fields: is_admin and is_banned. Both are set to default=False to avoid affecting existing users.
After updating the model, you need to manually update your database table structure. The corresponding SQL statements are as follows:
-- Add is_admin column to users table ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE; -- Add is_banned column to users table ALTER TABLE users ADD COLUMN is_banned BOOLEAN DEFAULT FALSE;
If your database was created using Leapcell,
you can execute these SQL statements directly in its web-based operation panel.

Step 2: Manually Appoint an Administrator
Our forum doesn't have an "admin backend" to appoint administrators yet. Since creating an admin is an infrequent requirement, we can just operate the database directly to manually set your user as an admin.
Execute the following command in your database:
-- Set the user with username 'your_username' as an admin UPDATE users SET is_admin = TRUE WHERE username = 'your_username';
Remember to replace your_username with the username you registered with.
Step 3: Create the Admin Panel Page
We need a page that only administrators can access, which will display all users and provide action buttons.
In the templates folder, create a new file named 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>
This page iterates through all users. If a user is not an admin, a "Ban" or "Unban" button will be displayed next to them. These buttons will point to the API routes we are about to create via a POST request.
Step 4: Implement the Admin Backend Routes
Now, we need to add new routes in main.py to handle the admin panel logic.
main.py (Add new routes and dependency)
# ... (Previous imports remain unchanged) ... # --- Dependencies --- # ... (get_current_user remains unchanged) ... # 1. Add a new dependency to verify admin permissions 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: # If not an admin, raise a 403 error raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to access this resource." ) return current_user # --- Routes --- # ... (Previous routes /, /posts, /api/posts, etc., remain unchanged) ... # 2. Add admin panel route @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) ): # Query all users 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. Ban user route @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") # Admins cannot ban other admins 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. Unban user route @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) # ... (Subsequent routes /posts/{post_id}, /posts/{post_id}/comments, etc., remain unchanged) ...
This includes these main changes:
- Created a new dependency
get_admin_user, which is based onget_current_userand additionally checks ifcurrent_user.is_adminisTrue. - Created the
GET /adminroute, which queries all users and renders theadmin.htmltemplate. This route is protected byDepends(get_admin_user)to ensure only admins can access it. - Created
POST /admin/ban/{user_id}andPOST /admin/unban/{user_id}routes to ban/unban specific users.
Step 5: Enforce the Ban (Prevent Posting)
A user can now be marked as "banned," but their actions are not yet affected. A banned user can still create posts and comments.
We need to modify the create_post and create_comment routes to check the user's status before performing the action.
main.py (Update create_post and create_comment)
# ... (Previous code) ... @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) # --- Add check --- 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) # ... (Subsequent code remains unchanged) ... db.add(new_post) await db.commit() await db.refresh(new_post) return RedirectResponse(url="/posts", status_code=status.HTTP_303_SEE_OTHER) # ... (Other routes) ... @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) # --- Add check --- 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 ) # ... (Subsequent code remains unchanged) ... db.add(new_comment) await db.commit() return RedirectResponse(url=f"/posts/{post_id}", status_code=status.HTTP_303_SEE_OTHER) # ... (Subsequent routes /posts/{post_id}/edit, /register, /login, /logout, etc., remain unchanged) ...
Now, if a banned user tries to submit a post or comment form, the backend will reject the request and return a 403 error.
Step 6: Update the Frontend UI
The backend is now secure, but from a user experience perspective, we should hide the posting and commenting forms from the frontend and give admins an entry point to the backend.
templates/posts.html (Update)
... (Header and styles remain unchanged) ... <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 /> ... (Post list section remains unchanged) ... </body> </html>
templates/post_detail.html (Update)
... (Header and styles remain unchanged) ... <body> ... (Post detail section remains unchanged) ... <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> ... (Comment section remains unchanged) ... </body> </html>
This includes two main changes:
- In the
posts.htmlheader, if the current user is an admin (current_user.is_admin), an "Admin Panel" link is displayed. - In
posts.htmlandpost_detail.html, the original{% if current_user %}condition is changed to{% if current_user and not current_user.is_banned %}, meaning only users who are not banned can see the form.
Run and Verify
Restart your uvicorn server:
uvicorn main:app --reload
Log in to your admin account. You should be able to see the "Admin Panel" link in the top right.

Click it to go to the /admin page. You will see a list of all users and can ban other users.

Ban test_user. Switch to being logged in as test_user, and you will find that the "Create New Post" form has disappeared, replaced by a "You have been banned" message.

Conclusion
We have added basic management functions to the forum. Using the is_admin and is_banned fields, we've supported user role differentiation and permission control.
Based on this framework, you can further extend more management functions, such as shadow-banning users or prohibiting logins.
As the forum's content grows, users might find it difficult to locate old posts they are interested in.
To address this, in the next article, we will add a search function to the forum.

