From 473faac93bffe976a3c09067f2c758bd8085256a Mon Sep 17 00:00:00 2001 From: long2ice Date: Wed, 5 May 2021 14:46:26 +0800 Subject: [PATCH] error pages --- README.md | 14 ++ docker-compose.yml | 4 +- docs/en/docs/custom/file.md | 1 - docs/en/docs/custom/file_upload.md | 1 + .../docs/custom/{ => providers}/admin_log.md | 0 docs/en/docs/custom/{ => providers}/login.md | 0 .../docs/custom/{ => providers}/permission.md | 0 docs/en/docs/custom/{ => providers}/search.md | 0 docs/en/docs/getting_started/quick_start.md | 7 +- docs/en/docs/pro/exclusive.md | 51 +---- docs/en/docs/reference/errors.md | 4 +- docs/en/mkdocs.yml | 11 +- examples/main.py | 6 +- examples/models.py | 2 +- examples/resources.py | 4 +- fastapi_admin/app.py | 42 ++-- fastapi_admin/exceptions.py | 23 ++ fastapi_admin/{providers => }/file_upload.py | 2 +- .../locales/en_US/LC_MESSAGES/messages.po | 37 ++-- .../locales/zh_CN/LC_MESSAGES/messages.po | 37 ++-- fastapi_admin/providers/__init__.py | 11 + fastapi_admin/providers/login.py | 202 +++++++++++------- fastapi_admin/templates/errors/403.html | 27 +++ fastapi_admin/templates/errors/404.html | 27 +++ fastapi_admin/templates/errors/500.html | 27 +++ .../templates/errors/maintenance.html | 17 ++ fastapi_admin/templates/login.html | 4 +- .../templates/widgets/inputs/json.html | 9 +- fastapi_admin/utils.py | 23 ++ fastapi_admin/widgets/inputs.py | 4 +- images/icon-white.svg | 39 ---- 31 files changed, 390 insertions(+), 246 deletions(-) delete mode 100644 docs/en/docs/custom/file.md create mode 100644 docs/en/docs/custom/file_upload.md rename docs/en/docs/custom/{ => providers}/admin_log.md (100%) rename docs/en/docs/custom/{ => providers}/login.md (100%) rename docs/en/docs/custom/{ => providers}/permission.md (100%) rename docs/en/docs/custom/{ => providers}/search.md (100%) rename fastapi_admin/{providers => }/file_upload.py (98%) create mode 100644 fastapi_admin/templates/errors/403.html create mode 100644 fastapi_admin/templates/errors/404.html create mode 100644 fastapi_admin/templates/errors/500.html create mode 100644 fastapi_admin/templates/errors/maintenance.html create mode 100644 fastapi_admin/utils.py delete mode 100644 images/icon-white.svg diff --git a/README.md b/README.md index 666caf9..2d4c29e 100644 --- a/README.md +++ b/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 to create first admin. + ## Documentation See documentation at [https://fastapi-admin.github.io/fastapi-admin](https://fastapi-admin.github.io/fastapi-admin). diff --git a/docker-compose.yml b/docker-compose.yml index aea51e2..5e11fae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/en/docs/custom/file.md b/docs/en/docs/custom/file.md deleted file mode 100644 index 8d4cd52..0000000 --- a/docs/en/docs/custom/file.md +++ /dev/null @@ -1 +0,0 @@ -# File Upload Provider diff --git a/docs/en/docs/custom/file_upload.md b/docs/en/docs/custom/file_upload.md new file mode 100644 index 0000000..10e3470 --- /dev/null +++ b/docs/en/docs/custom/file_upload.md @@ -0,0 +1 @@ +# File Upload diff --git a/docs/en/docs/custom/admin_log.md b/docs/en/docs/custom/providers/admin_log.md similarity index 100% rename from docs/en/docs/custom/admin_log.md rename to docs/en/docs/custom/providers/admin_log.md diff --git a/docs/en/docs/custom/login.md b/docs/en/docs/custom/providers/login.md similarity index 100% rename from docs/en/docs/custom/login.md rename to docs/en/docs/custom/providers/login.md diff --git a/docs/en/docs/custom/permission.md b/docs/en/docs/custom/providers/permission.md similarity index 100% rename from docs/en/docs/custom/permission.md rename to docs/en/docs/custom/providers/permission.md diff --git a/docs/en/docs/custom/search.md b/docs/en/docs/custom/providers/search.md similarity index 100% rename from docs/en/docs/custom/search.md rename to docs/en/docs/custom/providers/search.md diff --git a/docs/en/docs/getting_started/quick_start.md b/docs/en/docs/getting_started/quick_start.md index 65665f4..16e37e8 100644 --- a/docs/en/docs/getting_started/quick_start.md +++ b/docs/en/docs/getting_started/quick_start.md @@ -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): diff --git a/docs/en/docs/pro/exclusive.md b/docs/en/docs/pro/exclusive.md index 6de673d..2178f3f 100644 --- a/docs/en/docs/pro/exclusive.md +++ b/docs/en/docs/pro/exclusive.md @@ -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()]) ``` diff --git a/docs/en/docs/reference/errors.md b/docs/en/docs/reference/errors.md index 9addf89..ed8b9c5 100644 --- a/docs/en/docs/reference/errors.md +++ b/docs/en/docs/reference/errors.md @@ -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. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 03b1e82..d7cc811 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -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 diff --git a/examples/main.py b/examples/main.py index c6ac4c5..cba3d87 100644 --- a/examples/main.py +++ b/examples/main.py @@ -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, ) diff --git a/examples/models.py b/examples/models.py index 899ffda..9b4e552 100644 --- a/examples/models.py +++ b/examples/models.py @@ -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): diff --git a/examples/resources.py b/examples/resources.py index 6aa298b..22dc4d1 100644 --- a/examples/resources.py +++ b/examples/resources.py @@ -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 diff --git a/fastapi_admin/app.py b/fastapi_admin/app.py index 4af6ff6..b26e824 100644 --- a/fastapi_admin/app.py +++ b/fastapi_admin/app.py @@ -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) diff --git a/fastapi_admin/exceptions.py b/fastapi_admin/exceptions.py index 3f11be7..1eead76 100644 --- a/fastapi_admin/exceptions.py +++ b/fastapi_admin/exceptions.py @@ -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} + ) diff --git a/fastapi_admin/providers/file_upload.py b/fastapi_admin/file_upload.py similarity index 98% rename from fastapi_admin/providers/file_upload.py rename to fastapi_admin/file_upload.py index 8a9cc01..af2c719 100644 --- a/fastapi_admin/providers/file_upload.py +++ b/fastapi_admin/file_upload.py @@ -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, diff --git a/fastapi_admin/locales/en_US/LC_MESSAGES/messages.po b/fastapi_admin/locales/en_US/LC_MESSAGES/messages.po index 36f0777..66661e4 100644 --- a/fastapi_admin/locales/en_US/LC_MESSAGES/messages.po +++ b/fastapi_admin/locales/en_US/LC_MESSAGES/messages.po @@ -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 \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" + diff --git a/fastapi_admin/locales/zh_CN/LC_MESSAGES/messages.po b/fastapi_admin/locales/zh_CN/LC_MESSAGES/messages.po index 3d01a1a..551bf51 100644 --- a/fastapi_admin/locales/zh_CN/LC_MESSAGES/messages.po +++ b/fastapi_admin/locales/zh_CN/LC_MESSAGES/messages.po @@ -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 \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 "返回首页" + diff --git a/fastapi_admin/providers/__init__.py b/fastapi_admin/providers/__init__.py index e69de29..d984693 100644 --- a/fastapi_admin/providers/__init__.py +++ b/fastapi_admin/providers/__init__.py @@ -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) diff --git a/fastapi_admin/providers/login.py b/fastapi_admin/providers/login.py index 35ac74e..84b43e6 100644 --- a/fastapi_admin/providers/login.py +++ b/fastapi_admin/providers/login.py @@ -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) diff --git a/fastapi_admin/templates/errors/403.html b/fastapi_admin/templates/errors/403.html new file mode 100644 index 0000000..4b1d0e7 --- /dev/null +++ b/fastapi_admin/templates/errors/403.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} +{% block body %} +
+
+
+
403
+

Oops… You are forbidden

+

+ We are sorry but the page you are looking for was forbidden +

+ +
+
+
+{% endblock %} diff --git a/fastapi_admin/templates/errors/404.html b/fastapi_admin/templates/errors/404.html new file mode 100644 index 0000000..3dec8f3 --- /dev/null +++ b/fastapi_admin/templates/errors/404.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} +{% block body %} +
+
+
+
404
+

Oops… You just found an error page

+

+ We are sorry but the page you are looking for was not found +

+ +
+
+
+{% endblock %} diff --git a/fastapi_admin/templates/errors/500.html b/fastapi_admin/templates/errors/500.html new file mode 100644 index 0000000..0381358 --- /dev/null +++ b/fastapi_admin/templates/errors/500.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} +{% block body %} +
+
+
+
500
+

Oops… You just found an error page

+

+ We are sorry but our server encountered an internal error +

+ +
+
+
+{% endblock %} diff --git a/fastapi_admin/templates/errors/maintenance.html b/fastapi_admin/templates/errors/maintenance.html new file mode 100644 index 0000000..e28aa70 --- /dev/null +++ b/fastapi_admin/templates/errors/maintenance.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% block body %} +
+
+
+
+
+

Temporarily down for maintenance

+

+ Sorry for the inconvenience but we’re performing some maintenance at the moment. We’ll be back + online shortly! +

+
+
+
+{% endblock %} diff --git a/fastapi_admin/templates/login.html b/fastapi_admin/templates/login.html index f337656..74309c2 100644 --- a/fastapi_admin/templates/login.html +++ b/fastapi_admin/templates/login.html @@ -35,7 +35,9 @@ autocomplete="off" >
-

{{ _('login_title') }}

+

+ {{ request.app.login_provider.login_title }} +

{{ label }}
- +