Fix example

This commit is contained in:
long2ice
2021-04-28 22:51:53 +08:00
parent 5e230eca90
commit 8bb34f2032
9 changed files with 168 additions and 54 deletions

View File

@@ -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]`.

View File

@@ -1,4 +1,4 @@
# Quickstart
# 入门指南
`FastAPI-Admin` is easy to mount your `FastAPI` app, just need a few configs.

View File

@@ -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

View File

@@ -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,
)

View File

@@ -12,7 +12,7 @@ async def home(
resources=Depends(get_resources),
):
return templates.TemplateResponse(
"main.html",
"home.html",
context={
"request": request,
"resources": resources,

View File

@@ -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

View File

@@ -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}"

View File

@@ -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

View File

@@ -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"])