From 3fecbb0ff63d1da568bdc9456e60c5d61efe579f Mon Sep 17 00:00:00 2001 From: yuxf Date: Thu, 27 Jan 2022 15:21:41 +0800 Subject: [PATCH 01/12] add login and login.html --- sqladmin/application.py | 132 ++++++++++++++++++++++++++++++-- sqladmin/auth/__init__.py | 0 sqladmin/auth/dantic.py | 6 ++ sqladmin/auth/hashers.py | 24 ++++++ sqladmin/auth/models.py | 29 +++++++ sqladmin/auth/utils/__init__.py | 0 sqladmin/auth/utils/token.py | 23 ++++++ sqladmin/conf.py | 46 +++++++++++ sqladmin/depends.py | 96 +++++++++++++++++++++++ sqladmin/statics/img/logo.svg | 4 + sqladmin/templates/login.html | 68 ++++++++++++++++ sqladmin_cli/__init__.py | 29 +++++++ tests/test_i18n/test.py | 17 ++++ 13 files changed, 466 insertions(+), 8 deletions(-) create mode 100644 sqladmin/auth/__init__.py create mode 100644 sqladmin/auth/dantic.py create mode 100644 sqladmin/auth/hashers.py create mode 100644 sqladmin/auth/models.py create mode 100644 sqladmin/auth/utils/__init__.py create mode 100644 sqladmin/auth/utils/token.py create mode 100644 sqladmin/conf.py create mode 100644 sqladmin/depends.py create mode 100644 sqladmin/statics/img/logo.svg create mode 100644 sqladmin/templates/login.html create mode 100644 sqladmin_cli/__init__.py create mode 100644 tests/test_i18n/test.py diff --git a/sqladmin/application.py b/sqladmin/application.py index 71041703..f4aa425c 100644 --- a/sqladmin/application.py +++ b/sqladmin/application.py @@ -1,6 +1,10 @@ +import gettext +import os from typing import TYPE_CHECKING, List, Type, Union +import anyio from jinja2 import ChoiceLoader, FileSystemLoader, PackageLoader +from sqlalchemy import select from sqlalchemy.engine import Engine from sqlalchemy.ext.asyncio import AsyncEngine from starlette.applications import Starlette @@ -11,10 +15,13 @@ from starlette.staticfiles import StaticFiles from starlette.templating import Jinja2Templates +from sqladmin.auth.hashers import make_password +from sqladmin.auth.models import User +from sqladmin.auth.utils.token import create_access_token, decode_access_token + if TYPE_CHECKING: from sqladmin.models import ModelAdmin - __all__ = [ "Admin", ] @@ -34,6 +41,7 @@ def __init__( base_url: str = "/admin", title: str = "Admin", logo_url: str = None, + language: str = None, ) -> None: self.app = app self.engine = engine @@ -41,6 +49,16 @@ def __init__( self._model_admins: List[Type["ModelAdmin"]] = [] self.templates = Jinja2Templates("templates") + self.templates.env.add_extension("jinja2.ext.i18n") + if language: + translation = gettext.translation( + "lang", + os.path.dirname(__file__) + "/translations", + languages=[language], + ) + self.templates.env.install_gettext_translations(translation, newstyle=True) + else: + self.templates.env.install_null_translations(newstyle=True) self.templates.env.loader = ChoiceLoader( [ FileSystemLoader("templates"), @@ -100,6 +118,17 @@ class UserAdmin(ModelAdmin, model=User): self._model_admins.append(model) +def check_token(request: Request) -> bool: + token = request.cookies.get("access_token") + if token: + try: + decode_access_token(token) + return True + except: + pass + return False + + class Admin(BaseAdmin): """Main entrypoint to admin interface. @@ -130,6 +159,7 @@ def __init__( base_url: str = "/admin", title: str = "Admin", logo_url: str = None, + language: str = None, ) -> None: """ Args: @@ -142,7 +172,12 @@ def __init__( assert isinstance(engine, (Engine, AsyncEngine)) super().__init__( - app=app, engine=engine, base_url=base_url, title=title, logo_url=logo_url + app=app, + engine=engine, + base_url=base_url, + title=title, + logo_url=logo_url, + language=language, ) statics = StaticFiles(packages=["sqladmin"]) @@ -150,7 +185,7 @@ def __init__( router = Router( routes=[ Mount("/statics", app=statics, name="statics"), - Route("/", endpoint=self.index, name="index"), + Route("/", endpoint=self.index, name="index", methods=["GET", "POST"]), Route("/{identity}/list", endpoint=self.list, name="list"), Route("/{identity}/detail/{pk}", endpoint=self.detail, name="detail"), Route( @@ -165,6 +200,12 @@ def __init__( name="create", methods=["GET", "POST"], ), + Route( + "/login", + endpoint=self.login, + name="login", + methods=["GET", "POST"], + ), ] ) self.app.mount(base_url, app=router, name="admin") @@ -173,12 +214,22 @@ def __init__( async def index(self, request: Request) -> Response: """Index route which can be overriden to create dashboards.""" - + if not check_token(request): + return RedirectResponse( + url=request.url_for( + "admin:login", + ), + ) return self.templates.TemplateResponse("index.html", {"request": request}) async def list(self, request: Request) -> Response: """List route to display paginated Model instances.""" - + if not check_token(request): + return RedirectResponse( + request.url_for( + "admin:login", + ), + ) model_admin = self._find_model_admin(request.path_params["identity"]) page = int(request.query_params.get("page", 1)) @@ -204,7 +255,12 @@ async def list(self, request: Request) -> Response: async def detail(self, request: Request) -> Response: """Detail route.""" - + if not check_token(request): + return RedirectResponse( + request.url_for( + "admin:login", + ), + ) model_admin = self._find_model_admin(request.path_params["identity"]) if not model_admin.can_view_details: return self._unathorized_response(request) @@ -224,7 +280,12 @@ async def detail(self, request: Request) -> Response: async def delete(self, request: Request) -> Response: """Delete route.""" - + if not check_token(request): + return RedirectResponse( + request.url_for( + "admin:login", + ), + ) identity = request.path_params["identity"] model_admin = self._find_model_admin(identity) if not model_admin.can_delete: @@ -240,7 +301,12 @@ async def delete(self, request: Request) -> Response: async def create(self, request: Request) -> Response: """Create model endpoint.""" - + if not check_token(request): + return RedirectResponse( + request.url_for( + "admin:login", + ), + ) identity = request.path_params["identity"] model_admin = self._find_model_admin(identity) if not model_admin.can_create: @@ -272,3 +338,53 @@ async def create(self, request: Request) -> Response: request.url_for("admin:list", identity=identity), status_code=302, ) + + async def login(self, request: Request) -> Response: + context = { + "request": request, + "errinfo": "", + "username_err": False, + "password_err": False, + } + + if request.method == "GET": + return self.templates.TemplateResponse("login.html", context) + form = await request.form() + username = form.get("username") + raw_password = form.get("password") + + if not username: + context["username_err"] = True + return self.templates.TemplateResponse("login.html", context) + if not raw_password: + context["password_err"] = True + return self.templates.TemplateResponse("login.html", context) + if isinstance(self.engine, Engine): + res = await anyio.to_thread.run_sync( + self.engine.execute, + select(User.password) + .where(User.username == username, User.is_active == True) + .limit(1), + ) + else: + res = await self.engine.execute( + select(User.password) + .where(User.username == username, User.is_active == True) + .limit(1) + ) + password = res.scalar_one_or_none() + if password is not None: + if make_password(raw_password) == password: + request.cookies.setdefault( + "access_token", + ) + res = RedirectResponse( + request.url_for( + "admin:index", + ), + ) + access_token = create_access_token({"username": username}) + res.set_cookie("access_token", access_token) + return res + context["errinfo"] = "e" + return self.templates.TemplateResponse("login.html", context) diff --git a/sqladmin/auth/__init__.py b/sqladmin/auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sqladmin/auth/dantic.py b/sqladmin/auth/dantic.py new file mode 100644 index 00000000..a8367d52 --- /dev/null +++ b/sqladmin/auth/dantic.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class LoginInfo(BaseModel): + username: str + password: str diff --git a/sqladmin/auth/hashers.py b/sqladmin/auth/hashers.py new file mode 100644 index 00000000..98165b3d --- /dev/null +++ b/sqladmin/auth/hashers.py @@ -0,0 +1,24 @@ +import binascii +import hashlib + +from sqladmin.conf import settings + +SECRET_KEY = settings.SECRET_KEY + + +def make_password(raw_password: str) -> str: + password = hashlib.pbkdf2_hmac( + "sha256", raw_password.encode("utf-8"), SECRET_KEY.encode("utf-8"), 16 + ) + return binascii.hexlify(password).decode() + + +def verify_password(raw_password: str, password: str) -> bool: + random_salt = SECRET_KEY.encode("utf-8") + raw_password_bytes = hashlib.pbkdf2_hmac( + "sha256", raw_password.encode("utf-8"), random_salt, 16 + ) + if binascii.hexlify(raw_password_bytes).decode() == password: + return True + else: + return False diff --git a/sqladmin/auth/models.py b/sqladmin/auth/models.py new file mode 100644 index 00000000..97ab9518 --- /dev/null +++ b/sqladmin/auth/models.py @@ -0,0 +1,29 @@ +from sqlalchemy import Boolean, Column, Integer, String +from sqlalchemy.ext.declarative import declarative_base + +from sqladmin.auth.hashers import make_password, verify_password + +Base = declarative_base() + + +class User(Base): + __tablename__ = "auth_users" + + id = Column(Integer, primary_key=True) + username = Column(String(length=128), unique=True) + email = Column(String(length=128)) + password = Column(String(length=128)) + is_active = Column(Boolean, default=True) + + # is_superuser = Column(Boolean) + + def set_password(self, raw_password: str): + self.password = make_password( + raw_password, + ) + + def verify_password(self, raw_password: str) -> bool: + return verify_password( + raw_password, + self.password, + ) diff --git a/sqladmin/auth/utils/__init__.py b/sqladmin/auth/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sqladmin/auth/utils/token.py b/sqladmin/auth/utils/token.py new file mode 100644 index 00000000..774935fb --- /dev/null +++ b/sqladmin/auth/utils/token.py @@ -0,0 +1,23 @@ +from datetime import datetime, timedelta + +from jose import jwt + +from sqladmin.conf import settings + +SECRET_KEY = settings.SECRET_KEY +ALGORITHM = settings.ALGORITHM +EXPIRES_DELTA = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + + +def create_access_token(data: dict): + to_encode = data.copy() + expire = datetime.utcnow() + EXPIRES_DELTA + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def decode_access_token( + token: str, +): + return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) diff --git a/sqladmin/conf.py b/sqladmin/conf.py new file mode 100644 index 00000000..a0ff51fe --- /dev/null +++ b/sqladmin/conf.py @@ -0,0 +1,46 @@ +from pydantic import BaseSettings, validator + + +class Settings(BaseSettings): + SECRET_KEY: str + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 + DEBUG: bool = True + DATABASE_URL: str # db connect url + + @validator("DEBUG", pre=True) + def get_debug(cls, v: str) -> bool: + if isinstance(v, str): + if v != "True": + return False + return True + + # sentry's config + + # SENTRY_DSN: Optional[HttpUrl] = None + # SENTRY_ENVIROMENT: str = "development" + # + # @validator("SENTRY_DSN", pre=True) + # def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]: + # if v and len(v) > 0: + # return v + # return None + + class Config: + case_sensitive = True + env_file = ".env" # default env file + + # init sentry + # def __init__(self): + # super(Settings, self).__init__() + # + # if self.SENTRY_DSN: + # import sentry_sdk + # + # sentry_sdk.init( + # dsn=self.SENTRY_DSN, + # environment=self.SENTRY_ENVIROMENT, + # ) + + +settings = Settings() diff --git a/sqladmin/depends.py b/sqladmin/depends.py new file mode 100644 index 00000000..8718d1ea --- /dev/null +++ b/sqladmin/depends.py @@ -0,0 +1,96 @@ +from typing import Optional + +from fast_tmp.apps.exceptions import credentials_exception +from fast_tmp.conf import settings +from fast_tmp.db import get_db_session +from fast_tmp.models import User +from fast_tmp.schemas import LoginSchema +from fast_tmp.utils.token import decode_access_token +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl=settings.FAST_TMP_URL + "/auth/token") + + +async def get_username(token: str = Depends(oauth2_scheme)) -> str: + """ + 从token获取username + """ + try: + payload = decode_access_token(token) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + return username + + +async def get_user( + username: str = Depends(get_username), + session: AsyncSession = Depends(get_db_session), +) -> Optional[User]: + """ + 从数据库读取数据 + """ + async with session.begin(): + res = await session.execute( + select(User).where(User.username == username).limit(1) + ) + ret = res.scalar_one_or_none() + return ret + + +async def _get_user( + session: AsyncSession, + username: str = Depends(get_username), +) -> Optional[User]: + """ + 从数据库读取数据 + """ + res = await session.execute(select(User).where(User.username == username).limit(1)) + return res.scalar_one_or_none() + + +async def authenticate_user( + logininfo: LoginSchema, session: AsyncSession = Depends(get_db_session) +) -> Optional[User]: + """ + 验证密码 + """ + user = await _get_user( + session, + logininfo.username, + ) + if not user: + return None + if not user.verify_password(logininfo.password): + return None + return user + + +async def get_current_user( + user: Optional[User] = Depends(get_user), +) -> User: + """ + 获取存在的用户 + """ + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user(current_user: User = Depends(get_current_user)): + """ + 获取活跃的用户 + """ + if not current_user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user diff --git a/sqladmin/statics/img/logo.svg b/sqladmin/statics/img/logo.svg new file mode 100644 index 00000000..109341aa --- /dev/null +++ b/sqladmin/statics/img/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/sqladmin/templates/login.html b/sqladmin/templates/login.html new file mode 100644 index 00000000..0f63f4ba --- /dev/null +++ b/sqladmin/templates/login.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} +{% block body %} +
+
+
+ +
+
+
+

{{ _('Login to your account') }}

+
+ + {% if username_err==true %} + +
{{ _('Username can not be null.') }}
+ {% else %} + + {% endif %} +
+
+ +
+ {% if password_err==true %} + +
{{ _('Password can not be null.') }}
+ {% else %} + + {% endif %} + + + + + +
+
+
+ +
+
+ {% if errinfo!="" %} + {{ _('Username or password is error!') }} + {% endif %} +
+ +
+
+
+
+ +{% endblock %} diff --git a/sqladmin_cli/__init__.py b/sqladmin_cli/__init__.py new file mode 100644 index 00000000..5ca99492 --- /dev/null +++ b/sqladmin_cli/__init__.py @@ -0,0 +1,29 @@ +import typer +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from sqladmin.auth.models import User +from sqladmin.conf import settings + +app = typer.Typer() + + +@app.command() +def createmanager(username: str, password: str): + """ + create manager + """ + engine = create_engine(settings.DATABASE_URL, echo=settings.DEBUG) + user = User( + username=username, is_active=True + ) + user.set_password(password) + session = Session(engine) + session.add(user) + session.commit() + print(f"create {username} success.") +def main(): + app() + +if __name__ == "__main__": + main() diff --git a/tests/test_i18n/test.py b/tests/test_i18n/test.py new file mode 100644 index 00000000..658f74b2 --- /dev/null +++ b/tests/test_i18n/test.py @@ -0,0 +1,17 @@ +# coding: utf-8 + +import gettext +import os + +for name in ["LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"]: + if name in os.environ and os.environ[name]: + print("当前的语言环境是:", os.environ[name]) + break + +gettext.bindtextdomain("test_messages", "translations/") +gettext.textdomain("test_messages") + +_ = gettext.gettext +print(_("just a test string")) +gettext.textdomain("messages") +print(_("just a test string")) From c71781dadbb17c0c80723c707171dc7deceb63bf Mon Sep 17 00:00:00 2001 From: yuxf Date: Thu, 27 Jan 2022 15:30:45 +0800 Subject: [PATCH 02/12] make style --- sqladmin/application.py | 12 +++-- sqladmin/auth/models.py | 4 +- sqladmin/auth/utils/token.py | 4 +- sqladmin/depends.py | 96 ------------------------------------ sqladmin_cli/__init__.py | 3 ++ 5 files changed, 14 insertions(+), 105 deletions(-) delete mode 100644 sqladmin/depends.py diff --git a/sqladmin/application.py b/sqladmin/application.py index f4aa425c..dfa564fb 100644 --- a/sqladmin/application.py +++ b/sqladmin/application.py @@ -56,9 +56,11 @@ def __init__( os.path.dirname(__file__) + "/translations", languages=[language], ) - self.templates.env.install_gettext_translations(translation, newstyle=True) + self.templates.env.install_gettext_translations( # type: ignore + translation, newstyle=True + ) # type: ignore else: - self.templates.env.install_null_translations(newstyle=True) + self.templates.env.install_null_translations(newstyle=True) # type: ignore self.templates.env.loader = ChoiceLoader( [ FileSystemLoader("templates"), @@ -124,7 +126,7 @@ def check_token(request: Request) -> bool: try: decode_access_token(token) return True - except: + except: # noqa pass return False @@ -363,13 +365,13 @@ async def login(self, request: Request) -> Response: res = await anyio.to_thread.run_sync( self.engine.execute, select(User.password) - .where(User.username == username, User.is_active == True) + .where(User.username == username, User.is_active == True) # noqa .limit(1), ) else: res = await self.engine.execute( select(User.password) - .where(User.username == username, User.is_active == True) + .where(User.username == username, User.is_active == True) # noqa .limit(1) ) password = res.scalar_one_or_none() diff --git a/sqladmin/auth/models.py b/sqladmin/auth/models.py index 97ab9518..7ea2ad59 100644 --- a/sqladmin/auth/models.py +++ b/sqladmin/auth/models.py @@ -6,7 +6,7 @@ Base = declarative_base() -class User(Base): +class User(Base): # type: ignore __tablename__ = "auth_users" id = Column(Integer, primary_key=True) @@ -17,7 +17,7 @@ class User(Base): # is_superuser = Column(Boolean) - def set_password(self, raw_password: str): + def set_password(self, raw_password: str) -> None: self.password = make_password( raw_password, ) diff --git a/sqladmin/auth/utils/token.py b/sqladmin/auth/utils/token.py index 774935fb..7bd123b8 100644 --- a/sqladmin/auth/utils/token.py +++ b/sqladmin/auth/utils/token.py @@ -9,7 +9,7 @@ EXPIRES_DELTA = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) -def create_access_token(data: dict): +def create_access_token(data: dict) -> str: to_encode = data.copy() expire = datetime.utcnow() + EXPIRES_DELTA to_encode.update({"exp": expire}) @@ -19,5 +19,5 @@ def create_access_token(data: dict): def decode_access_token( token: str, -): +) -> dict: return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) diff --git a/sqladmin/depends.py b/sqladmin/depends.py deleted file mode 100644 index 8718d1ea..00000000 --- a/sqladmin/depends.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Optional - -from fast_tmp.apps.exceptions import credentials_exception -from fast_tmp.conf import settings -from fast_tmp.db import get_db_session -from fast_tmp.models import User -from fast_tmp.schemas import LoginSchema -from fast_tmp.utils.token import decode_access_token -from fastapi import Depends, FastAPI, HTTPException, status -from fastapi.security import OAuth2PasswordBearer -from jose import JWTError -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl=settings.FAST_TMP_URL + "/auth/token") - - -async def get_username(token: str = Depends(oauth2_scheme)) -> str: - """ - 从token获取username - """ - try: - payload = decode_access_token(token) - username: str = payload.get("sub") - if username is None: - raise credentials_exception - except JWTError: - raise credentials_exception - return username - - -async def get_user( - username: str = Depends(get_username), - session: AsyncSession = Depends(get_db_session), -) -> Optional[User]: - """ - 从数据库读取数据 - """ - async with session.begin(): - res = await session.execute( - select(User).where(User.username == username).limit(1) - ) - ret = res.scalar_one_or_none() - return ret - - -async def _get_user( - session: AsyncSession, - username: str = Depends(get_username), -) -> Optional[User]: - """ - 从数据库读取数据 - """ - res = await session.execute(select(User).where(User.username == username).limit(1)) - return res.scalar_one_or_none() - - -async def authenticate_user( - logininfo: LoginSchema, session: AsyncSession = Depends(get_db_session) -) -> Optional[User]: - """ - 验证密码 - """ - user = await _get_user( - session, - logininfo.username, - ) - if not user: - return None - if not user.verify_password(logininfo.password): - return None - return user - - -async def get_current_user( - user: Optional[User] = Depends(get_user), -) -> User: - """ - 获取存在的用户 - """ - if user is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - return user - - -async def get_current_active_user(current_user: User = Depends(get_current_user)): - """ - 获取活跃的用户 - """ - if not current_user.is_active: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user diff --git a/sqladmin_cli/__init__.py b/sqladmin_cli/__init__.py index 5ca99492..390c6dad 100644 --- a/sqladmin_cli/__init__.py +++ b/sqladmin_cli/__init__.py @@ -22,8 +22,11 @@ def createmanager(username: str, password: str): session.add(user) session.commit() print(f"create {username} success.") + + def main(): app() + if __name__ == "__main__": main() From cb194ff6b107cdabf7cb5ebc6aa7e23e624ddab6 Mon Sep 17 00:00:00 2001 From: yuxf Date: Thu, 27 Jan 2022 16:17:15 +0800 Subject: [PATCH 03/12] fix test add i18n file. --- .env | 2 + .../translations/zh_CN/LC_MESSAGES/lang.mo | Bin 0 -> 624 bytes .../translations/zh_CN/LC_MESSAGES/lang.po | 37 ++++++++++++++++++ tests/__init__.py | 5 +++ tests/test_admin_async.py | 23 +++++++++++ tests/test_admin_sync.py | 24 ++++++++++++ tests/test_application.py | 4 ++ tests/test_i18n/test.py | 17 -------- 8 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 .env create mode 100644 sqladmin/translations/zh_CN/LC_MESSAGES/lang.mo create mode 100644 sqladmin/translations/zh_CN/LC_MESSAGES/lang.po delete mode 100644 tests/test_i18n/test.py diff --git a/.env b/.env new file mode 100644 index 00000000..7b6678f0 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +SECRET_KEY=aawinjwol;egbfnjek bnl +DATABASE_URL=sqlite:///example.db \ No newline at end of file diff --git a/sqladmin/translations/zh_CN/LC_MESSAGES/lang.mo b/sqladmin/translations/zh_CN/LC_MESSAGES/lang.mo new file mode 100644 index 0000000000000000000000000000000000000000..c84d7e8223aff9ceb931effe8612d07c03758b16 GIT binary patch literal 624 zcmYk2&ui2`6vxL}twvC&6hS=9y-d|xNvkw=r*$zumTXeci%gSgI z74=Y0y(pg4gP|fDSw^{!SUvOo8@ZrmQyziSYb1*&m20={$mw*H~13U$q`Uac@ zz5_GBFW?k#09*uWpz%%|`#X0)F97F(b1b^Et>evrO3O8_4A+vC9Qmpc5?h70RHZS?GF3^7I?R{{O6Mr%B`?>(P;#tb zZ&MhY^ZO#>s4EJIg+%GUQys1D@ywETPS&_KLP-~QyyaWoD#dFw?zW@QiFsdXO9E4f zoZP4iLlWg;rxTGFt~MUhVwSu3HuYi>M5N{6n~@u|XYWKIb$n(lNi>tD$E?yWUP*l) zhh9MOA`QH@zvQ)AG3|D*!>TMQR&bJVR=U{t!qv1%vgrFuJu#fIl|cefAs_v*KghhN^q6b(LY*L#o9@Z;yh{pUwJFGo9QZ2AK? Cx35V6 literal 0 HcmV?d00001 diff --git a/sqladmin/translations/zh_CN/LC_MESSAGES/lang.po b/sqladmin/translations/zh_CN/LC_MESSAGES/lang.po new file mode 100644 index 00000000..2ec001f0 --- /dev/null +++ b/sqladmin/translations/zh_CN/LC_MESSAGES/lang.po @@ -0,0 +1,37 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-27 10:58+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: test.py:16 test.py:18 +msgid "Login to your account" +msgstr "登陆你的账户" + +msgid "Remember me on this device" +msgstr "记住我" + +msgid "Sign in" +msgstr "登陆" + +msgid "Username" +msgstr "用户名" + +msgid "Password" +msgstr "密码" + +msgid "Enter username" +msgstr "输入用户名" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..0162aa54 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,5 @@ +from sqladmin.auth.utils.token import create_access_token + + +def get_test_token() -> str: + return create_access_token({"user": "root"}) diff --git a/tests/test_admin_async.py b/tests/test_admin_async.py index 778bd6d7..f27d8e69 100644 --- a/tests/test_admin_async.py +++ b/tests/test_admin_async.py @@ -9,6 +9,7 @@ from starlette.testclient import TestClient from sqladmin import Admin, ModelAdmin +from tests import get_test_token from tests.common import TEST_DATABASE_URI_ASYNC pytestmark = pytest.mark.anyio @@ -83,6 +84,7 @@ class AddressAdmin(ModelAdmin, model=Address): async def test_root_view() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin") assert response.status_code == 200 @@ -92,6 +94,7 @@ async def test_root_view() -> None: async def test_invalid_list_page() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/example/list") assert response.status_code == 404 @@ -104,6 +107,7 @@ async def test_list_view_single_page() -> None: await session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -129,6 +133,7 @@ async def test_list_view_multi_page() -> None: await session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -142,6 +147,7 @@ async def test_list_view_multi_page() -> None: assert response.text.count('
  • ') == 1 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list?page=3") assert response.status_code == 200 @@ -152,6 +158,7 @@ async def test_list_view_multi_page() -> None: assert response.text.count('
  • ') == 2 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list?page=5") assert response.status_code == 200 @@ -177,6 +184,7 @@ async def test_list_page_permission_actions() -> None: await session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -184,6 +192,7 @@ async def test_list_page_permission_actions() -> None: assert response.text.count('') == 10 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/address/list") assert response.status_code == 200 @@ -194,6 +203,7 @@ async def test_list_page_permission_actions() -> None: async def test_unauthorized_detail_page() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/address/detail/1") assert response.status_code == 401 @@ -201,6 +211,7 @@ async def test_unauthorized_detail_page() -> None: async def test_not_found_detail_page() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/detail/1") assert response.status_code == 404 @@ -217,6 +228,7 @@ async def test_detail_page() -> None: await session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/detail/1") assert response.status_code == 200 @@ -244,12 +256,14 @@ async def test_column_labels() -> None: await session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list") assert response.status_code == 200 assert response.text.count("Email") == 1 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/detail/1") assert response.status_code == 200 @@ -258,6 +272,7 @@ async def test_column_labels() -> None: async def test_delete_endpoint_unauthorized_response() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.delete("/admin/address/delete/1") assert response.status_code == 401 @@ -265,6 +280,7 @@ async def test_delete_endpoint_unauthorized_response() -> None: async def test_delete_endpoint_not_found_response() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.delete("/admin/user/delete/1") assert response.status_code == 404 @@ -285,6 +301,7 @@ async def test_delete_endpoint() -> None: assert result.scalar_one() == 1 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.delete("/admin/user/delete/1") assert response.status_code == 200 @@ -297,6 +314,7 @@ async def test_create_endpoint_unauthorized_response() -> None: admin._model_admins[1].can_create = False with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/address/create") assert response.status_code == 401 @@ -306,6 +324,7 @@ async def test_create_endpoint_unauthorized_response() -> None: async def test_create_endpoint_get_form() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/create") assert response.status_code == 200 @@ -326,6 +345,7 @@ async def test_create_endpoint_get_form() -> None: async def test_create_endpoint_post_form() -> None: data: dict = {"date_of_birth": "Wrong Date Format"} with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.post("/admin/user/create", data=data) assert response.status_code == 400 @@ -335,6 +355,7 @@ async def test_create_endpoint_post_form() -> None: data = {"name": "SQLAlchemy"} with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.post("/admin/user/create", data=data) stmt = select(func.count(User.id)) @@ -351,6 +372,7 @@ async def test_create_endpoint_post_form() -> None: data = {"user": user.id} with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.post("/admin/address/create", data=data) stmt = select(func.count(Address.id)) @@ -366,6 +388,7 @@ async def test_create_endpoint_post_form() -> None: data = {"name": "SQLAdmin", "addresses": [address.id]} with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.post("/admin/user/create", data=data) stmt = select(func.count(User.id)) diff --git a/tests/test_admin_sync.py b/tests/test_admin_sync.py index 7c0807ee..9aac7dae 100644 --- a/tests/test_admin_sync.py +++ b/tests/test_admin_sync.py @@ -17,6 +17,7 @@ from starlette.testclient import TestClient from sqladmin import Admin, ModelAdmin +from tests import get_test_token from tests.common import TEST_DATABASE_URI_SYNC Base = declarative_base() # type: Any @@ -85,6 +86,7 @@ class AddressAdmin(ModelAdmin, model=Address): def test_root_view() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin") assert response.status_code == 200 @@ -94,6 +96,8 @@ def test_root_view() -> None: def test_invalid_list_page() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/example/list") assert response.status_code == 404 @@ -106,6 +110,7 @@ def test_list_view_single_page() -> None: session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -131,6 +136,7 @@ def test_list_view_multi_page() -> None: session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -144,6 +150,7 @@ def test_list_view_multi_page() -> None: assert response.text.count('
  • ') == 1 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list?page=3") assert response.status_code == 200 @@ -154,6 +161,7 @@ def test_list_view_multi_page() -> None: assert response.text.count('
  • ') == 2 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list?page=5") assert response.status_code == 200 @@ -179,6 +187,7 @@ def test_list_page_permission_actions() -> None: session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -186,6 +195,7 @@ def test_list_page_permission_actions() -> None: assert response.text.count('') == 10 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/address/list") assert response.status_code == 200 @@ -196,6 +206,7 @@ def test_list_page_permission_actions() -> None: def test_unauthorized_detail_page() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/address/detail/1") assert response.status_code == 401 @@ -203,6 +214,7 @@ def test_unauthorized_detail_page() -> None: def test_not_found_detail_page() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/detail/1") assert response.status_code == 404 @@ -219,6 +231,7 @@ def test_detail_page() -> None: session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/detail/1") assert response.status_code == 200 @@ -246,12 +259,14 @@ def test_column_labels() -> None: session.commit() with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/list") assert response.status_code == 200 assert response.text.count("Email") == 1 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/detail/1") assert response.status_code == 200 @@ -260,6 +275,7 @@ def test_column_labels() -> None: def test_delete_endpoint_unauthorized_response() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.delete("/admin/address/delete/1") assert response.status_code == 401 @@ -267,6 +283,7 @@ def test_delete_endpoint_unauthorized_response() -> None: def test_delete_endpoint_not_found_response() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.delete("/admin/user/delete/1") assert response.status_code == 404 @@ -281,6 +298,7 @@ def test_delete_endpoint() -> None: assert session.query(User).count() == 1 with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.delete("/admin/user/delete/1") assert response.status_code == 200 @@ -291,6 +309,7 @@ def test_create_endpoint_unauthorized_response() -> None: admin._model_admins[1].can_create = False with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/address/create") assert response.status_code == 401 @@ -300,6 +319,7 @@ def test_create_endpoint_unauthorized_response() -> None: def test_create_endpoint_get_form() -> None: with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/admin/user/create") assert response.status_code == 200 @@ -320,6 +340,7 @@ def test_create_endpoint_get_form() -> None: def test_create_endpoint_post_form() -> None: data: dict = {"birthdate": "Wrong Date Format"} with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.post("/admin/user/create", data=data) assert response.status_code == 400 @@ -329,6 +350,7 @@ def test_create_endpoint_post_form() -> None: data = {"name": "SQLAlchemy"} with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.post("/admin/user/create", data=data) stmt = select(func.count(User.id)) @@ -343,6 +365,7 @@ def test_create_endpoint_post_form() -> None: data = {"user": user.id} with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.post("/admin/address/create", data=data) stmt = select(func.count(Address.id)) @@ -356,6 +379,7 @@ def test_create_endpoint_post_form() -> None: data = {"name": "SQLAdmin", "addresses": [address.id]} with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.post("/admin/user/create", data=data) stmt = select(func.count(User.id)) diff --git a/tests/test_application.py b/tests/test_application.py index e7765e77..3ca9049d 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -7,6 +7,7 @@ from starlette.testclient import TestClient from sqladmin import Admin +from tests import get_test_token from tests.common import TEST_DATABASE_URI_SYNC Base = declarative_base() # type: Any @@ -24,6 +25,8 @@ def test_application_title() -> None: Admin(app=app, engine=engine) with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) + response = client.get("/admin") assert response.status_code == 200 @@ -39,6 +42,7 @@ def test_application_logo() -> None: ) with TestClient(app) as client: + client.cookies.setdefault("access_token", get_test_token()) response = client.get("/dashboard") assert response.status_code == 200 diff --git a/tests/test_i18n/test.py b/tests/test_i18n/test.py deleted file mode 100644 index 658f74b2..00000000 --- a/tests/test_i18n/test.py +++ /dev/null @@ -1,17 +0,0 @@ -# coding: utf-8 - -import gettext -import os - -for name in ["LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"]: - if name in os.environ and os.environ[name]: - print("当前的语言环境是:", os.environ[name]) - break - -gettext.bindtextdomain("test_messages", "translations/") -gettext.textdomain("test_messages") - -_ = gettext.gettext -print(_("just a test string")) -gettext.textdomain("messages") -print(_("just a test string")) From f3c78c71968ebb6adbd0f5d0c6bd3bf8bf2039be Mon Sep 17 00:00:00 2001 From: yuxf Date: Thu, 27 Jan 2022 16:40:15 +0800 Subject: [PATCH 04/12] fix Admin.__init__ annotation. --- sqladmin/application.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqladmin/application.py b/sqladmin/application.py index dfa564fb..0b388a5d 100644 --- a/sqladmin/application.py +++ b/sqladmin/application.py @@ -169,7 +169,8 @@ def __init__( engine: SQLAlchemy engine instance. base_url: Base URL for Admin interface. title: Admin title. - logo: URL of logo to be displayed instead of title. + logo_url: URL of logo to be displayed instead of title. + language: Now it can write "zh_CN" or None. """ assert isinstance(engine, (Engine, AsyncEngine)) From cc70b320dc5a4321555867d6e476a5a9cd5f83d7 Mon Sep 17 00:00:00 2001 From: yuxf Date: Thu, 27 Jan 2022 16:43:10 +0800 Subject: [PATCH 05/12] delete pydantic --- sqladmin/auth/dantic.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 sqladmin/auth/dantic.py diff --git a/sqladmin/auth/dantic.py b/sqladmin/auth/dantic.py deleted file mode 100644 index a8367d52..00000000 --- a/sqladmin/auth/dantic.py +++ /dev/null @@ -1,6 +0,0 @@ -from pydantic import BaseModel - - -class LoginInfo(BaseModel): - username: str - password: str From fb3855c8d86322f9f859ee50a60197165cd1f0aa Mon Sep 17 00:00:00 2001 From: yuxf Date: Thu, 27 Jan 2022 16:48:50 +0800 Subject: [PATCH 06/12] add pydantic --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 200b51fe..28ee9254 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ mkdocstrings==0.17.0 # Packaging twine==3.7.1 wheel==0.37.0 +pydantic==1.9.0 \ No newline at end of file From 44260656d71b65cd068b1f410265e20fc3dda5b8 Mon Sep 17 00:00:00 2001 From: yuxf Date: Thu, 27 Jan 2022 18:26:48 +0800 Subject: [PATCH 07/12] add test --- .env | 3 +- sqladmin/application.py | 17 +++--- sqladmin/auth/models.py | 14 ----- sqladmin/auth/utils/password.py | 0 tests/test_admin_async.py | 99 +++++++++++++++++++++++++++++- tests/test_admin_sync.py | 103 ++++++++++++++++++++++++++++++++ 6 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 sqladmin/auth/utils/password.py diff --git a/.env b/.env index 7b6678f0..19526a4d 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ SECRET_KEY=aawinjwol;egbfnjek bnl -DATABASE_URL=sqlite:///example.db \ No newline at end of file +DATABASE_URL=sqlite:///example.db +DEBUG=False \ No newline at end of file diff --git a/sqladmin/application.py b/sqladmin/application.py index 0b388a5d..9e02d0c2 100644 --- a/sqladmin/application.py +++ b/sqladmin/application.py @@ -1,12 +1,13 @@ import gettext import os -from typing import TYPE_CHECKING, List, Type, Union +from typing import TYPE_CHECKING, List, Optional, Type, Union import anyio from jinja2 import ChoiceLoader, FileSystemLoader, PackageLoader from sqlalchemy import select from sqlalchemy.engine import Engine -from sqlalchemy.ext.asyncio import AsyncEngine +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession +from sqlalchemy.orm import sessionmaker from starlette.applications import Starlette from starlette.exceptions import HTTPException from starlette.requests import Request @@ -214,6 +215,7 @@ def __init__( self.app.mount(base_url, app=router, name="admin") self.templates.env.globals["model_admins"] = self.model_admins + self.session: Optional[sessionmaker] = None async def index(self, request: Request) -> Response: """Index route which can be overriden to create dashboards.""" @@ -284,11 +286,7 @@ async def detail(self, request: Request) -> Response: async def delete(self, request: Request) -> Response: """Delete route.""" if not check_token(request): - return RedirectResponse( - request.url_for( - "admin:login", - ), - ) + return self._not_found_response(request) identity = request.path_params["identity"] model_admin = self._find_model_admin(identity) if not model_admin.can_delete: @@ -370,7 +368,10 @@ async def login(self, request: Request) -> Response: .limit(1), ) else: - res = await self.engine.execute( + if self.session is None: + LocalSession = sessionmaker(bind=self.engine, class_=AsyncSession) + self.session = LocalSession() + res = await self.session.execute( select(User.password) .where(User.username == username, User.is_active == True) # noqa .limit(1) diff --git a/sqladmin/auth/models.py b/sqladmin/auth/models.py index 7ea2ad59..fa2b1ab1 100644 --- a/sqladmin/auth/models.py +++ b/sqladmin/auth/models.py @@ -1,8 +1,6 @@ from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy.ext.declarative import declarative_base -from sqladmin.auth.hashers import make_password, verify_password - Base = declarative_base() @@ -14,16 +12,4 @@ class User(Base): # type: ignore email = Column(String(length=128)) password = Column(String(length=128)) is_active = Column(Boolean, default=True) - # is_superuser = Column(Boolean) - - def set_password(self, raw_password: str) -> None: - self.password = make_password( - raw_password, - ) - - def verify_password(self, raw_password: str) -> bool: - return verify_password( - raw_password, - self.password, - ) diff --git a/sqladmin/auth/utils/password.py b/sqladmin/auth/utils/password.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_admin_async.py b/tests/test_admin_async.py index f27d8e69..5342b3c6 100644 --- a/tests/test_admin_async.py +++ b/tests/test_admin_async.py @@ -9,6 +9,8 @@ from starlette.testclient import TestClient from sqladmin import Admin, ModelAdmin +from sqladmin.auth.hashers import make_password +from sqladmin.auth.models import Base as AdminBase, User as AdminUser from tests import get_test_token from tests.common import TEST_DATABASE_URI_ASYNC @@ -58,7 +60,7 @@ def __str__(self) -> str: async def prepare_database() -> AsyncGenerator[None, None]: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) - + await conn.run_sync(AdminBase.metadata.create_all) yield async with engine.begin() as conn: @@ -401,3 +403,98 @@ async def test_create_endpoint_post_form() -> None: user = result.scalar_one() assert user.name == "SQLAdmin" assert user.addresses == [address] + + +async def test_login() -> None: + + user = AdminUser( + username="root2", + is_active=True, + email="test@email.com", + password=make_password("root2"), + ) + session.add(user) + await session.commit() + with TestClient(app) as client: + response = client.post( + "/admin/login", data={"username": "root2", "password": "root2"} + ) + + assert response.status_code == 307 + assert len(response.cookies.get("access_token")) > 0 + with TestClient(app) as client: + response = client.post( + "/admin/login", data={"username": "root", "password": "root2"} + ) + + assert response.status_code == 200 + assert ( + response.text.count( + 'Username or password is error!' + ) + == 1 + ) + with TestClient(app) as client: + response = client.post( + "/admin/login", data={"username": "root", "password": ""} + ) + + assert response.status_code == 200 + assert response.text.count("Password can not be null.") == 1 + with TestClient(app) as client: + response = client.post( + "/admin/login", data={"username": "", "password": "root"} + ) + + assert response.status_code == 200 + assert response.text.count("Username can not be null.") == 1 + + +def check_redirect(url: str, method: str = "GET") -> None: + with TestClient(app) as client: + response = client.get(url) + assert response.status_code == 200 or response.status_code == 307 + assert ( + response.text.count( + '

    Login to your account

    ' + ) + == 1 + ) + + +def test_redirect() -> None: + check_redirect("/admin") + check_redirect("/admin/address/detail/1") + check_redirect("/admin/address/list") + check_redirect("/admin/address/create") + with TestClient(app) as client: + response = client.delete("/admin/address/delete/1") + assert response.status_code == 404 + + +async def test_i18n() -> None: + app = Starlette() + i18n_admin = Admin(app=app, engine=engine, language="zh_CN") + i18n_admin.register_model(UserAdmin) + i18n_admin.register_model(AddressAdmin) + with TestClient(app) as client: + response = client.get("/admin/login") + + assert response.status_code == 200 + assert ( + response.text.count('

    登陆你的账户

    ') == 1 + ) + + +async def test_expire_time() -> None: + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2NDMyNzYyMDl9._iFrRq5TqlomtM0bCr-p0L-VSst-bP9rZDf4s9Zc-1c" # noqa + with TestClient(app) as client: + client.cookies.setdefault("access_token", token) + response = client.get("/admin") + assert response.status_code == 200 or response.status_code == 307 + assert ( + response.text.count( + '

    Login to your account

    ' + ) + == 1 + ) diff --git a/tests/test_admin_sync.py b/tests/test_admin_sync.py index 9aac7dae..b90732aa 100644 --- a/tests/test_admin_sync.py +++ b/tests/test_admin_sync.py @@ -17,6 +17,8 @@ from starlette.testclient import TestClient from sqladmin import Admin, ModelAdmin +from sqladmin.auth.hashers import make_password, verify_password +from sqladmin.auth.models import Base as AdminBase, User as AdminUser from tests import get_test_token from tests.common import TEST_DATABASE_URI_SYNC @@ -63,8 +65,10 @@ def __str__(self) -> str: @pytest.fixture(autouse=True, scope="function") def prepare_database() -> Generator[None, None, None]: Base.metadata.create_all(engine) + AdminBase.metadata.create_all(engine) yield Base.metadata.drop_all(engine) + AdminBase.metadata.drop_all(engine) class UserAdmin(ModelAdmin, model=User): @@ -390,3 +394,102 @@ def test_create_endpoint_post_form() -> None: user = session.execute(stmt).scalar_one() assert user.name == "SQLAdmin" assert user.addresses == [address] + + +def test_login() -> None: + user = AdminUser( + username="root", + is_active=True, + email="test@email.com", + password=make_password("root"), + ) + session.add(user) + session.commit() + assert not verify_password("root1", "root2") + pd = make_password("root1") + assert verify_password("root1", pd) + with TestClient(app) as client: + response = client.post( + "/admin/login", data={"username": "root", "password": "root"} + ) + + assert response.status_code == 307 + assert len(response.cookies.get("access_token")) > 0 + with TestClient(app) as client: + response = client.post( + "/admin/login", data={"username": "root", "password": "root1"} + ) + + assert response.status_code == 200 + assert ( + response.text.count( + 'Username or password is error!' + ) + == 1 + ) + with TestClient(app) as client: + response = client.post( + "/admin/login", data={"username": "root", "password": ""} + ) + + assert response.status_code == 200 + assert response.text.count("Password can not be null.") == 1 + with TestClient(app) as client: + response = client.post( + "/admin/login", data={"username": "", "password": "root"} + ) + + assert response.status_code == 200 + assert response.text.count("Username can not be null.") == 1 + + +def check_redirect( + url: str, +) -> None: + with TestClient(app) as client: + response = client.get(url) + assert response.status_code == 200 or response.status_code == 307 + assert ( + response.text.count( + '

    Login to your account

    ' + ) + == 1 + ) + + +def test_redirect() -> None: + check_redirect("/admin") + check_redirect("/admin/address/detail/1") + check_redirect("/admin/address/list") + check_redirect("/admin/address/create") + with TestClient(app) as client: + response = client.delete("/admin/address/delete/1") + assert response.status_code == 404 + + +def test_i18n() -> None: + app = Starlette() + i18n_admin = Admin(app=app, engine=engine, language="zh_CN") + i18n_admin.register_model(UserAdmin) + i18n_admin.register_model(AddressAdmin) + with TestClient(app) as client: + response = client.get("/admin/login") + + assert response.status_code == 200 + assert ( + response.text.count('

    登陆你的账户

    ') == 1 + ) + + +def test_expire_time() -> None: + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2NDMyNzYyMDl9._iFrRq5TqlomtM0bCr-p0L-VSst-bP9rZDf4s9Zc-1c" # noqa + with TestClient(app) as client: + client.cookies.setdefault("access_token", token) + response = client.get("/admin") + assert response.status_code == 200 or response.status_code == 307 + assert ( + response.text.count( + '

    Login to your account

    ' + ) + == 1 + ) From 992a0c7be0b8258d4ffb261dd95c619232af80f1 Mon Sep 17 00:00:00 2001 From: Chise1 Date: Thu, 27 Jan 2022 21:05:32 +0800 Subject: [PATCH 08/12] update --- .gitignore | 1 + requirements.txt | 3 +- sqladmin/application.py | 74 ++++++++++++------------------------ sqladmin/auth/middlewares.py | 41 ++++++++++++++++++++ 4 files changed, 69 insertions(+), 50 deletions(-) create mode 100644 sqladmin/auth/middlewares.py diff --git a/.gitignore b/.gitignore index 50d5b466..fefe6557 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ htmlcov/ .mypy_cache/ coverage.xml examples/ +.idea/ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 28ee9254..68865977 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,5 @@ mkdocstrings==0.17.0 # Packaging twine==3.7.1 wheel==0.37.0 -pydantic==1.9.0 \ No newline at end of file +pydantic[dotenv]==1.9.0 +python-jose==3.3.0 \ No newline at end of file diff --git a/sqladmin/application.py b/sqladmin/application.py index 9e02d0c2..60798f0d 100644 --- a/sqladmin/application.py +++ b/sqladmin/application.py @@ -7,9 +7,11 @@ from sqlalchemy import select from sqlalchemy.engine import Engine from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker, Session from starlette.applications import Starlette +from starlette.authentication import requires from starlette.exceptions import HTTPException +from starlette.middleware.authentication import AuthenticationMiddleware from starlette.requests import Request from starlette.responses import RedirectResponse, Response from starlette.routing import Mount, Route, Router @@ -17,6 +19,7 @@ from starlette.templating import Jinja2Templates from sqladmin.auth.hashers import make_password +from sqladmin.auth.middlewares import BasicAuthBackend from sqladmin.auth.models import User from sqladmin.auth.utils.token import create_access_token, decode_access_token @@ -121,17 +124,6 @@ class UserAdmin(ModelAdmin, model=User): self._model_admins.append(model) -def check_token(request: Request) -> bool: - token = request.cookies.get("access_token") - if token: - try: - decode_access_token(token) - return True - except: # noqa - pass - return False - - class Admin(BaseAdmin): """Main entrypoint to admin interface. @@ -183,7 +175,15 @@ def __init__( logo_url=logo_url, language=language, ) - + if isinstance(engine, Engine): + LocalSession = sessionmaker(bind=self.engine, class_=Session) + self.session = LocalSession() + self._sync=True + else: + LocalSession = sessionmaker(bind=self.engine, class_=AsyncSession) + self.session = LocalSession() + self._sync=False + app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend(self.session,self._sync)) statics = StaticFiles(packages=["sqladmin"]) router = Router( @@ -215,26 +215,15 @@ def __init__( self.app.mount(base_url, app=router, name="admin") self.templates.env.globals["model_admins"] = self.model_admins - self.session: Optional[sessionmaker] = None + @requires('authenticated', redirect='login') async def index(self, request: Request) -> Response: """Index route which can be overriden to create dashboards.""" - if not check_token(request): - return RedirectResponse( - url=request.url_for( - "admin:login", - ), - ) return self.templates.TemplateResponse("index.html", {"request": request}) + @requires('authenticated', redirect='login') async def list(self, request: Request) -> Response: """List route to display paginated Model instances.""" - if not check_token(request): - return RedirectResponse( - request.url_for( - "admin:login", - ), - ) model_admin = self._find_model_admin(request.path_params["identity"]) page = int(request.query_params.get("page", 1)) @@ -258,14 +247,10 @@ async def list(self, request: Request) -> Response: return self.templates.TemplateResponse("list.html", context) + @requires('authenticated', redirect='login') async def detail(self, request: Request) -> Response: """Detail route.""" - if not check_token(request): - return RedirectResponse( - request.url_for( - "admin:login", - ), - ) + model_admin = self._find_model_admin(request.path_params["identity"]) if not model_admin.can_view_details: return self._unathorized_response(request) @@ -283,10 +268,9 @@ async def detail(self, request: Request) -> Response: return self.templates.TemplateResponse("detail.html", context) + @requires('authenticated', redirect='login') async def delete(self, request: Request) -> Response: """Delete route.""" - if not check_token(request): - return self._not_found_response(request) identity = request.path_params["identity"] model_admin = self._find_model_admin(identity) if not model_admin.can_delete: @@ -300,14 +284,9 @@ async def delete(self, request: Request) -> Response: return Response(content=request.url_for("admin:list", identity=identity)) + @requires('authenticated', redirect='login') async def create(self, request: Request) -> Response: """Create model endpoint.""" - if not check_token(request): - return RedirectResponse( - request.url_for( - "admin:login", - ), - ) identity = request.path_params["identity"] model_admin = self._find_model_admin(identity) if not model_admin.can_create: @@ -360,21 +339,18 @@ async def login(self, request: Request) -> Response: if not raw_password: context["password_err"] = True return self.templates.TemplateResponse("login.html", context) - if isinstance(self.engine, Engine): + if self._sync: res = await anyio.to_thread.run_sync( - self.engine.execute, + self.session.execute, select(User.password) - .where(User.username == username, User.is_active == True) # noqa - .limit(1), + .where(User.username == username, User.is_active == True) # noqa + .limit(1), ) else: - if self.session is None: - LocalSession = sessionmaker(bind=self.engine, class_=AsyncSession) - self.session = LocalSession() res = await self.session.execute( select(User.password) - .where(User.username == username, User.is_active == True) # noqa - .limit(1) + .where(User.username == username, User.is_active == True) # noqa + .limit(1) ) password = res.scalar_one_or_none() if password is not None: diff --git a/sqladmin/auth/middlewares.py b/sqladmin/auth/middlewares.py new file mode 100644 index 00000000..f0dbdf13 --- /dev/null +++ b/sqladmin/auth/middlewares.py @@ -0,0 +1,41 @@ +import anyio +from sqlalchemy import select +from starlette.authentication import ( + AuthCredentials, AuthenticationBackend, SimpleUser +) +from starlette.requests import HTTPConnection + + +from sqladmin.auth.models import User +from sqladmin.auth.utils.token import decode_access_token + + +class BasicAuthBackend(AuthenticationBackend): + def __init__(self, session, _sync: bool): + self.session = session + self._sync = _sync + + async def authenticate(self, conn: HTTPConnection): + access_token = conn.cookies.get("access_token") + if access_token: + try: + data = decode_access_token(access_token) + if self._sync: + res = await anyio.to_thread.run_sync( + self.session.execute, + select(User.username) + .where(User.username == username, User.is_active == True) # noqa + .limit(1), + ) + else: + res = await self.session.execute( + select(User.username) + .where(User.username == username, User.is_active == True) # noqa + .limit(1) + ) + if not res.scalar_one_or_none(): + return + return AuthCredentials(["authenticated"]), SimpleUser(data["username"]) + except: # noqa + pass + return \ No newline at end of file From ed3a5d607c3c3be33458c4f28db42c71f24839eb Mon Sep 17 00:00:00 2001 From: yuxf Date: Fri, 28 Jan 2022 10:19:10 +0800 Subject: [PATCH 09/12] fix test. --- .env | 2 +- .gitignore | 5 ++- requirements.txt | 5 ++- sqladmin/application.py | 32 +++++++++-------- sqladmin/auth/middlewares.py | 35 ++++++++++-------- sqladmin_cli/__init__.py | 9 ++--- tests/__init__.py | 4 +-- tests/test_admin_async.py | 69 +++++++++++++++++++----------------- tests/test_admin_sync.py | 60 ++++++++++++++++++------------- tests/test_application.py | 33 ++++++++++++++--- 10 files changed, 153 insertions(+), 101 deletions(-) diff --git a/.env b/.env index 19526a4d..d847fa48 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ SECRET_KEY=aawinjwol;egbfnjek bnl -DATABASE_URL=sqlite:///example.db +DATABASE_URL=sqlite:///test.db DEBUG=False \ No newline at end of file diff --git a/.gitignore b/.gitignore index fefe6557..de284e96 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ htmlcov/ .mypy_cache/ coverage.xml examples/ -.idea/ \ No newline at end of file +example/ +.idea/ +build/ +site/ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 68865977..3e3bdfe9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,7 @@ mkdocstrings==0.17.0 twine==3.7.1 wheel==0.37.0 pydantic[dotenv]==1.9.0 -python-jose==3.3.0 \ No newline at end of file +python-jose==3.3.0 +anyio==3.5.0 +wtforms==3.0.1 +typer==0.4.0 \ No newline at end of file diff --git a/sqladmin/application.py b/sqladmin/application.py index 60798f0d..cdf04b48 100644 --- a/sqladmin/application.py +++ b/sqladmin/application.py @@ -1,13 +1,13 @@ import gettext import os -from typing import TYPE_CHECKING, List, Optional, Type, Union +from typing import TYPE_CHECKING, List, Type, Union import anyio from jinja2 import ChoiceLoader, FileSystemLoader, PackageLoader from sqlalchemy import select from sqlalchemy.engine import Engine from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession -from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.orm import Session, sessionmaker from starlette.applications import Starlette from starlette.authentication import requires from starlette.exceptions import HTTPException @@ -21,7 +21,7 @@ from sqladmin.auth.hashers import make_password from sqladmin.auth.middlewares import BasicAuthBackend from sqladmin.auth.models import User -from sqladmin.auth.utils.token import create_access_token, decode_access_token +from sqladmin.auth.utils.token import create_access_token if TYPE_CHECKING: from sqladmin.models import ModelAdmin @@ -178,12 +178,14 @@ def __init__( if isinstance(engine, Engine): LocalSession = sessionmaker(bind=self.engine, class_=Session) self.session = LocalSession() - self._sync=True + self._sync = True else: LocalSession = sessionmaker(bind=self.engine, class_=AsyncSession) self.session = LocalSession() - self._sync=False - app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend(self.session,self._sync)) + self._sync = False + app.add_middleware( + AuthenticationMiddleware, backend=BasicAuthBackend(self.session, self._sync) + ) statics = StaticFiles(packages=["sqladmin"]) router = Router( @@ -216,12 +218,12 @@ def __init__( self.templates.env.globals["model_admins"] = self.model_admins - @requires('authenticated', redirect='login') + @requires("authenticated", redirect="admin:login") async def index(self, request: Request) -> Response: """Index route which can be overriden to create dashboards.""" return self.templates.TemplateResponse("index.html", {"request": request}) - @requires('authenticated', redirect='login') + @requires("authenticated", redirect="admin:login") async def list(self, request: Request) -> Response: """List route to display paginated Model instances.""" model_admin = self._find_model_admin(request.path_params["identity"]) @@ -247,7 +249,7 @@ async def list(self, request: Request) -> Response: return self.templates.TemplateResponse("list.html", context) - @requires('authenticated', redirect='login') + @requires("authenticated", redirect="admin:login") async def detail(self, request: Request) -> Response: """Detail route.""" @@ -268,7 +270,7 @@ async def detail(self, request: Request) -> Response: return self.templates.TemplateResponse("detail.html", context) - @requires('authenticated', redirect='login') + @requires("authenticated", redirect="admin:login") async def delete(self, request: Request) -> Response: """Delete route.""" identity = request.path_params["identity"] @@ -284,7 +286,7 @@ async def delete(self, request: Request) -> Response: return Response(content=request.url_for("admin:list", identity=identity)) - @requires('authenticated', redirect='login') + @requires("authenticated", redirect="admin:login") async def create(self, request: Request) -> Response: """Create model endpoint.""" identity = request.path_params["identity"] @@ -343,14 +345,14 @@ async def login(self, request: Request) -> Response: res = await anyio.to_thread.run_sync( self.session.execute, select(User.password) - .where(User.username == username, User.is_active == True) # noqa - .limit(1), + .where(User.username == username, User.is_active == True) # noqa + .limit(1), ) else: res = await self.session.execute( select(User.password) - .where(User.username == username, User.is_active == True) # noqa - .limit(1) + .where(User.username == username, User.is_active == True) # noqa + .limit(1) ) password = res.scalar_one_or_none() if password is not None: diff --git a/sqladmin/auth/middlewares.py b/sqladmin/auth/middlewares.py index f0dbdf13..321487b6 100644 --- a/sqladmin/auth/middlewares.py +++ b/sqladmin/auth/middlewares.py @@ -1,41 +1,48 @@ +import typing + import anyio from sqlalchemy import select -from starlette.authentication import ( - AuthCredentials, AuthenticationBackend, SimpleUser -) +from sqlalchemy.orm import sessionmaker +from starlette.authentication import AuthCredentials, AuthenticationBackend, SimpleUser from starlette.requests import HTTPConnection - from sqladmin.auth.models import User from sqladmin.auth.utils.token import decode_access_token class BasicAuthBackend(AuthenticationBackend): - def __init__(self, session, _sync: bool): + def __init__(self, session: sessionmaker, _sync: bool): self.session = session self._sync = _sync - async def authenticate(self, conn: HTTPConnection): + async def authenticate( + self, conn: HTTPConnection + ) -> typing.Optional[typing.Tuple[AuthCredentials, SimpleUser]]: access_token = conn.cookies.get("access_token") if access_token: try: data = decode_access_token(access_token) + username = data["username"] if self._sync: res = await anyio.to_thread.run_sync( self.session.execute, select(User.username) - .where(User.username == username, User.is_active == True) # noqa - .limit(1), + .where( + User.username == username, User.is_active == True # noqa + ) + .limit(1), ) else: res = await self.session.execute( select(User.username) - .where(User.username == username, User.is_active == True) # noqa - .limit(1) + .where( + User.username == username, User.is_active == True # noqa + ) # noqa + .limit(1) ) if not res.scalar_one_or_none(): - return + return None return AuthCredentials(["authenticated"]), SimpleUser(data["username"]) - except: # noqa - pass - return \ No newline at end of file + except Exception as e: # noqa + print(e) + return None diff --git a/sqladmin_cli/__init__.py b/sqladmin_cli/__init__.py index 390c6dad..94477862 100644 --- a/sqladmin_cli/__init__.py +++ b/sqladmin_cli/__init__.py @@ -2,6 +2,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import Session +from sqladmin.auth.hashers import make_password from sqladmin.auth.models import User from sqladmin.conf import settings @@ -9,22 +10,22 @@ @app.command() -def createmanager(username: str, password: str): +def createmanager(username: str, password: str) -> None: """ create manager """ engine = create_engine(settings.DATABASE_URL, echo=settings.DEBUG) user = User( - username=username, is_active=True + username=username, is_active=True, + password=make_password(password) ) - user.set_password(password) session = Session(engine) session.add(user) session.commit() print(f"create {username} success.") -def main(): +def main() -> None: app() diff --git a/tests/__init__.py b/tests/__init__.py index 0162aa54..f07776eb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ from sqladmin.auth.utils.token import create_access_token -def get_test_token() -> str: - return create_access_token({"user": "root"}) +def get_test_token(username: str) -> str: + return create_access_token({"username": username}) diff --git a/tests/test_admin_async.py b/tests/test_admin_async.py index 5342b3c6..fd3daaa8 100644 --- a/tests/test_admin_async.py +++ b/tests/test_admin_async.py @@ -11,10 +11,12 @@ from sqladmin import Admin, ModelAdmin from sqladmin.auth.hashers import make_password from sqladmin.auth.models import Base as AdminBase, User as AdminUser +from sqladmin.conf import settings from tests import get_test_token from tests.common import TEST_DATABASE_URI_ASYNC pytestmark = pytest.mark.anyio +settings.DATABASE_URL = TEST_DATABASE_URI_ASYNC Base = declarative_base() # type: Any @@ -61,6 +63,16 @@ async def prepare_database() -> AsyncGenerator[None, None]: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) await conn.run_sync(AdminBase.metadata.create_all) + + res = await session.execute( + select(AdminUser).where(AdminUser.username == "root_async").limit(1) + ) + if not res.scalar_one_or_none(): + user = AdminUser( + username="root_async", is_active=True, password=make_password("root") + ) + session.add(user) + await session.commit() yield async with engine.begin() as conn: @@ -86,7 +98,7 @@ class AddressAdmin(ModelAdmin, model=Address): async def test_root_view() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin") assert response.status_code == 200 @@ -96,7 +108,7 @@ async def test_root_view() -> None: async def test_invalid_list_page() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/example/list") assert response.status_code == 404 @@ -109,7 +121,7 @@ async def test_list_view_single_page() -> None: await session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -135,7 +147,7 @@ async def test_list_view_multi_page() -> None: await session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -149,7 +161,7 @@ async def test_list_view_multi_page() -> None: assert response.text.count('
  • ') == 1 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/list?page=3") assert response.status_code == 200 @@ -160,7 +172,7 @@ async def test_list_view_multi_page() -> None: assert response.text.count('
  • ') == 2 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/list?page=5") assert response.status_code == 200 @@ -186,7 +198,7 @@ async def test_list_page_permission_actions() -> None: await session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -194,7 +206,7 @@ async def test_list_page_permission_actions() -> None: assert response.text.count('') == 10 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/address/list") assert response.status_code == 200 @@ -205,7 +217,7 @@ async def test_list_page_permission_actions() -> None: async def test_unauthorized_detail_page() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/address/detail/1") assert response.status_code == 401 @@ -213,7 +225,7 @@ async def test_unauthorized_detail_page() -> None: async def test_not_found_detail_page() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/detail/1") assert response.status_code == 404 @@ -230,7 +242,7 @@ async def test_detail_page() -> None: await session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/detail/1") assert response.status_code == 200 @@ -258,14 +270,14 @@ async def test_column_labels() -> None: await session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/list") assert response.status_code == 200 assert response.text.count("Email") == 1 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/detail/1") assert response.status_code == 200 @@ -274,7 +286,7 @@ async def test_column_labels() -> None: async def test_delete_endpoint_unauthorized_response() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.delete("/admin/address/delete/1") assert response.status_code == 401 @@ -282,7 +294,7 @@ async def test_delete_endpoint_unauthorized_response() -> None: async def test_delete_endpoint_not_found_response() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.delete("/admin/user/delete/1") assert response.status_code == 404 @@ -303,7 +315,7 @@ async def test_delete_endpoint() -> None: assert result.scalar_one() == 1 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.delete("/admin/user/delete/1") assert response.status_code == 200 @@ -316,7 +328,7 @@ async def test_create_endpoint_unauthorized_response() -> None: admin._model_admins[1].can_create = False with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/address/create") assert response.status_code == 401 @@ -326,7 +338,7 @@ async def test_create_endpoint_unauthorized_response() -> None: async def test_create_endpoint_get_form() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.get("/admin/user/create") assert response.status_code == 200 @@ -347,7 +359,7 @@ async def test_create_endpoint_get_form() -> None: async def test_create_endpoint_post_form() -> None: data: dict = {"date_of_birth": "Wrong Date Format"} with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.post("/admin/user/create", data=data) assert response.status_code == 400 @@ -357,7 +369,7 @@ async def test_create_endpoint_post_form() -> None: data = {"name": "SQLAlchemy"} with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.post("/admin/user/create", data=data) stmt = select(func.count(User.id)) @@ -374,7 +386,7 @@ async def test_create_endpoint_post_form() -> None: data = {"user": user.id} with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.post("/admin/address/create", data=data) stmt = select(func.count(Address.id)) @@ -390,7 +402,7 @@ async def test_create_endpoint_post_form() -> None: data = {"name": "SQLAdmin", "addresses": [address.id]} with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_async")) response = client.post("/admin/user/create", data=data) stmt = select(func.count(User.id)) @@ -406,18 +418,9 @@ async def test_create_endpoint_post_form() -> None: async def test_login() -> None: - - user = AdminUser( - username="root2", - is_active=True, - email="test@email.com", - password=make_password("root2"), - ) - session.add(user) - await session.commit() with TestClient(app) as client: response = client.post( - "/admin/login", data={"username": "root2", "password": "root2"} + "/admin/login", data={"username": "root_async", "password": "root"} ) assert response.status_code == 307 @@ -469,7 +472,7 @@ def test_redirect() -> None: check_redirect("/admin/address/create") with TestClient(app) as client: response = client.delete("/admin/address/delete/1") - assert response.status_code == 404 + assert response.status_code == 303 async def test_i18n() -> None: diff --git a/tests/test_admin_sync.py b/tests/test_admin_sync.py index b90732aa..fadb4471 100644 --- a/tests/test_admin_sync.py +++ b/tests/test_admin_sync.py @@ -19,11 +19,12 @@ from sqladmin import Admin, ModelAdmin from sqladmin.auth.hashers import make_password, verify_password from sqladmin.auth.models import Base as AdminBase, User as AdminUser +from sqladmin.conf import settings from tests import get_test_token from tests.common import TEST_DATABASE_URI_SYNC Base = declarative_base() # type: Any - +settings.DATABASE_URL = TEST_DATABASE_URI_SYNC engine = create_engine( TEST_DATABASE_URI_SYNC, connect_args={"check_same_thread": False} ) @@ -66,6 +67,15 @@ def __str__(self) -> str: def prepare_database() -> Generator[None, None, None]: Base.metadata.create_all(engine) AdminBase.metadata.create_all(engine) + res = session.execute( + select(AdminUser).where(AdminUser.username == "root_sync").limit(1) + ) + if not res.scalar_one_or_none(): + user = AdminUser( + username="root_sync", is_active=True, password=make_password("root") + ) + session.add(user) + session.commit() yield Base.metadata.drop_all(engine) AdminBase.metadata.drop_all(engine) @@ -90,7 +100,7 @@ class AddressAdmin(ModelAdmin, model=Address): def test_root_view() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin") assert response.status_code == 200 @@ -100,8 +110,8 @@ def test_root_view() -> None: def test_invalid_list_page() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/example/list") assert response.status_code == 404 @@ -114,7 +124,7 @@ def test_list_view_single_page() -> None: session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -140,7 +150,7 @@ def test_list_view_multi_page() -> None: session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -154,7 +164,7 @@ def test_list_view_multi_page() -> None: assert response.text.count('
  • ') == 1 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/list?page=3") assert response.status_code == 200 @@ -165,7 +175,7 @@ def test_list_view_multi_page() -> None: assert response.text.count('
  • ') == 2 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/list?page=5") assert response.status_code == 200 @@ -191,7 +201,7 @@ def test_list_page_permission_actions() -> None: session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/list") assert response.status_code == 200 @@ -199,7 +209,7 @@ def test_list_page_permission_actions() -> None: assert response.text.count('') == 10 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/address/list") assert response.status_code == 200 @@ -210,7 +220,7 @@ def test_list_page_permission_actions() -> None: def test_unauthorized_detail_page() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/address/detail/1") assert response.status_code == 401 @@ -218,7 +228,7 @@ def test_unauthorized_detail_page() -> None: def test_not_found_detail_page() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/detail/1") assert response.status_code == 404 @@ -235,7 +245,7 @@ def test_detail_page() -> None: session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/detail/1") assert response.status_code == 200 @@ -263,14 +273,14 @@ def test_column_labels() -> None: session.commit() with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/list") assert response.status_code == 200 assert response.text.count("Email") == 1 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/detail/1") assert response.status_code == 200 @@ -279,7 +289,7 @@ def test_column_labels() -> None: def test_delete_endpoint_unauthorized_response() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.delete("/admin/address/delete/1") assert response.status_code == 401 @@ -287,7 +297,7 @@ def test_delete_endpoint_unauthorized_response() -> None: def test_delete_endpoint_not_found_response() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.delete("/admin/user/delete/1") assert response.status_code == 404 @@ -302,7 +312,7 @@ def test_delete_endpoint() -> None: assert session.query(User).count() == 1 with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.delete("/admin/user/delete/1") assert response.status_code == 200 @@ -313,7 +323,7 @@ def test_create_endpoint_unauthorized_response() -> None: admin._model_admins[1].can_create = False with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/address/create") assert response.status_code == 401 @@ -323,7 +333,7 @@ def test_create_endpoint_unauthorized_response() -> None: def test_create_endpoint_get_form() -> None: with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin/user/create") assert response.status_code == 200 @@ -344,7 +354,7 @@ def test_create_endpoint_get_form() -> None: def test_create_endpoint_post_form() -> None: data: dict = {"birthdate": "Wrong Date Format"} with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.post("/admin/user/create", data=data) assert response.status_code == 400 @@ -354,7 +364,7 @@ def test_create_endpoint_post_form() -> None: data = {"name": "SQLAlchemy"} with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.post("/admin/user/create", data=data) stmt = select(func.count(User.id)) @@ -369,7 +379,7 @@ def test_create_endpoint_post_form() -> None: data = {"user": user.id} with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.post("/admin/address/create", data=data) stmt = select(func.count(Address.id)) @@ -383,7 +393,7 @@ def test_create_endpoint_post_form() -> None: data = {"name": "SQLAdmin", "addresses": [address.id]} with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.post("/admin/user/create", data=data) stmt = select(func.count(User.id)) @@ -464,7 +474,7 @@ def test_redirect() -> None: check_redirect("/admin/address/create") with TestClient(app) as client: response = client.delete("/admin/address/delete/1") - assert response.status_code == 404 + assert response.status_code == 303 def test_i18n() -> None: diff --git a/tests/test_application.py b/tests/test_application.py index 3ca9049d..70e9f885 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,18 +1,23 @@ -from typing import Any +from typing import Any, Generator -from sqlalchemy import create_engine +import pytest +from sqlalchemy import create_engine, select from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Session, sessionmaker from starlette.applications import Starlette from starlette.testclient import TestClient from sqladmin import Admin +from sqladmin.auth.hashers import make_password +from sqladmin.auth.models import Base as AdminBase, User as AdminUser from tests import get_test_token from tests.common import TEST_DATABASE_URI_SYNC Base = declarative_base() # type: Any -engine = create_engine(TEST_DATABASE_URI_SYNC) +engine = create_engine( + TEST_DATABASE_URI_SYNC, connect_args={"check_same_thread": False} +) LocalSession = sessionmaker(bind=engine) @@ -21,11 +26,29 @@ app = Starlette() +@pytest.fixture(autouse=True, scope="function") +def prepare_database() -> Generator[None, None, None]: + Base.metadata.create_all(engine) + AdminBase.metadata.create_all(engine) + res = session.execute( + select(AdminUser).where(AdminUser.username == "root_sync").limit(1) + ) + if not res.scalar_one_or_none(): + user = AdminUser( + username="root_sync", is_active=True, password=make_password("root") + ) + session.add(user) + session.commit() + yield + Base.metadata.drop_all(engine) + AdminBase.metadata.drop_all(engine) + + def test_application_title() -> None: Admin(app=app, engine=engine) with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/admin") @@ -42,7 +65,7 @@ def test_application_logo() -> None: ) with TestClient(app) as client: - client.cookies.setdefault("access_token", get_test_token()) + client.cookies.setdefault("access_token", get_test_token("root_sync")) response = client.get("/dashboard") assert response.status_code == 200 From 43483699fbae9f2991b70548ca1d888308965c63 Mon Sep 17 00:00:00 2001 From: yuxf Date: Fri, 28 Jan 2022 10:28:34 +0800 Subject: [PATCH 10/12] add sqladmin cli. --- setup.cfg | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.cfg b/setup.cfg index 43a2c04e..b47a334b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,9 @@ install_requires = sqlalchemy >=1.4, <1.5 wtforms >=3, <4 python-multipart + typer + pydantic[dotenv] + python-jose [options.package_data] sqladmin = py.typed @@ -66,3 +69,7 @@ exclude_lines = pragma: no cover pragma: nocover if TYPE_CHECKING: + +[options.entry_points] +console_scripts = + sqladmin = sqladmin_cli.__init__:main \ No newline at end of file From 82539d7338314c2f76e586cc94ae9a93526443e6 Mon Sep 17 00:00:00 2001 From: yuxf Date: Fri, 28 Jan 2022 10:38:50 +0800 Subject: [PATCH 11/12] clear i18n. --- sqladmin/application.py | 13 ------ sqladmin/templates/login.html | 24 ++++++------ .../translations/zh_CN/LC_MESSAGES/lang.mo | Bin 624 -> 0 bytes .../translations/zh_CN/LC_MESSAGES/lang.po | 37 ------------------ tests/test_admin_async.py | 14 ------- tests/test_admin_sync.py | 14 ------- 6 files changed, 12 insertions(+), 90 deletions(-) delete mode 100644 sqladmin/translations/zh_CN/LC_MESSAGES/lang.mo delete mode 100644 sqladmin/translations/zh_CN/LC_MESSAGES/lang.po diff --git a/sqladmin/application.py b/sqladmin/application.py index cdf04b48..dc3ea81b 100644 --- a/sqladmin/application.py +++ b/sqladmin/application.py @@ -1,5 +1,3 @@ -import gettext -import os from typing import TYPE_CHECKING, List, Type, Union import anyio @@ -54,17 +52,6 @@ def __init__( self.templates = Jinja2Templates("templates") self.templates.env.add_extension("jinja2.ext.i18n") - if language: - translation = gettext.translation( - "lang", - os.path.dirname(__file__) + "/translations", - languages=[language], - ) - self.templates.env.install_gettext_translations( # type: ignore - translation, newstyle=True - ) # type: ignore - else: - self.templates.env.install_null_translations(newstyle=True) # type: ignore self.templates.env.loader = ChoiceLoader( [ FileSystemLoader("templates"), diff --git a/sqladmin/templates/login.html b/sqladmin/templates/login.html index 0f63f4ba..77f70891 100644 --- a/sqladmin/templates/login.html +++ b/sqladmin/templates/login.html @@ -7,31 +7,31 @@
    -

    {{ _('Login to your account') }}

    +

    Login to your account

    - + {% if username_err==true %} - -
    {{ _('Username can not be null.') }}
    +
    Username can not be null.
    {% else %} - {% endif %}
    {% if password_err==true %} -
    {{ _('Password can not be null.') }}
    +
    Password can not be null.
    {% else %} {% endif %} @@ -49,16 +49,16 @@

    {{ _('Login to your account') }}

    {% if errinfo!="" %} - {{ _('Username or password is error!') }} + Username or password is error! {% endif %}
    diff --git a/sqladmin/translations/zh_CN/LC_MESSAGES/lang.mo b/sqladmin/translations/zh_CN/LC_MESSAGES/lang.mo deleted file mode 100644 index c84d7e8223aff9ceb931effe8612d07c03758b16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 624 zcmYk2&ui2`6vxL}twvC&6hS=9y-d|xNvkw=r*$zumTXeci%gSgI z74=Y0y(pg4gP|fDSw^{!SUvOo8@ZrmQyziSYb1*&m20={$mw*H~13U$q`Uac@ zz5_GBFW?k#09*uWpz%%|`#X0)F97F(b1b^Et>evrO3O8_4A+vC9Qmpc5?h70RHZS?GF3^7I?R{{O6Mr%B`?>(P;#tb zZ&MhY^ZO#>s4EJIg+%GUQys1D@ywETPS&_KLP-~QyyaWoD#dFw?zW@QiFsdXO9E4f zoZP4iLlWg;rxTGFt~MUhVwSu3HuYi>M5N{6n~@u|XYWKIb$n(lNi>tD$E?yWUP*l) zhh9MOA`QH@zvQ)AG3|D*!>TMQR&bJVR=U{t!qv1%vgrFuJu#fIl|cefAs_v*KghhN^q6b(LY*L#o9@Z;yh{pUwJFGo9QZ2AK? Cx35V6 diff --git a/sqladmin/translations/zh_CN/LC_MESSAGES/lang.po b/sqladmin/translations/zh_CN/LC_MESSAGES/lang.po deleted file mode 100644 index 2ec001f0..00000000 --- a/sqladmin/translations/zh_CN/LC_MESSAGES/lang.po +++ /dev/null @@ -1,37 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-01-27 10:58+0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: test.py:16 test.py:18 -msgid "Login to your account" -msgstr "登陆你的账户" - -msgid "Remember me on this device" -msgstr "记住我" - -msgid "Sign in" -msgstr "登陆" - -msgid "Username" -msgstr "用户名" - -msgid "Password" -msgstr "密码" - -msgid "Enter username" -msgstr "输入用户名" \ No newline at end of file diff --git a/tests/test_admin_async.py b/tests/test_admin_async.py index fd3daaa8..e9f0a7e1 100644 --- a/tests/test_admin_async.py +++ b/tests/test_admin_async.py @@ -475,20 +475,6 @@ def test_redirect() -> None: assert response.status_code == 303 -async def test_i18n() -> None: - app = Starlette() - i18n_admin = Admin(app=app, engine=engine, language="zh_CN") - i18n_admin.register_model(UserAdmin) - i18n_admin.register_model(AddressAdmin) - with TestClient(app) as client: - response = client.get("/admin/login") - - assert response.status_code == 200 - assert ( - response.text.count('

    登陆你的账户

    ') == 1 - ) - - async def test_expire_time() -> None: token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2NDMyNzYyMDl9._iFrRq5TqlomtM0bCr-p0L-VSst-bP9rZDf4s9Zc-1c" # noqa with TestClient(app) as client: diff --git a/tests/test_admin_sync.py b/tests/test_admin_sync.py index fadb4471..8555b9d4 100644 --- a/tests/test_admin_sync.py +++ b/tests/test_admin_sync.py @@ -477,20 +477,6 @@ def test_redirect() -> None: assert response.status_code == 303 -def test_i18n() -> None: - app = Starlette() - i18n_admin = Admin(app=app, engine=engine, language="zh_CN") - i18n_admin.register_model(UserAdmin) - i18n_admin.register_model(AddressAdmin) - with TestClient(app) as client: - response = client.get("/admin/login") - - assert response.status_code == 200 - assert ( - response.text.count('

    登陆你的账户

    ') == 1 - ) - - def test_expire_time() -> None: token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2NDMyNzYyMDl9._iFrRq5TqlomtM0bCr-p0L-VSst-bP9rZDf4s9Zc-1c" # noqa with TestClient(app) as client: From 57e5a22b0a9eb290fca46261e33d1d90ee3d5b14 Mon Sep 17 00:00:00 2001 From: yuxf Date: Fri, 28 Jan 2022 11:08:49 +0800 Subject: [PATCH 12/12] add logout button. --- sqladmin/statics/js/logout.js | 12 ++++ sqladmin/templates/layout.html | 100 +++++++++++++++++---------------- 2 files changed, 65 insertions(+), 47 deletions(-) create mode 100644 sqladmin/statics/js/logout.js diff --git a/sqladmin/statics/js/logout.js b/sqladmin/statics/js/logout.js new file mode 100644 index 00000000..8e3d1233 --- /dev/null +++ b/sqladmin/statics/js/logout.js @@ -0,0 +1,12 @@ +//设置cookie +function setCookie(cname, cvalue, exdays) { + var d = new Date(); + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + var expires = "expires=" + d.toUTCString(); + document.cookie = cname + "=" + cvalue + "; " + expires; +} + +function logout() { + setCookie("access_token", "", -1); + self.location = "/admin/login"; +} \ No newline at end of file diff --git a/sqladmin/templates/layout.html b/sqladmin/templates/layout.html index 8dc34c2d..317fb465 100644 --- a/sqladmin/templates/layout.html +++ b/sqladmin/templates/layout.html @@ -1,55 +1,61 @@ {% extends "base.html" %} {% block body %} -
    - +
    +
    + +
    +
    +
    +
    + {% block content %} {% endblock %} +
    +
    +
    + {% block footer %} + {% endblock %}
    -
    - {% block footer %} - {% endblock %} -
    - {% endblock %} +{% block head %} + +{% endblock %} \ No newline at end of file