mirror of
https://github.com/fastapi-admin/fastapi-admin.git
synced 2026-03-13 10:32:25 +08:00
Fix example
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Installation
|
||||
# 安装
|
||||
|
||||
## From pypi
|
||||
## 从 pypi
|
||||
|
||||
You can install from pypi.
|
||||
|
||||
@@ -8,7 +8,7 @@ You can install from pypi.
|
||||
> pip install fastapi-admin
|
||||
```
|
||||
|
||||
## From source
|
||||
## 从源码
|
||||
|
||||
Or you can install from source with latest code.
|
||||
|
||||
@@ -16,7 +16,7 @@ Or you can install from source with latest code.
|
||||
> pip install git+https://github.com/fastapi-admin/fastapi-admin.git
|
||||
```
|
||||
|
||||
### With requirements.txt
|
||||
### 使用 requirements.txt
|
||||
|
||||
Add the following line.
|
||||
|
||||
@@ -24,7 +24,7 @@ Add the following line.
|
||||
-e https://github.com/fastapi-admin/fastapi-admin.git@develop#egg=fastapi-admin
|
||||
```
|
||||
|
||||
### With poetry
|
||||
### 使用 poetry
|
||||
|
||||
Add the following line in section `[tool.poetry.dependencies]`.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Quickstart
|
||||
# 入门指南
|
||||
|
||||
`FastAPI-Admin` is easy to mount your `FastAPI` app, just need a few configs.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ theme:
|
||||
logo: https://raw.githubusercontent.com/fastapi-admin/fastapi-admin/dev/images/icon-white.svg
|
||||
favicon: https://raw.githubusercontent.com/fastapi-admin/fastapi-admin/dev/images/favicon.png
|
||||
name: material
|
||||
language: en
|
||||
language: zh
|
||||
icon:
|
||||
repo: fontawesome/brands/github
|
||||
palette:
|
||||
@@ -49,7 +49,11 @@ nav:
|
||||
- reference/file_upload.md
|
||||
- reference/middleware.md
|
||||
- 自定义:
|
||||
- custom/index.md
|
||||
- custom/page.md
|
||||
- custom/overwrite.md
|
||||
- custom/login.md
|
||||
- custom/file.md
|
||||
- custom/widget.md
|
||||
- 赞助者 Pro 版本:
|
||||
- pro/index.md
|
||||
- pro/sponsor.md
|
||||
@@ -69,5 +73,4 @@ copyright: Copyright © 2021 long2ice
|
||||
plugins:
|
||||
- git-revision-date-localized:
|
||||
type: datetime
|
||||
- markdownextradata
|
||||
dev_addr: 127.0.0.1:7999
|
||||
|
||||
@@ -32,7 +32,6 @@ def create_app():
|
||||
login_logo_url="https://preview.tabler.io/static/logo.svg",
|
||||
template_folders=[os.path.join(BASE_DIR, "templates")],
|
||||
login_provider=login_provider,
|
||||
maintenance=False,
|
||||
redis=redis,
|
||||
)
|
||||
|
||||
@@ -49,7 +48,12 @@ def create_app():
|
||||
app,
|
||||
config={
|
||||
"connections": {"default": settings.DATABASE_URL},
|
||||
"apps": {"models": {"models": ["examples.models"], "default_connection": "default"}},
|
||||
"apps": {
|
||||
"models": {
|
||||
"models": ["examples.models"],
|
||||
"default_connection": "default",
|
||||
}
|
||||
},
|
||||
},
|
||||
generate_schemas=True,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ async def home(
|
||||
resources=Depends(get_resources),
|
||||
):
|
||||
return templates.TemplateResponse(
|
||||
"main.html",
|
||||
"home.html",
|
||||
context={
|
||||
"request": request,
|
||||
"resources": resources,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from aioredis import Redis
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from tortoise import Model
|
||||
@@ -14,22 +15,25 @@ from .routes import router
|
||||
|
||||
class FastAdmin(FastAPI):
|
||||
logo_url: str
|
||||
login_logo_url: str
|
||||
admin_path: str
|
||||
resources: List[Type[Resource]] = []
|
||||
model_resources: Dict[Type[Model], Type[Resource]] = {}
|
||||
login_provider: Optional[Type[LoginProvider]] = LoginProvider
|
||||
redis: Redis
|
||||
|
||||
def configure(
|
||||
self,
|
||||
redis: Redis,
|
||||
logo_url: str = None,
|
||||
login_logo_url: str = None,
|
||||
default_locale: str = "en_US",
|
||||
admin_path: str = "/admin",
|
||||
template_folders: Optional[List[str]] = None,
|
||||
login_provider: Optional[Type[LoginProvider]] = LoginProvider,
|
||||
login_provider: Optional[LoginProvider] = None,
|
||||
):
|
||||
"""
|
||||
Config FastAdmin
|
||||
:param maintenance: If set True, all request will redirect to maintenance page
|
||||
:param logo_url:
|
||||
:param default_locale:
|
||||
:param admin_path:
|
||||
@@ -37,6 +41,8 @@ class FastAdmin(FastAPI):
|
||||
:param login_provider:
|
||||
:return:
|
||||
"""
|
||||
self.redis = redis
|
||||
self.login_logo_url = login_logo_url
|
||||
template.set_locale(default_locale)
|
||||
self.admin_path = admin_path
|
||||
self.logo_url = logo_url
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# time format
|
||||
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
DATE_FORMAT = "%Y-%m-%d"
|
||||
DATETIME_FORMAT_MOMENT = "YYYY-MM-DD HH:mm:ss"
|
||||
DATE_FORMAT_MOMENT = "YYYY-MM-DD"
|
||||
|
||||
# redis cache
|
||||
CAPTCHA_ID = "captcha:{captcha_id}"
|
||||
LOGIN_ERROR_TIMES = "login_error_times:{ip}"
|
||||
LOGIN_USER = "login_user:{token}"
|
||||
|
||||
@@ -48,3 +48,7 @@ def _get_resources(resources: List[Type[Resource]]):
|
||||
def get_resources(request: Request) -> List[dict]:
|
||||
resources = request.app.resources
|
||||
return _get_resources(resources)
|
||||
|
||||
|
||||
def get_redis(request: Request):
|
||||
return request.app.redis
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import uuid
|
||||
from gettext import gettext as _
|
||||
from typing import Callable, Type
|
||||
|
||||
import bcrypt
|
||||
from aioredis import Redis
|
||||
from fastapi import Depends
|
||||
from pydantic import EmailStr
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import RedirectResponse
|
||||
from starlette.status import HTTP_303_SEE_OTHER
|
||||
from starlette.status import HTTP_303_SEE_OTHER, HTTP_401_UNAUTHORIZED
|
||||
from tortoise import Model, fields
|
||||
|
||||
from fastapi_admin import constants
|
||||
from fastapi_admin.depends import get_redis
|
||||
from fastapi_admin.template import templates
|
||||
|
||||
|
||||
class LoginProvider:
|
||||
login_path = "/login"
|
||||
logout_path = "/logout"
|
||||
template = "login.html"
|
||||
def __init__(
|
||||
self, login_path="/login", logout_path="/logout", template="login.html"
|
||||
):
|
||||
self.template = template
|
||||
self.logout_path = logout_path
|
||||
self.login_path = login_path
|
||||
|
||||
@classmethod
|
||||
async def get(
|
||||
cls,
|
||||
self,
|
||||
request: Request,
|
||||
):
|
||||
return templates.TemplateResponse(cls.template, context={"request": request})
|
||||
return templates.TemplateResponse(self.template, context={"request": request})
|
||||
|
||||
@classmethod
|
||||
async def post(
|
||||
cls,
|
||||
self,
|
||||
request: Request,
|
||||
):
|
||||
"""
|
||||
@@ -34,21 +40,22 @@ class LoginProvider:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def authenticate(
|
||||
cls,
|
||||
self,
|
||||
request: Request,
|
||||
call_next: Callable,
|
||||
):
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
async def logout(cls, request: Request):
|
||||
def redirect_login(self, request: Request):
|
||||
return RedirectResponse(
|
||||
url=request.app.admin_path + cls.login_path, status_code=HTTP_303_SEE_OTHER
|
||||
url=request.app.admin_path + self.login_path, status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
async def logout(self, request: Request):
|
||||
return self.redirect_login(request)
|
||||
|
||||
|
||||
class UserMixin(Model):
|
||||
username = fields.CharField(max_length=50, unique=True)
|
||||
@@ -60,44 +67,128 @@ class UserMixin(Model):
|
||||
|
||||
|
||||
class UsernamePasswordProvider(LoginProvider):
|
||||
model: Type[UserMixin]
|
||||
access_token = "access_token"
|
||||
|
||||
@classmethod
|
||||
async def post(
|
||||
cls,
|
||||
request: Request,
|
||||
def __init__(
|
||||
self,
|
||||
user_model: Type[UserMixin],
|
||||
enable_captcha: bool = False,
|
||||
login_path="/login",
|
||||
logout_path="/logout",
|
||||
template="login.html",
|
||||
):
|
||||
super().__init__(login_path, logout_path, template)
|
||||
self.user_model = user_model
|
||||
self.enable_captcha = enable_captcha
|
||||
|
||||
async def captcha(
|
||||
self,
|
||||
request: Request,
|
||||
width: int = 160,
|
||||
height: int = 60,
|
||||
redis: Redis = Depends(get_redis),
|
||||
):
|
||||
if not self.enable_captcha:
|
||||
raise ConfigurationError(error="Should enable captcha first")
|
||||
captcha = ImageCaptcha(width=width, height=height)
|
||||
code = utils.generate_random_str(4)
|
||||
captcha_id = uuid.uuid4().hex
|
||||
captcha_key = constants.CAPTCHA_ID.format(captcha_id=captcha_id)
|
||||
image = captcha.generate(code)
|
||||
response = StreamingResponse(content=image, media_type="image/png")
|
||||
await redis.set(captcha_key, code, expire=60)
|
||||
response.set_cookie(
|
||||
"captcha_id",
|
||||
captcha_id,
|
||||
max_age=60,
|
||||
path=request.app.admin_path,
|
||||
httponly=True,
|
||||
)
|
||||
return response
|
||||
|
||||
async def post(self, request: Request, redis: Redis = Depends(get_redis)):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
user = await cls.model.get_or_none(username=username)
|
||||
if not user:
|
||||
return templates.TemplateResponse(
|
||||
cls.template, context={"request": request, "error": _("no_such_user")}
|
||||
)
|
||||
if not cls.check_password(user, password):
|
||||
return templates.TemplateResponse(
|
||||
cls.template, context={"request": request, "error": _("password_error")}
|
||||
)
|
||||
return RedirectResponse(url=request.app.admin_path, status_code=HTTP_303_SEE_OTHER)
|
||||
remember_me = form.get("remember_me")
|
||||
|
||||
@classmethod
|
||||
def check_password(cls, user: UserMixin, password: str):
|
||||
user = await self.user_model.get_or_none(username=username)
|
||||
if not user or not self.check_password(user, password):
|
||||
return templates.TemplateResponse(
|
||||
self.template,
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
context={"request": request, "error": _("login_failed")},
|
||||
)
|
||||
response = RedirectResponse(
|
||||
url=request.app.admin_path, status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
if remember_me == "on":
|
||||
expire = 3600 * 24 * 30
|
||||
response.set_cookie("remember_me", "on")
|
||||
else:
|
||||
expire = 3600
|
||||
response.delete_cookie("remember_me")
|
||||
token = uuid.uuid4().hex
|
||||
response.set_cookie(
|
||||
self.access_token,
|
||||
token,
|
||||
expires=expire,
|
||||
path=request.app.admin_path,
|
||||
httponly=True,
|
||||
)
|
||||
await redis.set(
|
||||
constants.LOGIN_USER.format(token=token), user.pk, expire=expire
|
||||
)
|
||||
return response
|
||||
|
||||
async def logout(self, request: Request, redis: Redis = Depends(get_redis)):
|
||||
response = await super(UsernamePasswordProvider, self).logout(request)
|
||||
response.delete_cookie(self.access_token)
|
||||
token = request.cookies.get(self.access_token)
|
||||
await redis.delete(constants.LOGIN_USER.format(token=token))
|
||||
return response
|
||||
|
||||
async def authenticate(
|
||||
self,
|
||||
request: Request,
|
||||
call_next: Callable,
|
||||
):
|
||||
redis = request.app.redis # type:Redis
|
||||
token = request.cookies.get(self.access_token)
|
||||
path = request.scope["path"]
|
||||
token_key = constants.LOGIN_USER.format(token=token)
|
||||
user_id = await redis.get(token_key)
|
||||
if not user_id and path != self.login_path:
|
||||
return self.redirect_login(request)
|
||||
user = await self.user_model.get_or_none(pk=user_id)
|
||||
if not user:
|
||||
if path != self.login_path:
|
||||
response = self.redirect_login(request)
|
||||
response.delete_cookie(self.access_token)
|
||||
return response
|
||||
else:
|
||||
if path == self.login_path:
|
||||
return RedirectResponse(
|
||||
url=request.app.admin_path, status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
request.state.user = user
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
def check_password(self, user: UserMixin, password: str):
|
||||
return bcrypt.checkpw(password.encode(), user.password.encode())
|
||||
|
||||
@classmethod
|
||||
def hash_password(cls, password: str):
|
||||
def hash_password(self, password: str):
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
@classmethod
|
||||
async def create_user(cls, username: str, password: str, email: EmailStr):
|
||||
return await cls.model.create(
|
||||
async def create_user(self, username: str, password: str, email: EmailStr):
|
||||
return await self.user_model.create(
|
||||
username=username,
|
||||
password=cls.hash_password(password),
|
||||
password=self.hash_password(password),
|
||||
email=email,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def update_password(cls, user: UserMixin, password: str):
|
||||
user.password = cls.hash_password(password)
|
||||
async def update_password(self, user: UserMixin, password: str):
|
||||
user.password = self.hash_password(password)
|
||||
await user.save(update_fields=["password"])
|
||||
|
||||
Reference in New Issue
Block a user