error pages

This commit is contained in:
long2ice
2021-05-05 14:46:26 +08:00
parent 3935adba00
commit 473faac93b
31 changed files with 390 additions and 246 deletions

View File

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

View File

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

View File

@ -1 +0,0 @@
# File Upload Provider

View File

@ -0,0 +1 @@
# File Upload

View File

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

View File

@ -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()])
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 "返回首页"

View File

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

View File

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

View 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 %}

View 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 %}

View 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 %}

View 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 were performing some maintenance at the moment. Well be back
online shortly!
</p>
</div>
</div>
</div>
{% endblock %}

View File

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

View File

@ -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
View 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()

View File

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

View File

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