mirror of
https://github.com/fastapi-admin/fastapi-admin.git
synced 2025-08-14 02:28:40 +08:00
error pages
This commit is contained in:
14
README.md
14
README.md
@ -43,6 +43,20 @@ Or pro version online demo [here](https://fastapi-admin-pro.long2ice.cn/admin/lo
|
||||
|
||||
## Run examples in local
|
||||
|
||||
1. Clone repo.
|
||||
2. Create `.env` file.
|
||||
|
||||
```dotenv
|
||||
DATABASE_URL=mysql://root:123456@127.0.0.1:3306/fastapi-admin
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
```
|
||||
|
||||
3. Run `docker-compose up -d --build`.
|
||||
4. Visit <http://localhost:8000/admin/init> to create first admin.
|
||||
|
||||
## Documentation
|
||||
|
||||
See documentation at [https://fastapi-admin.github.io/fastapi-admin](https://fastapi-admin.github.io/fastapi-admin).
|
||||
|
@ -3,9 +3,7 @@ services:
|
||||
app:
|
||||
build: .
|
||||
restart: always
|
||||
environment:
|
||||
DATABASE_URL: mysql://root:123456@127.0.0.1:3306/fastapi-admin
|
||||
TZ: Asia/Shanghai
|
||||
env_file: .env
|
||||
network_mode: host
|
||||
image: fastapi-admin
|
||||
command: uvicorn examples.main:app_ --port 8000 --host 0.0.0.0
|
||||
|
@ -1 +0,0 @@
|
||||
# File Upload Provider
|
1
docs/en/docs/custom/file_upload.md
Normal file
1
docs/en/docs/custom/file_upload.md
Normal file
@ -0,0 +1 @@
|
||||
# File Upload
|
@ -78,12 +78,13 @@ The `Model` make a TortoiseORM model as a menu with CURD page.
|
||||
|
||||
from examples.models import Admin
|
||||
from fastapi_admin.app import app
|
||||
from fastapi_admin.resources import Field, Model,Action
|
||||
from fastapi_admin.resources import Field, Model, Action
|
||||
from fastapi_admin.widgets import displays, filters, inputs
|
||||
from typing import List
|
||||
from fastapi_admin.providers.file_upload import FileUploadProvider
|
||||
from fastapi_admin.file_upload import FileUpload
|
||||
|
||||
upload_provider = FileUpload(uploads_dir=os.path.join(BASE_DIR, "static", "uploads"))
|
||||
|
||||
upload_provider = FileUploadProvider(uploads_dir=os.path.join(BASE_DIR, "static", "uploads"))
|
||||
|
||||
@app.register
|
||||
class AdminResource(Model):
|
||||
|
@ -18,48 +18,13 @@ admin_app.add_middleware(BaseHTTPMiddleware, dispatch=LoginPasswordMaxTryMiddlew
|
||||
|
||||
## Permission Control
|
||||
|
||||
## Additional File Upload Providers
|
||||
## Additional File Upload
|
||||
|
||||
### ALiYunOSSProvider
|
||||
### ALiYunOSS
|
||||
|
||||
### AwsS3Provider
|
||||
### AwsS3
|
||||
|
||||
## Error pages
|
||||
|
||||
### 403
|
||||
|
||||
You can catch all `403` error to show builtin `403` page.
|
||||
|
||||
```python
|
||||
from fastapi_admin.exceptions import forbidden_error_exception
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
admin_app.add_exception_handler(HTTP_403_FORBIDDEN, forbidden_error_exception)
|
||||
```
|
||||
|
||||
### 404
|
||||
|
||||
You can catch all `404` error to show builtin `404` page.
|
||||
|
||||
```python
|
||||
from fastapi_admin.exceptions import not_found_error_exception
|
||||
from starlette.status import HTTP_404_NOT_FOUND
|
||||
|
||||
app.add_exception_handler(HTTP_404_NOT_FOUND, not_found_error_exception)
|
||||
```
|
||||
|
||||
### 500
|
||||
|
||||
You can catch all `500` error to show builtin `500` page.
|
||||
|
||||
```python
|
||||
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
|
||||
from fastapi_admin.exceptions import server_error_exception
|
||||
|
||||
app.add_exception_handler(HTTP_500_INTERNAL_SERVER_ERROR, server_error_exception)
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
## Maintenance
|
||||
|
||||
If your site is in maintenance, you can set `true` to `admin_app.configure(...)`.
|
||||
|
||||
@ -69,16 +34,16 @@ admin_app.configure(maintenance=True)
|
||||
|
||||
## Admin Log
|
||||
|
||||
If you want to log all `create/update/delete` actions, you can set `admin_log_provider` to `admin_app.configure(...)`.
|
||||
If you want to log all `create/update/delete` actions, you can add `AdminLogProvider` to `admin_app.configure(...)`.
|
||||
|
||||
```python
|
||||
admin_app.configure(admin_log_provider=AdminLogProvider(Log))
|
||||
admin_app.configure(providers=[AdminLogProvider(Log)])
|
||||
```
|
||||
|
||||
## Site Search
|
||||
|
||||
You can enable site search by set `search_provider` to `admin_app.configure(...)`.
|
||||
You can enable site search by add `SearchProvider` to `admin_app.configure(...)`.
|
||||
|
||||
```python
|
||||
admin_app.configure(search_provider=SearchProvider())
|
||||
admin_app.configure(providers=[SearchProvider()])
|
||||
```
|
||||
|
@ -1 +1,3 @@
|
||||
# Error Pages (💗 Pro only)
|
||||
# Error Pages
|
||||
|
||||
`fastapi-admin` will catch all `403`,`404`,`500` errors and redirect to builtin error pages default.
|
||||
|
@ -58,11 +58,12 @@ nav:
|
||||
- custom/page.md
|
||||
- custom/overwrite.md
|
||||
- custom/widget.md
|
||||
- custom/login.md
|
||||
- custom/file.md
|
||||
- custom/search.md
|
||||
- custom/admin_log.md
|
||||
- custom/permission.md
|
||||
- custom/file_upload.md
|
||||
- Providers:
|
||||
- custom/providers/login.md
|
||||
- custom/providers/search.md
|
||||
- custom/providers/admin_log.md
|
||||
- custom/providers/permission.md
|
||||
- Pro Version For Sponsor:
|
||||
- pro/sponsor.md
|
||||
- pro/exclusive.md
|
||||
|
@ -14,8 +14,6 @@ from examples.models import Admin
|
||||
from examples.providers import LoginProvider
|
||||
from fastapi_admin.app import app as admin_app
|
||||
|
||||
login_provider = LoginProvider(admin_model=Admin)
|
||||
|
||||
|
||||
def create_app():
|
||||
app = FastAPI()
|
||||
@ -37,11 +35,11 @@ def create_app():
|
||||
password=settings.REDIS_PASSWORD,
|
||||
encoding="utf8",
|
||||
)
|
||||
admin_app.configure(
|
||||
await admin_app.configure(
|
||||
logo_url="https://preview.tabler.io/static/logo-white.svg",
|
||||
login_logo_url="https://preview.tabler.io/static/logo.svg",
|
||||
template_folders=[os.path.join(BASE_DIR, "templates")],
|
||||
login_provider=login_provider,
|
||||
providers=[LoginProvider(admin_model=Admin)],
|
||||
redis=redis,
|
||||
)
|
||||
|
||||
|
@ -3,7 +3,7 @@ import datetime
|
||||
from tortoise import Model, fields
|
||||
|
||||
from examples.enums import ProductType, Status
|
||||
from fastapi_admin.providers.login import AbstractAdmin
|
||||
from fastapi_admin.models import AbstractAdmin
|
||||
|
||||
|
||||
class Admin(AbstractAdmin):
|
||||
|
@ -5,11 +5,11 @@ from examples import enums
|
||||
from examples.constants import BASE_DIR
|
||||
from examples.models import Admin, Category, Config, Product
|
||||
from fastapi_admin.app import app
|
||||
from fastapi_admin.providers.file_upload import FileUploadProvider
|
||||
from fastapi_admin.file_upload import FileUpload
|
||||
from fastapi_admin.resources import Action, Dropdown, Field, Link, Model
|
||||
from fastapi_admin.widgets import displays, filters, inputs
|
||||
|
||||
upload_provider = FileUploadProvider(uploads_dir=os.path.join(BASE_DIR, "static", "uploads"))
|
||||
upload_provider = FileUpload(uploads_dir=os.path.join(BASE_DIR, "static", "uploads"))
|
||||
|
||||
|
||||
@app.register
|
||||
|
@ -3,28 +3,33 @@ from typing import Dict, List, Optional, Type
|
||||
from aioredis import Redis
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.status import HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_500_INTERNAL_SERVER_ERROR
|
||||
from tortoise import Model
|
||||
|
||||
from fastapi_admin import i18n
|
||||
from fastapi_admin.exceptions import (
|
||||
forbidden_error_exception,
|
||||
not_found_error_exception,
|
||||
server_error_exception,
|
||||
)
|
||||
|
||||
from . import middlewares, template
|
||||
from .providers.login import LoginProvider
|
||||
from .providers import Provider
|
||||
from .resources import Dropdown
|
||||
from .resources import Model as ModelResource
|
||||
from .resources import Resource
|
||||
from .routes import router
|
||||
|
||||
|
||||
class FastAdmin(FastAPI):
|
||||
class FastAPIAdmin(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[LoginProvider]
|
||||
redis: Redis
|
||||
|
||||
def configure(
|
||||
async def configure(
|
||||
self,
|
||||
redis: Redis,
|
||||
logo_url: str = None,
|
||||
@ -32,17 +37,8 @@ class FastAdmin(FastAPI):
|
||||
default_locale: str = "en_US",
|
||||
admin_path: str = "/admin",
|
||||
template_folders: Optional[List[str]] = None,
|
||||
login_provider: Optional[LoginProvider] = None,
|
||||
providers: Optional[List[Provider]] = None,
|
||||
):
|
||||
"""
|
||||
Config FastAdmin
|
||||
:param logo_url:
|
||||
:param default_locale:
|
||||
:param admin_path:
|
||||
:param template_folders:
|
||||
:param login_provider:
|
||||
:return:
|
||||
"""
|
||||
self.redis = redis
|
||||
self.login_logo_url = login_logo_url
|
||||
i18n.set_locale(default_locale)
|
||||
@ -50,16 +46,11 @@ class FastAdmin(FastAPI):
|
||||
self.logo_url = logo_url
|
||||
if template_folders:
|
||||
template.add_template_folder(*template_folders)
|
||||
self.login_provider = login_provider
|
||||
self._register_providers()
|
||||
await self._register_providers(providers)
|
||||
|
||||
def _register_providers(self):
|
||||
if self.login_provider:
|
||||
login_path = self.login_provider.login_path
|
||||
app.get(login_path)(self.login_provider.get)
|
||||
app.post(login_path)(self.login_provider.post)
|
||||
app.get(self.login_provider.logout_path)(self.login_provider.logout)
|
||||
app.add_middleware(BaseHTTPMiddleware, dispatch=self.login_provider.authenticate)
|
||||
async def _register_providers(self, providers: Optional[List[Provider]] = None):
|
||||
for p in providers or []:
|
||||
await p.register(self)
|
||||
|
||||
def register_resources(self, *resource: Type[Resource]):
|
||||
for r in resource:
|
||||
@ -80,9 +71,12 @@ class FastAdmin(FastAPI):
|
||||
return self.model_resources[model]()
|
||||
|
||||
|
||||
app = FastAdmin(
|
||||
app = FastAPIAdmin(
|
||||
title="FastAdmin",
|
||||
description="A fast admin dashboard based on fastapi and tortoise-orm with tabler ui.",
|
||||
)
|
||||
app.add_middleware(BaseHTTPMiddleware, dispatch=middlewares.language_processor)
|
||||
app.add_exception_handler(HTTP_500_INTERNAL_SERVER_ERROR, server_error_exception)
|
||||
app.add_exception_handler(HTTP_404_NOT_FOUND, not_found_error_exception)
|
||||
app.add_exception_handler(HTTP_403_FORBIDDEN, forbidden_error_exception)
|
||||
app.include_router(router)
|
||||
|
@ -1,6 +1,9 @@
|
||||
from fastapi import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
|
||||
|
||||
from fastapi_admin.template import templates
|
||||
|
||||
|
||||
class ServerHTTPException(HTTPException):
|
||||
def __init__(self, error: str = None):
|
||||
@ -31,3 +34,23 @@ class FileExtNotAllowed(ServerHTTPException):
|
||||
"""
|
||||
raise when the upload file ext not allowed
|
||||
"""
|
||||
|
||||
|
||||
async def server_error_exception(request: Request, exc: HTTPException):
|
||||
return templates.TemplateResponse(
|
||||
"errors/500.html",
|
||||
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
context={"request": request},
|
||||
)
|
||||
|
||||
|
||||
async def not_found_error_exception(request: Request, exc: HTTPException):
|
||||
return templates.TemplateResponse(
|
||||
"errors/404.html", status_code=exc.status_code, context={"request": request}
|
||||
)
|
||||
|
||||
|
||||
async def forbidden_error_exception(request: Request, exc: HTTPException):
|
||||
return templates.TemplateResponse(
|
||||
"errors/403.html", status_code=exc.status_code, context={"request": request}
|
||||
)
|
||||
|
@ -7,7 +7,7 @@ from starlette.datastructures import UploadFile
|
||||
from fastapi_admin.exceptions import FileExtNotAllowed, FileMaxSizeLimit
|
||||
|
||||
|
||||
class FileUploadProvider:
|
||||
class FileUpload:
|
||||
def __init__(
|
||||
self,
|
||||
uploads_dir: str,
|
@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2021-05-02 14:00+0800\n"
|
||||
"POT-Creation-Date: 2021-05-05 14:09+0800\n"
|
||||
"PO-Revision-Date: 2021-04-16 22:46+0800\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en_US\n"
|
||||
@ -18,27 +18,31 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
|
||||
#: fastapi_admin/resources.py:80
|
||||
#: fastapi_admin/resources.py:75
|
||||
msgid "update"
|
||||
msgstr "Update"
|
||||
|
||||
#: fastapi_admin/resources.py:81
|
||||
#: fastapi_admin/resources.py:76
|
||||
msgid "delete"
|
||||
msgstr "Delete"
|
||||
|
||||
#: fastapi_admin/resources.py:87
|
||||
#: fastapi_admin/resources.py:81
|
||||
msgid "delete_selected"
|
||||
msgstr "Delete Selected"
|
||||
|
||||
#: fastapi_admin/providers/login.py:89
|
||||
#: fastapi_admin/providers/login.py:68
|
||||
msgid "login_failed"
|
||||
msgstr "Login to your account failed"
|
||||
|
||||
#: fastapi_admin/routes/password.py:39
|
||||
#: fastapi_admin/providers/login.py:152
|
||||
msgid "confirm_password_different"
|
||||
msgstr "Password is different"
|
||||
|
||||
#: fastapi_admin/providers/login.py:187 fastapi_admin/routes/password.py:38
|
||||
msgid "old_password_error"
|
||||
msgstr "Old password error"
|
||||
|
||||
#: fastapi_admin/routes/password.py:41
|
||||
#: fastapi_admin/providers/login.py:189 fastapi_admin/routes/password.py:40
|
||||
msgid "new_password_different"
|
||||
msgstr "New password is different"
|
||||
|
||||
@ -89,27 +93,23 @@ msgstr "Bulk Actions"
|
||||
msgid "create"
|
||||
msgstr "Create"
|
||||
|
||||
#: fastapi_admin/templates/list.html:126
|
||||
#: fastapi_admin/templates/list.html:132
|
||||
msgid "actions"
|
||||
msgstr "Actions"
|
||||
|
||||
#: fastapi_admin/templates/list.html:157
|
||||
#: fastapi_admin/templates/list.html:163
|
||||
#, python-format
|
||||
msgid "Showing %(from)s to %(to)s of %(total)s entries"
|
||||
msgstr ""
|
||||
|
||||
#: fastapi_admin/templates/list.html:168
|
||||
#: fastapi_admin/templates/list.html:174
|
||||
msgid "prev_page"
|
||||
msgstr "Prev"
|
||||
|
||||
#: fastapi_admin/templates/list.html:189
|
||||
#: fastapi_admin/templates/list.html:195
|
||||
msgid "next_page"
|
||||
msgstr "Next"
|
||||
|
||||
#: fastapi_admin/templates/login.html:38
|
||||
msgid "login_title"
|
||||
msgstr "Login to your account"
|
||||
|
||||
#: fastapi_admin/templates/login.html:40
|
||||
msgid "username"
|
||||
msgstr "Username"
|
||||
@ -165,3 +165,10 @@ msgstr "Submit"
|
||||
#: fastapi_admin/templates/update.html:23
|
||||
msgid "save_and_return"
|
||||
msgstr "Save and return"
|
||||
|
||||
#: fastapi_admin/templates/errors/403.html:21
|
||||
#: fastapi_admin/templates/errors/404.html:21
|
||||
#: fastapi_admin/templates/errors/500.html:21
|
||||
msgid "return_home"
|
||||
msgstr "Take me home"
|
||||
|
||||
|
@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2021-05-02 14:00+0800\n"
|
||||
"POT-Creation-Date: 2021-05-05 14:09+0800\n"
|
||||
"PO-Revision-Date: 2021-04-16 22:46+0800\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: zh_Hans_CN\n"
|
||||
@ -18,27 +18,31 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
|
||||
#: fastapi_admin/resources.py:80
|
||||
#: fastapi_admin/resources.py:75
|
||||
msgid "update"
|
||||
msgstr "编辑"
|
||||
|
||||
#: fastapi_admin/resources.py:81
|
||||
#: fastapi_admin/resources.py:76
|
||||
msgid "delete"
|
||||
msgstr "删除"
|
||||
|
||||
#: fastapi_admin/resources.py:87
|
||||
#: fastapi_admin/resources.py:81
|
||||
msgid "delete_selected"
|
||||
msgstr "删除选中"
|
||||
|
||||
#: fastapi_admin/providers/login.py:89
|
||||
#: fastapi_admin/providers/login.py:68
|
||||
msgid "login_failed"
|
||||
msgstr "登录管理台"
|
||||
|
||||
#: fastapi_admin/routes/password.py:39
|
||||
#: fastapi_admin/providers/login.py:152
|
||||
msgid "confirm_password_different"
|
||||
msgstr "密码不一致"
|
||||
|
||||
#: fastapi_admin/providers/login.py:187 fastapi_admin/routes/password.py:38
|
||||
msgid "old_password_error"
|
||||
msgstr "旧密码错误"
|
||||
|
||||
#: fastapi_admin/routes/password.py:41
|
||||
#: fastapi_admin/providers/login.py:189 fastapi_admin/routes/password.py:40
|
||||
msgid "new_password_different"
|
||||
msgstr "新密码不一致"
|
||||
|
||||
@ -89,27 +93,23 @@ msgstr "批量操作"
|
||||
msgid "create"
|
||||
msgstr "创建"
|
||||
|
||||
#: fastapi_admin/templates/list.html:126
|
||||
#: fastapi_admin/templates/list.html:132
|
||||
msgid "actions"
|
||||
msgstr "动作"
|
||||
|
||||
#: fastapi_admin/templates/list.html:157
|
||||
#: fastapi_admin/templates/list.html:163
|
||||
#, python-format
|
||||
msgid "Showing %(from)s to %(to)s of %(total)s entries"
|
||||
msgstr "显示 %(from)s 到 %(to)s 共 %(total)s 项"
|
||||
|
||||
#: fastapi_admin/templates/list.html:168
|
||||
#: fastapi_admin/templates/list.html:174
|
||||
msgid "prev_page"
|
||||
msgstr "上一页"
|
||||
|
||||
#: fastapi_admin/templates/list.html:189
|
||||
#: fastapi_admin/templates/list.html:195
|
||||
msgid "next_page"
|
||||
msgstr "下一页"
|
||||
|
||||
#: fastapi_admin/templates/login.html:38
|
||||
msgid "login_title"
|
||||
msgstr "登录管理台"
|
||||
|
||||
#: fastapi_admin/templates/login.html:40
|
||||
msgid "username"
|
||||
msgstr "用户名"
|
||||
@ -165,3 +165,10 @@ msgstr "提交"
|
||||
#: fastapi_admin/templates/update.html:23
|
||||
msgid "save_and_return"
|
||||
msgstr "保存并返回"
|
||||
|
||||
#: fastapi_admin/templates/errors/403.html:21
|
||||
#: fastapi_admin/templates/errors/404.html:21
|
||||
#: fastapi_admin/templates/errors/500.html:21
|
||||
msgid "return_home"
|
||||
msgstr "返回首页"
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from fastapi_admin.app import FastAPIAdmin
|
||||
|
||||
|
||||
class Provider:
|
||||
name = "provider"
|
||||
|
||||
async def register(self, app: "FastAPIAdmin"):
|
||||
setattr(app, self.name, self)
|
||||
|
@ -1,69 +1,29 @@
|
||||
import typing
|
||||
import uuid
|
||||
from typing import Callable, Type
|
||||
from typing import Type
|
||||
|
||||
import bcrypt
|
||||
from aioredis import Redis
|
||||
from fastapi import Depends
|
||||
from pydantic import EmailStr
|
||||
from fastapi import Depends, Form
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import RedirectResponse
|
||||
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.depends import get_current_admin, get_redis, get_resources
|
||||
from fastapi_admin.i18n import _
|
||||
from fastapi_admin.models import AbstractAdmin
|
||||
from fastapi_admin.providers import Provider
|
||||
from fastapi_admin.template import templates
|
||||
from fastapi_admin.utils import check_password, hash_password
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from fastapi_admin.app import FastAPIAdmin
|
||||
|
||||
|
||||
class LoginProvider:
|
||||
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
|
||||
class UsernamePasswordProvider(Provider):
|
||||
name = "login_provider"
|
||||
|
||||
async def get(
|
||||
self,
|
||||
request: Request,
|
||||
):
|
||||
return templates.TemplateResponse(self.template, context={"request": request})
|
||||
|
||||
async def post(
|
||||
self,
|
||||
request: Request,
|
||||
):
|
||||
"""
|
||||
Post login
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
|
||||
async def authenticate(
|
||||
self,
|
||||
request: Request,
|
||||
call_next: Callable,
|
||||
):
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
def redirect_login(self, request: Request):
|
||||
return RedirectResponse(
|
||||
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 AbstractAdmin(Model):
|
||||
username = fields.CharField(max_length=50, unique=True)
|
||||
password = fields.CharField(max_length=200)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class UsernamePasswordProvider(LoginProvider):
|
||||
access_token = "access_token"
|
||||
|
||||
def __init__(
|
||||
@ -72,17 +32,39 @@ class UsernamePasswordProvider(LoginProvider):
|
||||
login_path="/login",
|
||||
logout_path="/logout",
|
||||
template="login.html",
|
||||
login_title="Login to your account",
|
||||
):
|
||||
super().__init__(login_path, logout_path, template)
|
||||
self.login_path = login_path
|
||||
self.logout_path = logout_path
|
||||
self.template = template
|
||||
self.admin_model = admin_model
|
||||
self.login_title = login_title
|
||||
|
||||
async def post(self, request: Request, redis: Redis = Depends(get_redis)):
|
||||
async def login_view(
|
||||
self,
|
||||
request: Request,
|
||||
):
|
||||
return templates.TemplateResponse(self.template, context={"request": request})
|
||||
|
||||
async def register(self, app: "FastAPIAdmin"):
|
||||
await super(UsernamePasswordProvider, self).register(app)
|
||||
login_path = self.login_path
|
||||
app.get(login_path)(self.login_view)
|
||||
app.post(login_path)(self.login)
|
||||
app.get(self.logout_path)(self.logout)
|
||||
app.add_middleware(BaseHTTPMiddleware, dispatch=self.authenticate)
|
||||
app.get("/init")(self.init_view)
|
||||
app.post("/init")(self.init)
|
||||
app.get("/password")(self.password_view)
|
||||
app.post("/password")(self.password)
|
||||
|
||||
async def login(self, request: Request, redis: Redis = Depends(get_redis)):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
remember_me = form.get("remember_me")
|
||||
admin = await self.admin_model.get_or_none(username=username)
|
||||
if not admin or not self.check_password(admin, password):
|
||||
if not admin or not check_password(password, admin.password):
|
||||
return templates.TemplateResponse(
|
||||
self.template,
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
@ -106,52 +88,106 @@ class UsernamePasswordProvider(LoginProvider):
|
||||
await redis.set(constants.LOGIN_USER.format(token=token), admin.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)
|
||||
async def logout(self, request: Request):
|
||||
response = self.redirect_login(request)
|
||||
response.delete_cookie(self.access_token, path=request.app.admin_path)
|
||||
token = request.cookies.get(self.access_token)
|
||||
await redis.delete(constants.LOGIN_USER.format(token=token))
|
||||
await request.app.redis.delete(constants.LOGIN_USER.format(token=token))
|
||||
return response
|
||||
|
||||
async def authenticate(
|
||||
self,
|
||||
request: Request,
|
||||
call_next: Callable,
|
||||
call_next: RequestResponseEndpoint,
|
||||
):
|
||||
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:
|
||||
if not token and path != self.login_path and path != "/init":
|
||||
return self.redirect_login(request)
|
||||
admin = await self.admin_model.get_or_none(pk=user_id)
|
||||
if not admin:
|
||||
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)
|
||||
token_key = constants.LOGIN_USER.format(token=token)
|
||||
admin_id = await redis.get(token_key)
|
||||
admin = await self.admin_model.get_or_none(pk=admin_id)
|
||||
request.state.admin = admin
|
||||
|
||||
if path == self.login_path and admin:
|
||||
return RedirectResponse(url=request.app.admin_path, status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
def check_password(self, admin: AbstractAdmin, password: str):
|
||||
return bcrypt.checkpw(password.encode(), admin.password.encode())
|
||||
|
||||
def hash_password(self, password: str):
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
async def create_admin(self, username: str, password: str, email: EmailStr):
|
||||
async def create_user(self, username: str, password: str):
|
||||
return await self.admin_model.create(
|
||||
username=username,
|
||||
password=self.hash_password(password),
|
||||
email=email,
|
||||
password=hash_password(password),
|
||||
)
|
||||
|
||||
async def update_password(self, admin: AbstractAdmin, password: str):
|
||||
admin.password = self.hash_password(password)
|
||||
admin.password = hash_password(password)
|
||||
await admin.save(update_fields=["password"])
|
||||
|
||||
async def init_view(self, request: Request):
|
||||
exists = await self.admin_model.all().limit(1).exists()
|
||||
if exists:
|
||||
return self.redirect_login(request)
|
||||
return templates.TemplateResponse("init.html", context={"request": request})
|
||||
|
||||
async def init(
|
||||
self,
|
||||
request: Request,
|
||||
):
|
||||
exists = await self.admin_model.all().limit(1).exists()
|
||||
if exists:
|
||||
return self.redirect_login(request)
|
||||
form = await request.form()
|
||||
password = form.get("password")
|
||||
confirm_password = form.get("confirm_password")
|
||||
username = form.get("username")
|
||||
if password != confirm_password:
|
||||
return templates.TemplateResponse(
|
||||
"init.html",
|
||||
context={"request": request, "error": _("confirm_password_different")},
|
||||
)
|
||||
|
||||
await self.create_user(username, password)
|
||||
return self.redirect_login(request)
|
||||
|
||||
def redirect_login(self, request: Request):
|
||||
return RedirectResponse(
|
||||
url=request.app.admin_path + self.login_path, status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
async def password_view(
|
||||
self,
|
||||
request: Request,
|
||||
resources=Depends(get_resources),
|
||||
):
|
||||
return templates.TemplateResponse(
|
||||
"password.html",
|
||||
context={
|
||||
"request": request,
|
||||
"resources": resources,
|
||||
},
|
||||
)
|
||||
|
||||
async def password(
|
||||
self,
|
||||
request: Request,
|
||||
old_password: str = Form(...),
|
||||
new_password: str = Form(...),
|
||||
re_new_password: str = Form(...),
|
||||
admin: AbstractAdmin = Depends(get_current_admin),
|
||||
resources=Depends(get_resources),
|
||||
):
|
||||
error = None
|
||||
if not check_password(old_password, admin.password):
|
||||
error = _("old_password_error")
|
||||
elif new_password != re_new_password:
|
||||
error = _("new_password_different")
|
||||
if error:
|
||||
return templates.TemplateResponse(
|
||||
"password.html",
|
||||
context={"request": request, "resources": resources, "error": error},
|
||||
)
|
||||
await self.update_password(admin, new_password)
|
||||
return await self.logout(request)
|
||||
|
27
fastapi_admin/templates/errors/403.html
Normal file
27
fastapi_admin/templates/errors/403.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block body %}
|
||||
<div class="page page-center">
|
||||
<div class="container-tight py-4">
|
||||
<div class="empty">
|
||||
<div class="empty-header">403</div>
|
||||
<p class="empty-title">Oops… You are forbidden</p>
|
||||
<p class="empty-subtitle text-muted">
|
||||
We are sorry but the page you are looking for was forbidden
|
||||
</p>
|
||||
<div class="empty-action">
|
||||
<a href="{{ request.app.admin_path }}" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<line x1="5" y1="12" x2="11" y2="18"/>
|
||||
<line x1="5" y1="12" x2="11" y2="6"/>
|
||||
</svg>
|
||||
{{ _('return_home') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
27
fastapi_admin/templates/errors/404.html
Normal file
27
fastapi_admin/templates/errors/404.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block body %}
|
||||
<div class="page page-center">
|
||||
<div class="container-tight py-4">
|
||||
<div class="empty">
|
||||
<div class="empty-header">404</div>
|
||||
<p class="empty-title">Oops… You just found an error page</p>
|
||||
<p class="empty-subtitle text-muted">
|
||||
We are sorry but the page you are looking for was not found
|
||||
</p>
|
||||
<div class="empty-action">
|
||||
<a href="{{ request.app.admin_path }}" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<line x1="5" y1="12" x2="11" y2="18"/>
|
||||
<line x1="5" y1="12" x2="11" y2="6"/>
|
||||
</svg>
|
||||
{{ _('return_home') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
27
fastapi_admin/templates/errors/500.html
Normal file
27
fastapi_admin/templates/errors/500.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block body %}
|
||||
<div class="page page-center">
|
||||
<div class="container-tight py-4">
|
||||
<div class="empty">
|
||||
<div class="empty-header">500</div>
|
||||
<p class="empty-title">Oops… You just found an error page</p>
|
||||
<p class="empty-subtitle text-muted">
|
||||
We are sorry but our server encountered an internal error
|
||||
</p>
|
||||
<div class="empty-action">
|
||||
<a href="{{ request.app.admin_path }}" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<line x1="5" y1="12" x2="11" y2="18"/>
|
||||
<line x1="5" y1="12" x2="11" y2="6"/>
|
||||
</svg>
|
||||
{{ _('return_home') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
17
fastapi_admin/templates/errors/maintenance.html
Normal file
17
fastapi_admin/templates/errors/maintenance.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block body %}
|
||||
<div class="page page-center">
|
||||
<div class="container-tight py-4">
|
||||
<div class="empty">
|
||||
<div class="empty-img"><img src="https://preview.tabler.io/static/illustrations/undraw_quitting_time_dm8t.svg" height="128"
|
||||
alt="">
|
||||
</div>
|
||||
<p class="empty-title">Temporarily down for maintenance</p>
|
||||
<p class="empty-subtitle text-muted">
|
||||
Sorry for the inconvenience but we’re performing some maintenance at the moment. We’ll be back
|
||||
online shortly!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -35,7 +35,9 @@
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-4">{{ _('login_title') }}</h2>
|
||||
<h2 class="card-title text-center mb-4">
|
||||
{{ request.app.login_provider.login_title }}
|
||||
</h2>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ _('username') }}</label>
|
||||
<input
|
||||
|
@ -2,7 +2,7 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsoneditor@9.4.0/dist/jsoneditor.min.css">
|
||||
<div class="form-label">{{ label }}</div>
|
||||
<div id="{{ name }}" class="form-group mb-3"></div>
|
||||
<input {% if not null %}required{% endif %} type="text" name="{{ name }}" value='{{ value|safe }}' hidden>
|
||||
<input {% if not null %}required{% endif %} type="text" name="{{ name }}" value="{{ value|safe }}" hidden>
|
||||
<style>
|
||||
.jsoneditor {
|
||||
border: 1px solid #dadcde;
|
||||
@ -21,12 +21,15 @@
|
||||
let options = {{options|safe}};
|
||||
if (Object.keys(options).length === 0) {
|
||||
options = {
|
||||
modes: ['tree', 'view', 'form', 'code', 'text', 'preview']
|
||||
modes: ['tree', 'view', 'form', 'code', 'text', 'preview'],
|
||||
}
|
||||
}
|
||||
options.onChangeText = function (json) {
|
||||
$('input[name={{ name }}]').val(json);
|
||||
}
|
||||
const editor = new JSONEditor(container, options)
|
||||
editor.set({{ value|safe }})
|
||||
{% if value %}
|
||||
editor.set({{ value|safe }})
|
||||
{% endif %}
|
||||
editor.expandAll();
|
||||
</script>
|
||||
|
23
fastapi_admin/utils.py
Normal file
23
fastapi_admin/utils.py
Normal file
@ -0,0 +1,23 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
import bcrypt
|
||||
|
||||
|
||||
def generate_random_str(
|
||||
length: int,
|
||||
is_digit: bool = True,
|
||||
):
|
||||
if is_digit:
|
||||
all_char = string.digits
|
||||
else:
|
||||
all_char = string.ascii_letters + string.digits
|
||||
return "".join(random.sample(all_char, length))
|
||||
|
||||
|
||||
def check_password(password: str, password_hash: str):
|
||||
return bcrypt.checkpw(password.encode(), password_hash.encode())
|
||||
|
||||
|
||||
def hash_password(password: str):
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
@ -6,7 +6,7 @@ from typing import Any, List, Optional, Tuple, Type
|
||||
from starlette.datastructures import UploadFile
|
||||
from tortoise import Model
|
||||
|
||||
from fastapi_admin.providers.file_upload import FileUploadProvider
|
||||
from fastapi_admin.file_upload import FileUpload
|
||||
from fastapi_admin.widgets import Widget
|
||||
|
||||
|
||||
@ -189,7 +189,7 @@ class File(Input):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
upload_provider: FileUploadProvider,
|
||||
upload_provider: FileUpload,
|
||||
default: Any = None,
|
||||
null: bool = False,
|
||||
disabled: bool = False,
|
||||
|
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 6.3499999 6.3499999"
|
||||
height="6.3499999mm"
|
||||
width="6.3499999mm">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="translate(-87.539286,-84.426191)"
|
||||
id="layer1">
|
||||
<path
|
||||
id="path815"
|
||||
d="m 87.539286,84.426191 h 6.35 v 6.35 h -6.35 z"
|
||||
style="fill:none;stroke-width:0.26458332" />
|
||||
<path
|
||||
style="stroke-width:0.26458332;fill:#ffffff"
|
||||
id="path817"
|
||||
d="m 90.714286,84.960649 c -1.457854,0 -2.640542,1.182688 -2.640542,2.640542 0,1.457854 1.182688,2.640542 2.640542,2.640542 1.457854,0 2.640542,-1.182688 2.640542,-2.640542 0,-1.457854 -1.182688,-2.640542 -2.640542,-2.640542 z m -0.137583,4.757209 v -1.656292 h -0.92075 l 1.322916,-2.577042 v 1.656292 h 0.886354 z" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user