fastapi-starlette-admin について
fastapi-starlette-adminは、新規または既存のFastAPIプロジェクトにstarlette-adminのCRUD管理パネルを追加するスキルです。データベース方式と認証要件に応じた設定を案内し、統合用ファイルのテンプレートとトラブルシューティングを提供します。
主な機能
- 01既存プロジェクトの構成を確認
- 02データベース方式に応じて設定
- 03管理ビューとダッシュボードを生成
- 04セッションベース認証を設定
- 05ミドルウェアと管理ルートを統合
- 06よくある統合問題を診断
活用シーン
- 01FastAPIプロジェクトに管理パネルを追加
- 02管理画面向けに非同期DBを適合
- 03認証付きCRUD画面を生成
- 04メニュー欠落やログイン転送を診断
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ shyxin/ skills --skill fastapi-starlette-adminインストール後、Claude Code で/fastapi-starlette-adminを入力して読み込まれたか確認します。
詳細
FastAPI + Starlette-Admin のクイック統合
FastAPI プロジェクトに starlette-admin ベースの CRUD 管理パネルをすばやく追加します。
起動条件
ユーザーが「admin を追加」「管理画面を追加」「starlette-admin を統合」「admin panel」「バックエンド管理」と言ったときに起動します。
前提条件
pip install starlette-admin itsdangerous jinja2
# 如果项目用 async 数据库驱动,还需要同步驱动:
pip install pymysql # MySQL (对应 aiomysql)
# pip install psycopg2-binary # PostgreSQL (对应 asyncpg)
ワークフロー
Phase 1: プロジェクトの状態を確認(質問 2~3 件)
コードを生成する前に、必ず以下の質問をユーザーにしてください:
-
プロジェクトの状態:ゼロから始めますか?それとも既存の FastAPI プロジェクトがありますか?
- 既存プロジェクト:
config.py/settings.py、database.py、main.py、models/を読み込み、既存の構造を確認します - 新規プロジェクト:完全な骨組みを生成します
- 既存プロジェクト:
-
データベースの種類?
- A) Async メインエンジン(aiomysql / asyncpg / aiosqlite)→ 同期エンジンのアダプターを生成する必要があります
- B) 完全な同期構成(pymysql / psycopg2 / sqlite)→ 同じエンジンをそのまま使用します
- C) SQLite ですばやく起動 → 設定を簡略化します
-
認証は必要ですか?
- A) 必要(デフォルト)→ Session ベースの AuthProvider を生成します
- B) 不要(内部ツール/開発用)→ auth を省略し、
auth_provider=Noneにします
Phase 2: コードを生成
Phase 1 の回答に基づいて、以下のファイルを生成します。
ファイル 1:app/admin/__init__.py(新規作成)
コアとなるファクトリー関数です。以下のテンプレートは「既存の async プロジェクト+認証が必要」のケースに合わせたものです。それ以外のケースでは必要に応じて調整してください。
"""Starlette-Admin 管理面板配置。"""
import os
from starlette_admin.contrib.sqla import Admin
from starlette_admin import I18nConfig
from app.admin.views import DashboardView, YourModel1View, YourModel2View # TODO
from app.admin.auth import AdminAuthProvider
from app.core.database import db_manager # TODO: 替换为你的数据库管理器
def create_admin(app):
"""创建并配置 starlette-admin,挂载到 FastAPI app。
⚠️ 关键:
1. Admin 构造函数第一个参数是 sync_engine(不是 app)
2. starlette-admin 需要同步引擎,不能直接用 async 引擎
3. 调用 admin.mount_to(app) 挂载
4. templates_dir 必须存在(os.makedirs)
"""
# 获取同步引擎(starlette-admin 不支持 async 引擎)
engine = db_manager.get_sync_engine()
base_dir = os.path.dirname(os.path.abspath(__file__))
templates_dir = os.path.join(base_dir, 'templates')
os.makedirs(templates_dir, exist_ok=True)
admin = Admin(
engine=engine,
title="管理后台", # TODO: 修改为你的标题
base_url="/admin",
route_name="admin",
templates_dir=templates_dir,
index_view=DashboardView(),
auth_provider=AdminAuthProvider(),
i18n_config=I18nConfig(
default_locale="en",
language_switcher=["en", "zh_Hant"], # 语言切换按钮,按需调整
),
)
# 注册视图
admin.add_view(YourModel1View(YourModel1, engine)) # TODO
admin.add_view(YourModel2View(YourModel2, engine)) # TODO
# 挂载到 FastAPI
admin.mount_to(app)
return admin
⚠️ ユーザーが「認証は不要」を選択した場合:
admin = Admin(
engine=engine,
title="管理后台",
base_url="/admin",
route_name="admin",
templates_dir=templates_dir,
index_view=DashboardView(),
auth_provider=None, # 无认证
i18n_config=I18nConfig(default_locale="en"),
)
ファイル 2:app/admin/auth.py(新規作成、認証が必要な場合のみ)
"""Starlette-Admin 认证提供者。"""
from starlette.requests import Request
from starlette.responses import Response
from starlette_admin.auth import AdminConfig, AdminUser, AuthProvider
from sqlalchemy.orm import sessionmaker
from app.core.database import db_manager # TODO
from app.models.user import User # TODO: 替换为你的用户模型
from app.core.security import pwd_context # TODO: 替换为你的密码验证工具
class AdminAuthProvider(AuthProvider):
"""Session-based 认证,仅允许管理员登录。"""
def __init__(self):
super().__init__()
engine = db_manager.get_sync_engine()
self.SessionLocal = sessionmaker(engine)
async def login(
self,
username: str,
password: str,
remember_me: bool,
request: Request,
response: Response,
) -> Response:
db = self.SessionLocal()
try:
user = db.query(User).filter(User.username == username).first()
if not user or not pwd_context.verify(password, user.password_hash):
raise ValueError("用户名或密码错误")
# TODO: 根据你的角色字段调整
if user.role.value != "admin":
raise ValueError("仅管理员可登录后台")
request.session["admin_user_id"] = user.id
request.session["admin_username"] = user.username
return response
finally:
db.close() # ⚠️ 必须手动关闭,防止连接池泄漏
async def is_authenticated(self, request: Request) -> bool:
user_id = request.session.get("admin_user_id")
if not user_id:
return False
db = self.SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
# TODO: 根据你的角色字段调整
return user is not None and user.role.value == "admin"
finally:
db.close()
def get_admin_config(self, request: Request) -> AdminConfig:
username = request.session.get("admin_username", "Admin")
return AdminConfig(app_title=f"管理后台 — {username}")
def get_admin_user(self, request: Request) -> AdminUser:
username = request.session.get("admin_username", "Admin")
return AdminUser(username=username)
async def logout(self, request: Request, response: Response) -> Response:
request.session.clear()
return response
ファイル 3:app/admin/views.py(新規作成)
DashboardView + ModelView + batch actions を含む完全なテンプレートです。
"""Starlette-Admin 视图定义。"""
import csv
import io
from typing import List, Any
from starlette.requests import Request
from starlette.responses import Response
from starlette_admin import CustomView, action
from starlette_admin.contrib.sqla import ModelView
from starlette_admin.exceptions import ActionFailed
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.models import User, YourModel1, YourModel2 # TODO
class DashboardView(CustomView):
"""管理后台首页仪表盘。
⚠️ 关键:模板必须 extends "layout.html",用 {% block content %} 和 {% block header %}
不要用 {% block body %} —— 会覆盖整个页面结构(侧边栏、导航栏全部丢失)
"""
def __init__(self):
super().__init__(
label="仪表盘",
icon="fa fa-dashboard",
path="/",
template_path="dashboard.html",
add_to_menu=True,
)
async def render(self, request: Request, templates) -> Response:
session: Session = request.state.session
stats = self._collect_stats(session)
return templates.TemplateResponse(
request=request,
name="dashboard.html",
context={"title": "仪表盘", "stats": stats},
)
def _collect_stats(self, session: Session) -> dict:
# TODO: 替换为你自己的统计查询
return {
"user_count": session.query(func.count(User.id)).scalar(),
"model1_count": session.query(func.count(YourModel1.id)).scalar(),
}
class UserModelView(ModelView):
"""用户管理视图。"""
# TODO: 替换为你的模型
model = User
label = "用户"
icon = "fa fa-users"
fields = [User.id, User.username, User.email, User.role, User.is_active,
User.is_verified, User.created_at]
search_fields = [User.username, User.email]
exclude_fields_from_create = ["role", "password_hash"]
exclude_fields_from_edit = ["role", "password_hash"]
actions = ["delete", "toggle_active", "export_csv"]
@action(
name="toggle_active",
text="切换激活状态",
confirmation="确定要切换选中用户的激活状态吗?",
submit_btn_text="确定",
submit_btn_class="btn-warning",
icon_class="fa fa-toggle-on",
)
async def toggle_active_action(self, request: Request, pks: List[Any]) -> str:
session: Session = request.state.session
users = session.query(User).filter(User.id.in_(pks)).all()
count = 0
for user in users:
user.is_active = not user.is_active
count += 1
session.commit()
return f"已切换 {count} 个用户的激活状态"
@action(
name="export_csv",
text="导出 CSV",
confirmation="确定要导出选中的数据吗?",
submit_btn_text="导出",
submit_btn_class="btn-success",
icon_class="fa fa-download",
custom_response=True, # ⚠️ 返回文件下载时必须设为 True
)
async def export_csv_action(self, request: Request, pks: List[Any]) -> Response:
session: Session = request.state.session
items = session.query(User).filter(User.id.in_(pks)).all()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["ID", "用户名", "邮箱", "角色"])
for item in items:
writer.writerow([item.id, item.username, item.email, item.role.value])
return Response(
content=output.getvalue(),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=export.csv"},
)
class YourModel1View(ModelView):
"""TODO: 替换为你的模型管理视图。"""
model = YourModel1
label = "模型1"
icon = "fa fa-table"
fields = [YourModel1.id, YourModel1.title] # TODO
search_fields = [YourModel1.title] # TODO
actions = ["delete", "export_csv"]
@action(
name="export_csv",
text="导出 CSV",
confirmation="确定要导出选中的数据吗?",
submit_btn_text="导出",
submit_btn_class="btn-success",
icon_class="fa fa-download",
custom_response=True,
)
async def export_csv_action(self, request: Request, pks: List[Any]) -> Response:
session: Session = request.state.session
items = session.query(YourModel1).filter(YourModel1.id.in_(pks)).all()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["ID", "标题"]) # TODO
for item in items:
writer.writerow([item.id, item.title]) # TODO
return Response(
content=output.getvalue(),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=export.csv"},
)
# 只读视图示例(如审计日志)
class AuditLogModelView(ModelView):
"""审计日志(只读)。"""
# model = AuditLog # TODO
label = "审计日志"
icon = "fa fa-history"
# fields = [AuditLog.id, ...] # TODO
def can_create(self, request):
return False
def can_edit(self, request):
return False
def can_delete(self, request):
return False
ファイル 4:app/admin/templates/dashboard.html(新規作成)
{% extends "layout.html" %}
{% block header %}
<div class="page-header">
<div class="container-xl">
<h2 class="page-title">仪表盘</h2>
</div>
</div>
{% endblock %}
{% block content %}
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-blue text-white avatar">
<i class="fa fa-users"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">{{ stats.user_count }}</div>
<div class="text-muted">用户数</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
ファイル 5:app/core/database.py(変更)
既存の async エンジンをベースに同期エンジンを追加します。変更箇所を明記してください:
# ============ 已有 async 引擎(保持不变) ============
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
engine = create_async_engine(settings.DATABASE_URL, ...)
async_session = async_sessionmaker(engine, class_=AsyncSession, ...)
# ============ 新增:starlette-admin 同步引擎 ============
# ⚠️ starlette-admin 需要同步数据库引擎,不能直接用 async 引擎
# 通过替换 dialect 驱动名来创建同步版本
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker as sync_sessionmaker
def get_sync_engine():
"""获取同步引擎,供 starlette-admin 使用。"""
sync_url = settings.DATABASE_URL
# 自动检测并转换异步驱动为同步驱动
if "+aiomysql://" in sync_url:
sync_url = sync_url.replace("+aiomysql://", "+pymysql://")
elif "+asyncpg://" in sync_url:
sync_url = sync_url.replace("+asyncpg://", "postgresql://")
elif "+aiosqlite://" in sync_url:
sync_url = sync_url.replace("+aiosqlite://", "")
return create_engine(sync_url, pool_size=5, max_overflow=10, pool_recycle=1800)
# ============ 新增结束 ============
⚠️ 完全な同期プロジェクトの場合は同じエンジンをそのまま使用し、get_sync_engine()は不要です。
ファイル 6:app/main.py(変更)
SessionMiddleware は create_admin より前に追加する必要があります。また、FastAPI アプリに追加してください(admin.middlewares ではありません)。
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware # 新增
from app.core.config import settings
from app.api.xxx import router as xxx_router
from app.admin import create_admin # 新增
app = FastAPI(title="your-app", debug=True)
# ============ 新增:Session 中间件(必须在 create_admin 之前) ============
# ⚠️ starlette-admin 的登录系统依赖 session,没有它会导致无限重定向
app.add_middleware(
SessionMiddleware,
secret_key=settings.JWT_SECRET_KEY, # 复用 JWT secret 或单独配置
session_cookie="admin_session",
max_age=3600, # 1 小时
)
# ============ 新增结束 ============
# 路由注册
app.include_router(xxx_router)
# ============ 新增:挂载管理面板 ============
# ⚠️ 必须在路由注册之后、app 运行之前调用
admin = create_admin(app)
# ============ 新增结束 ============
Phase 3: 検証
コードの生成後、以下の検証手順をユーザーに伝えてください:
- バックエンドを起動:
uvicorn app.main:app --reload - admin にアクセス:
http://localhost:8000/admin - ログイン:管理者のアカウントとパスワードを使用します
- 確認:
- サイドバーメニューが正しく表示される
- 言語切り替えボタンが右上(アバターの横)に表示される
- 一覧ページに Export / Filter / Show/Hide columns ボタンがある
- 行を選択すると batch actions のドロップダウンメニューが表示される
- Dashboard に統計カードが表示される
トラブルシューティングの記録
この skill は実際のプロジェクトでの経験をもとにまとめています。以下は解決済みの問題です:
| 問題 | 原因 | 解決策 |
|---|---|---|
| サイドバー/メニューが描画されない | Dashboard テンプレートで {% block body %} を使用し、layout.html のページ構造全体を上書きしていた | {% block content %} と {% block header %} を使用します |
| Export/Filter ボタンが表示されない | DataTables の i18n ファイル(dt/zh.json など)が 404 になり、JS の初期化に失敗していた | default_localeに対応する i18n ファイルが存在することを確認するか、enを使用します |
momentjs/zh.js が 404 になる | default_locale="zh"だが、starlette-admin には簡体字中国語の言語パックがない | enまたはzh_Hant(繁体字中国語)を使用します |
Admin() が TypeError を出す | Admin コンストラクターの第 1 引数は app ではなく engine | Admin(engine, ...) |
| 404 Not Found | base_urlとapp.mountのパスが重複している | base_url="/admin"、admin.mount_to(app) |
| ログインページに無限にリダイレクトされる | SessionMiddleware が追加されていない、または追加位置が誤っている | FastAPI アプリに、create_admin より前に SessionMiddleware を追加します |
No module named 'itsdangerous' | starlette のセッションミドルウェアの依存関係が不足している | pip install itsdangerous |
| AuthProvider の接続プールがリークする | 手動で作成した sync session が閉じられていない | try/finally: db.close()を使用して確実に閉じます |
| batch actions が表示されない | actions はデフォルトで display: none になっており、行を選択した後に表示される | 正常な動作です。行を選択すると表示されます |
| カスタムテンプレートから layout.html が見つからない | templates_dir にローカルディレクトリしか含まれず、starlette_admin パッケージのテンプレートが含まれていない | templates_dirパラメーターを使用します。Jinja2 ChoiceLoader が自動的に統合します |
| i18n の翻訳が反映されない | Admin.init が Jinja2 の gettext を上書きする | Admin.init の後に patch_admin_templates()を呼び出して再インストールします(上級者向け) |
認証の拡張
デフォルトのテンプレートでは、session + アカウントとパスワードによる認証を使用します。JWT 認証が必要な場合:
AdminAuthProvider.login()を変更し、使用している JWT ログイン API を呼び出しますis_authenticated()を変更し、Bearer token を検証します/adminパスに対して CORS を許可するようにフロントエンドを設定します
言語切り替え
I18nConfig(language_switcher=[...])を設定すると、トップナビゲーションバーの右側(ユーザーアバターの横)に言語のドロップダウンメニューが表示されます。starlette-admin に組み込まれている対応言語:
| コード | 言語 |
|---|---|
en | 英語 |
de | ドイツ語 |
fr | フランス語 |
pt | ポルトガル語 |
ru | ロシア語 |
tr | トルコ語 |
zh_Hant | 繁体字中国語 |
切り替え後の設定は cookie(デフォルト名は language)に保存され、LocaleMiddlewareが自動的に読み取ります。