🎨 Format files with pre-commit and Ruff (#611)
This commit is contained in:

committed by
GitHub

parent
2802a4df9e
commit
0cc802eec8
@@ -1 +1 @@
|
||||
Generic single-database configuration.
|
||||
Generic single-database configuration.
|
||||
|
@@ -1,10 +1,8 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
@@ -69,7 +67,9 @@ def run_migrations_online():
|
||||
configuration = config.get_section(config.config_ini_section)
|
||||
configuration["sqlalchemy.url"] = get_url()
|
||||
connectable = engine_from_config(
|
||||
configuration, prefix="sqlalchemy.", poolclass=pool.NullPool,
|
||||
configuration,
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
|
@@ -1,17 +1,16 @@
|
||||
"""Initialize models
|
||||
|
||||
Revision ID: e2412789c190
|
||||
Revises:
|
||||
Revises:
|
||||
Create Date: 2023-11-24 22:55:43.195942
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e2412789c190'
|
||||
revision = "e2412789c190"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
@@ -19,30 +18,37 @@ depends_on = None
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user',
|
||||
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('is_superuser', sa.Boolean(), nullable=False),
|
||||
sa.Column('full_name', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('hashed_password', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table(
|
||||
"user",
|
||||
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_superuser", sa.Boolean(), nullable=False),
|
||||
sa.Column("full_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
|
||||
op.create_table('item',
|
||||
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
|
||||
op.create_table(
|
||||
"item",
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("owner_id", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["owner_id"],
|
||||
["user.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('item')
|
||||
op.drop_index(op.f('ix_user_email'), table_name='user')
|
||||
op.drop_table('user')
|
||||
op.drop_table("item")
|
||||
op.drop_index(op.f("ix_user_email"), table_name="user")
|
||||
op.drop_table("user")
|
||||
# ### end Alembic commands ###
|
||||
|
@@ -1,10 +1,10 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from sqlmodel import select, func
|
||||
from sqlmodel import func, select
|
||||
|
||||
from app.api.deps import CurrentUser, SessionDep
|
||||
from app.models import Item, ItemCreate, ItemOut, ItemUpdate, Message, ItemsOut
|
||||
from app.models import Item, ItemCreate, ItemOut, ItemsOut, ItemUpdate, Message
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -22,7 +22,7 @@ def read_items(
|
||||
|
||||
if current_user.is_superuser:
|
||||
statement = select(Item).offset(skip).limit(limit)
|
||||
items = session.exec(statement).all()
|
||||
items = session.exec(statement).all()
|
||||
else:
|
||||
statement = (
|
||||
select(Item)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from typing import Any, List
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import select, func
|
||||
from sqlmodel import func, select
|
||||
|
||||
from app import crud
|
||||
from app.api.deps import (
|
||||
@@ -28,9 +28,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
dependencies=[Depends(get_current_active_superuser)],
|
||||
response_model=UsersOut
|
||||
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UsersOut
|
||||
)
|
||||
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
|
||||
"""
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from typing import Annotated, Generator
|
||||
from collections.abc import Generator
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import secrets
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any
|
||||
|
||||
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator
|
||||
|
||||
@@ -14,21 +14,21 @@ class Settings(BaseSettings):
|
||||
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
|
||||
# e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
|
||||
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
|
||||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||
BACKEND_CORS_ORIGINS: list[AnyHttpUrl] = []
|
||||
|
||||
@validator("BACKEND_CORS_ORIGINS", pre=True)
|
||||
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
|
||||
def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
|
||||
if isinstance(v, str) and not v.startswith("["):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
elif isinstance(v, (list, str)):
|
||||
elif isinstance(v, list | str):
|
||||
return v
|
||||
raise ValueError(v)
|
||||
|
||||
PROJECT_NAME: str
|
||||
SENTRY_DSN: Optional[HttpUrl] = None
|
||||
SENTRY_DSN: HttpUrl | None = None
|
||||
|
||||
@validator("SENTRY_DSN", pre=True)
|
||||
def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]:
|
||||
def sentry_dsn_can_be_blank(cls, v: str) -> str | None:
|
||||
if len(v) == 0:
|
||||
return None
|
||||
return v
|
||||
@@ -37,10 +37,10 @@ class Settings(BaseSettings):
|
||||
POSTGRES_USER: str
|
||||
POSTGRES_PASSWORD: str
|
||||
POSTGRES_DB: str
|
||||
SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
|
||||
SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None
|
||||
|
||||
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
|
||||
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
|
||||
def assemble_db_connection(cls, v: str | None, values: dict[str, Any]) -> Any:
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
return PostgresDsn.build(
|
||||
@@ -52,15 +52,15 @@ class Settings(BaseSettings):
|
||||
)
|
||||
|
||||
SMTP_TLS: bool = True
|
||||
SMTP_PORT: Optional[int] = None
|
||||
SMTP_HOST: Optional[str] = None
|
||||
SMTP_USER: Optional[str] = None
|
||||
SMTP_PASSWORD: Optional[str] = None
|
||||
EMAILS_FROM_EMAIL: Optional[EmailStr] = None
|
||||
EMAILS_FROM_NAME: Optional[str] = None
|
||||
SMTP_PORT: int | None = None
|
||||
SMTP_HOST: str | None = None
|
||||
SMTP_USER: str | None = None
|
||||
SMTP_PASSWORD: str | None = None
|
||||
EMAILS_FROM_EMAIL: EmailStr | None = None
|
||||
EMAILS_FROM_NAME: str | None = None
|
||||
|
||||
@validator("EMAILS_FROM_NAME")
|
||||
def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str:
|
||||
def get_project_name(cls, v: str | None, values: dict[str, Any]) -> str:
|
||||
if not v:
|
||||
return values["PROJECT_NAME"]
|
||||
return v
|
||||
@@ -70,7 +70,7 @@ class Settings(BaseSettings):
|
||||
EMAILS_ENABLED: bool = False
|
||||
|
||||
@validator("EMAILS_ENABLED", pre=True)
|
||||
def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool:
|
||||
def get_emails_enabled(cls, v: bool, values: dict[str, Any]) -> bool:
|
||||
return bool(
|
||||
values.get("SMTP_HOST")
|
||||
and values.get("SMTP_PORT")
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Union
|
||||
from typing import Any
|
||||
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
@@ -12,9 +12,7 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
|
||||
def create_access_token(
|
||||
subject: Union[str, Any], expires_delta: timedelta = None
|
||||
) -> str:
|
||||
def create_access_token(subject: str | Any, expires_delta: timedelta = None) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
|
@@ -1,16 +1,15 @@
|
||||
from .crud_item import item
|
||||
from .crud_user import user
|
||||
|
||||
# For a new basic set of CRUD operations you could just do
|
||||
|
||||
# from .base import CRUDBase
|
||||
# from app.models.item import Item
|
||||
# from app.schemas.item import ItemCreate, ItemUpdate
|
||||
|
||||
# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item)
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
from app.models import UserCreate, User
|
||||
from app.models import User, UserCreate
|
||||
|
||||
from .crud_item import item as item
|
||||
from .crud_user import user as user
|
||||
|
||||
|
||||
def create_user(*, session: Session, user_create: UserCreate) -> User:
|
||||
@@ -30,9 +29,9 @@ def get_user_by_email(*, session: Session, email: str) -> User | None:
|
||||
|
||||
|
||||
def authenticate(*, session: Session, email: str, password: str) -> User | None:
|
||||
user = get_user_by_email(session=session, email=email)
|
||||
if not user:
|
||||
db_user = get_user_by_email(session=session, email=email)
|
||||
if not db_user:
|
||||
return None
|
||||
if not verify_password(password, user.hashed_password):
|
||||
if not verify_password(password, db_user.hashed_password):
|
||||
return None
|
||||
return user
|
||||
return db_user
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
@@ -10,7 +10,7 @@ UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
|
||||
|
||||
|
||||
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
def __init__(self, model: Type[ModelType]):
|
||||
def __init__(self, model: type[ModelType]):
|
||||
"""
|
||||
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
|
||||
|
||||
@@ -21,7 +21,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
"""
|
||||
self.model = model
|
||||
|
||||
def get(self, db: Session, id: Any) -> Optional[ModelType]:
|
||||
def get(self, db: Session, id: Any) -> ModelType | None:
|
||||
return db.query(self.model).filter(self.model.id == id).first()
|
||||
|
||||
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
|
||||
@@ -37,7 +37,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
db: Session,
|
||||
*,
|
||||
db_obj: ModelType,
|
||||
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
|
||||
obj_in: UpdateSchemaType | dict[str, Any],
|
||||
) -> ModelType:
|
||||
obj_data = jsonable_encoder(db_obj)
|
||||
if isinstance(obj_in, dict):
|
||||
|
@@ -1,5 +1,3 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -21,7 +19,7 @@ class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]):
|
||||
|
||||
def get_multi_by_owner(
|
||||
self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100
|
||||
) -> List[Item]:
|
||||
) -> list[Item]:
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(Item.owner_id == owner_id)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -9,7 +9,7 @@ from app.schemas.user import UserCreate, UserUpdate
|
||||
|
||||
|
||||
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
|
||||
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
|
||||
def get_by_email(self, db: Session, *, email: str) -> User | None:
|
||||
return db.query(User).filter(User.email == email).first()
|
||||
|
||||
def create(self, db: Session, *, obj_in: UserCreate) -> User:
|
||||
@@ -25,7 +25,7 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
|
||||
return db_obj
|
||||
|
||||
def update(
|
||||
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
|
||||
self, db: Session, *, db_obj: User, obj_in: UserUpdate | dict[str, Any]
|
||||
) -> User:
|
||||
if isinstance(obj_in, dict):
|
||||
update_data = obj_in
|
||||
@@ -37,7 +37,7 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
|
||||
update_data["hashed_password"] = hashed_password
|
||||
return super().update(db, db_obj=db_obj, obj_in=update_data)
|
||||
|
||||
def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]:
|
||||
def authenticate(self, db: Session, *, email: str, password: str) -> User | None:
|
||||
user = self.get_by_email(db, email=email)
|
||||
if not user:
|
||||
return None
|
||||
|
@@ -23,4 +23,4 @@
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }} - New Account</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">You have a new account:</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Username: {{ username }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Password: {{ password }}</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:50px 0px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;padding:10px 25px;background:#414141;" valign="middle"><a href="{{ link }}" style="background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Go to Dashboard</a></td></tr></table></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
|
@@ -23,4 +23,4 @@
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }} - Password Recovery</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">We received a request to recover the password for user {{ username }} with email {{ email }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Reset your password by clicking the button below:</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:50px 0px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;padding:10px 25px;background:#414141;" valign="middle"><a href="{{ link }}" style="background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Reset Password</a></td></tr></table></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Or open the following link:</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;"><a href="{{ link }}">{{ link }}</a></div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">The reset password link / button will expire in {{ valid_hours }} hours.</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">The reset password link / button will expire in {{ valid_hours }} hours.</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
|
@@ -22,4 +22,4 @@
|
||||
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Test email for: {{ email }}</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Test email for: {{ email }}</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
|
@@ -1,5 +1,3 @@
|
||||
from typing import Union
|
||||
|
||||
from pydantic import EmailStr
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
@@ -9,7 +7,7 @@ class UserBase(SQLModel):
|
||||
email: EmailStr = Field(unique=True, index=True)
|
||||
is_active: bool = True
|
||||
is_superuser: bool = False
|
||||
full_name: Union[str, None] = None
|
||||
full_name: str | None = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
@@ -20,18 +18,18 @@ class UserCreate(UserBase):
|
||||
class UserCreateOpen(SQLModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
full_name: Union[str, None] = None
|
||||
full_name: str | None = None
|
||||
|
||||
|
||||
# Properties to receive via API on update, all are optional
|
||||
class UserUpdate(UserBase):
|
||||
email: Union[EmailStr, None] = None
|
||||
password: Union[str, None] = None
|
||||
email: EmailStr | None = None
|
||||
password: str | None = None
|
||||
|
||||
|
||||
class UserUpdateMe(SQLModel):
|
||||
full_name: Union[str, None] = None
|
||||
email: Union[EmailStr, None] = None
|
||||
full_name: str | None = None
|
||||
email: EmailStr | None = None
|
||||
|
||||
|
||||
class UpdatePassword(SQLModel):
|
||||
@@ -41,7 +39,7 @@ class UpdatePassword(SQLModel):
|
||||
|
||||
# Database model, database table inferred from class name
|
||||
class User(UserBase, table=True):
|
||||
id: Union[int, None] = Field(default=None, primary_key=True)
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
hashed_password: str
|
||||
items: list["Item"] = Relationship(back_populates="owner")
|
||||
|
||||
@@ -59,7 +57,7 @@ class UsersOut(SQLModel):
|
||||
# Shared properties
|
||||
class ItemBase(SQLModel):
|
||||
title: str
|
||||
description: Union[str, None] = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
# Properties to receive on item creation
|
||||
@@ -69,17 +67,15 @@ class ItemCreate(ItemBase):
|
||||
|
||||
# Properties to receive on item update
|
||||
class ItemUpdate(ItemBase):
|
||||
title: Union[str, None] = None
|
||||
title: str | None = None
|
||||
|
||||
|
||||
# Database model, database table inferred from class name
|
||||
class Item(ItemBase, table=True):
|
||||
id: Union[int, None] = Field(default=None, primary_key=True)
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
title: str
|
||||
owner_id: Union[int, None] = Field(
|
||||
default=None, foreign_key="user.id", nullable=False
|
||||
)
|
||||
owner: Union[User, None] = Relationship(back_populates="items")
|
||||
owner_id: int | None = Field(default=None, foreign_key="user.id", nullable=False)
|
||||
owner: User | None = Relationship(back_populates="items")
|
||||
|
||||
|
||||
# Properties to return via API, id is always required
|
||||
@@ -106,7 +102,7 @@ class Token(SQLModel):
|
||||
|
||||
# Contents of JWT token
|
||||
class TokenPayload(SQLModel):
|
||||
sub: Union[int, None] = None
|
||||
sub: int | None = None
|
||||
|
||||
|
||||
class NewPassword(SQLModel):
|
||||
|
@@ -1,12 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Shared properties
|
||||
class ItemBase(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
# Properties to receive on item creation
|
||||
|
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -9,4 +7,4 @@ class Token(BaseModel):
|
||||
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
sub: Optional[int] = None
|
||||
sub: int | None = None
|
||||
|
@@ -1,14 +1,12 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
|
||||
# Shared properties
|
||||
class UserBase(BaseModel):
|
||||
email: Optional[EmailStr] = None
|
||||
is_active: Optional[bool] = True
|
||||
email: EmailStr | None = None
|
||||
is_active: bool | None = True
|
||||
is_superuser: bool = False
|
||||
full_name: Optional[str] = None
|
||||
full_name: str | None = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
@@ -19,11 +17,11 @@ class UserCreate(UserBase):
|
||||
|
||||
# Properties to receive via API on update
|
||||
class UserUpdate(UserBase):
|
||||
password: Optional[str] = None
|
||||
password: str | None = None
|
||||
|
||||
|
||||
class UserInDBBase(UserBase):
|
||||
id: Optional[int] = None
|
||||
id: int | None = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
@@ -1,12 +1,10 @@
|
||||
from typing import Dict
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
def test_celery_worker_test(
|
||||
client: TestClient, superuser_token_headers: Dict[str, str]
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
data = {"message": "test"}
|
||||
r = client.post(
|
||||
|
@@ -10,7 +10,9 @@ def test_create_item(
|
||||
) -> None:
|
||||
data = {"title": "Foo", "description": "Fighters"}
|
||||
response = client.post(
|
||||
f"{settings.API_V1_STR}/items/", headers=superuser_token_headers, json=data,
|
||||
f"{settings.API_V1_STR}/items/",
|
||||
headers=superuser_token_headers,
|
||||
json=data,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
@@ -25,7 +27,8 @@ def test_read_item(
|
||||
) -> None:
|
||||
item = create_random_item(db)
|
||||
response = client.get(
|
||||
f"{settings.API_V1_STR}/items/{item.id}", headers=superuser_token_headers,
|
||||
f"{settings.API_V1_STR}/items/{item.id}",
|
||||
headers=superuser_token_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
|
@@ -1,5 +1,3 @@
|
||||
from typing import Dict
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.config import settings
|
||||
@@ -18,10 +16,11 @@ def test_get_access_token(client: TestClient) -> None:
|
||||
|
||||
|
||||
def test_use_access_token(
|
||||
client: TestClient, superuser_token_headers: Dict[str, str]
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/login/test-token", headers=superuser_token_headers,
|
||||
f"{settings.API_V1_STR}/login/test-token",
|
||||
headers=superuser_token_headers,
|
||||
)
|
||||
result = r.json()
|
||||
assert r.status_code == 200
|
||||
|
@@ -1,5 +1,3 @@
|
||||
from typing import Dict
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -10,7 +8,7 @@ from app.tests.utils.utils import random_email, random_lower_string
|
||||
|
||||
|
||||
def test_get_users_superuser_me(
|
||||
client: TestClient, superuser_token_headers: Dict[str, str]
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
r = client.get(f"{settings.API_V1_STR}/users/me", headers=superuser_token_headers)
|
||||
current_user = r.json()
|
||||
@@ -21,7 +19,7 @@ def test_get_users_superuser_me(
|
||||
|
||||
|
||||
def test_get_users_normal_user_me(
|
||||
client: TestClient, normal_user_token_headers: Dict[str, str]
|
||||
client: TestClient, normal_user_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
r = client.get(f"{settings.API_V1_STR}/users/me", headers=normal_user_token_headers)
|
||||
current_user = r.json()
|
||||
@@ -38,7 +36,9 @@ def test_create_user_new_email(
|
||||
password = random_lower_string()
|
||||
data = {"email": username, "password": password}
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/users/", headers=superuser_token_headers, json=data,
|
||||
f"{settings.API_V1_STR}/users/",
|
||||
headers=superuser_token_headers,
|
||||
json=data,
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
created_user = r.json()
|
||||
@@ -56,7 +56,8 @@ def test_get_existing_user(
|
||||
user = crud.user.create(db, obj_in=user_in)
|
||||
user_id = user.id
|
||||
r = client.get(
|
||||
f"{settings.API_V1_STR}/users/{user_id}", headers=superuser_token_headers,
|
||||
f"{settings.API_V1_STR}/users/{user_id}",
|
||||
headers=superuser_token_headers,
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
api_user = r.json()
|
||||
@@ -75,7 +76,9 @@ def test_create_user_existing_username(
|
||||
crud.user.create(db, obj_in=user_in)
|
||||
data = {"email": username, "password": password}
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/users/", headers=superuser_token_headers, json=data,
|
||||
f"{settings.API_V1_STR}/users/",
|
||||
headers=superuser_token_headers,
|
||||
json=data,
|
||||
)
|
||||
created_user = r.json()
|
||||
assert r.status_code == 400
|
||||
@@ -83,13 +86,15 @@ def test_create_user_existing_username(
|
||||
|
||||
|
||||
def test_create_user_by_normal_user(
|
||||
client: TestClient, normal_user_token_headers: Dict[str, str]
|
||||
client: TestClient, normal_user_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
username = random_email()
|
||||
password = random_lower_string()
|
||||
data = {"email": username, "password": password}
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/users/", headers=normal_user_token_headers, json=data,
|
||||
f"{settings.API_V1_STR}/users/",
|
||||
headers=normal_user_token_headers,
|
||||
json=data,
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, Generator
|
||||
from collections.abc import Generator
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -24,12 +24,12 @@ def client() -> Generator:
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def superuser_token_headers(client: TestClient) -> Dict[str, str]:
|
||||
def superuser_token_headers(client: TestClient) -> dict[str, str]:
|
||||
return get_superuser_token_headers(client)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def normal_user_token_headers(client: TestClient, db: Session) -> Dict[str, str]:
|
||||
def normal_user_token_headers(client: TestClient, db: Session) -> dict[str, str]:
|
||||
return authentication_token_from_email(
|
||||
client=client, email=settings.EMAIL_TEST_USER, db=db
|
||||
)
|
||||
|
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import crud, models
|
||||
@@ -8,7 +6,7 @@ from app.tests.utils.user import create_random_user
|
||||
from app.tests.utils.utils import random_lower_string
|
||||
|
||||
|
||||
def create_random_item(db: Session, *, owner_id: Optional[int] = None) -> models.Item:
|
||||
def create_random_item(db: Session, *, owner_id: int | None = None) -> models.Item:
|
||||
if owner_id is None:
|
||||
user = create_random_user(db)
|
||||
owner_id = user.id
|
||||
|
@@ -1,5 +1,3 @@
|
||||
from typing import Dict
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -12,7 +10,7 @@ from app.tests.utils.utils import random_email, random_lower_string
|
||||
|
||||
def user_authentication_headers(
|
||||
*, client: TestClient, email: str, password: str
|
||||
) -> Dict[str, str]:
|
||||
) -> dict[str, str]:
|
||||
data = {"username": email, "password": password}
|
||||
|
||||
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=data)
|
||||
@@ -32,7 +30,7 @@ def create_random_user(db: Session) -> User:
|
||||
|
||||
def authentication_token_from_email(
|
||||
*, client: TestClient, email: str, db: Session
|
||||
) -> Dict[str, str]:
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Return a valid token for the user with given email.
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import random
|
||||
import string
|
||||
from typing import Dict
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -15,7 +14,7 @@ def random_email() -> str:
|
||||
return f"{random_lower_string()}@{random_lower_string()}.com"
|
||||
|
||||
|
||||
def get_superuser_token_headers(client: TestClient) -> Dict[str, str]:
|
||||
def get_superuser_token_headers(client: TestClient) -> dict[str, str]:
|
||||
login_data = {
|
||||
"username": settings.FIRST_SUPERUSER,
|
||||
"password": settings.FIRST_SUPERUSER_PASSWORD,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
|
||||
import emails
|
||||
from emails.template import JinjaTemplate
|
||||
@@ -14,8 +14,9 @@ def send_email(
|
||||
email_to: str,
|
||||
subject_template: str = "",
|
||||
html_template: str = "",
|
||||
environment: Dict[str, Any] = {},
|
||||
environment: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
current_environment = environment or {}
|
||||
assert settings.EMAILS_ENABLED, "no provided configuration for email variables"
|
||||
message = emails.Message(
|
||||
subject=JinjaTemplate(subject_template),
|
||||
@@ -29,7 +30,7 @@ def send_email(
|
||||
smtp_options["user"] = settings.SMTP_USER
|
||||
if settings.SMTP_PASSWORD:
|
||||
smtp_options["password"] = settings.SMTP_PASSWORD
|
||||
response = message.send(to=email_to, render=environment, smtp=smtp_options)
|
||||
response = message.send(to=email_to, render=current_environment, smtp=smtp_options)
|
||||
logging.info(f"send email result: {response}")
|
||||
|
||||
|
||||
@@ -42,7 +43,7 @@ def send_test_email(email_to: str) -> None:
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={"project_name": settings.PROJECT_NAME, "email": email_to},
|
||||
current_environment={"project_name": settings.PROJECT_NAME, "email": email_to},
|
||||
)
|
||||
|
||||
|
||||
@@ -57,7 +58,7 @@ def send_reset_password_email(email_to: str, email: str, token: str) -> None:
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={
|
||||
current_environment={
|
||||
"project_name": settings.PROJECT_NAME,
|
||||
"username": email,
|
||||
"email": email_to,
|
||||
@@ -77,7 +78,7 @@ def send_new_account_email(email_to: str, username: str, password: str) -> None:
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={
|
||||
current_environment={
|
||||
"project_name": settings.PROJECT_NAME,
|
||||
"username": username,
|
||||
"password": password,
|
||||
@@ -93,12 +94,14 @@ def generate_password_reset_token(email: str) -> str:
|
||||
expires = now + delta
|
||||
exp = expires.timestamp()
|
||||
encoded_jwt = jwt.encode(
|
||||
{"exp": exp, "nbf": now, "sub": email}, settings.SECRET_KEY, algorithm="HS256",
|
||||
{"exp": exp, "nbf": now, "sub": email},
|
||||
settings.SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_password_reset_token(token: str) -> Optional[str]:
|
||||
def verify_password_reset_token(token: str) -> str | None:
|
||||
try:
|
||||
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
||||
return decoded_token["email"]
|
||||
|
Reference in New Issue
Block a user