Done setup template
This commit is contained in:
1
backend/__init__.py
Normal file
1
backend/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__version__ = "develop"
|
82
backend/app.py
Normal file
82
backend/app.py
Normal file
@ -0,0 +1,82 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from backend.core.config import get_app_settings
|
||||
from backend.core.root_logger import get_logger
|
||||
from backend.routes import router
|
||||
from backend import __version__
|
||||
import uvicorn
|
||||
|
||||
settings = get_app_settings()
|
||||
logger = get_logger()
|
||||
|
||||
description = f"""
|
||||
fuware is a web application for managing your hours items and tracking them.
|
||||
"""
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan_fn(_: FastAPI) -> AsyncGenerator[None, None]:
|
||||
logger.info("start: database initialization")
|
||||
import backend.db.init_db as init_db
|
||||
|
||||
init_db.main()
|
||||
logger.info("end: database initialization")
|
||||
|
||||
logger.info("-----SYSTEM STARTUP-----")
|
||||
# logger.info("------APP SETTINGS------")
|
||||
# logger.info(
|
||||
# settings.model_dump_json(
|
||||
# indent=4,
|
||||
# exclude={
|
||||
# "SECRET",
|
||||
# "DB_URL", # replace by DB_URL_PUBLIC for logs
|
||||
# "DB_PROVIDER",
|
||||
# },
|
||||
# )
|
||||
# )
|
||||
yield
|
||||
logger.info("-----SYSTEM SHUTDOWN-----")
|
||||
|
||||
app = FastAPI(
|
||||
title="Fuware",
|
||||
description=description,
|
||||
version=__version__,
|
||||
docs_url=settings.DOCS_URL,
|
||||
redoc_url=settings.REDOC_URL,
|
||||
lifespan=lifespan_fn,
|
||||
)
|
||||
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
|
||||
if not settings.PRODUCTION:
|
||||
allowed_origins = ["http://localhost:3000"]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=allowed_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def unicorn_exception_handler(request: Request, exc: HTTPException):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"status": exc.status_code, "data": exc.detail},
|
||||
)
|
||||
|
||||
def api_routers():
|
||||
app.include_router(router)
|
||||
|
||||
api_routers()
|
||||
|
||||
def main():
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=settings.API_PORT, reload=True, workers=1, forwarded_allow_ips="*")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1
backend/core/__init__.py
Normal file
1
backend/core/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .message_code import *
|
31
backend/core/config.py
Normal file
31
backend/core/config.py
Normal file
@ -0,0 +1,31 @@
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from backend.core.settings import AppSettings, app_settings_constructor
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
BASE_DIR = CWD.parent.parent
|
||||
ENV = BASE_DIR.joinpath(".env")
|
||||
|
||||
load_dotenv()
|
||||
PRODUCTION = os.getenv("PRODUCTION", "True").lower() in ["true", "1"]
|
||||
TESTING = os.getenv("TESTING", "False").lower() in ["true", "1"]
|
||||
DATA_DIR = os.getenv("DATA_DIR")
|
||||
|
||||
def determine_data_dir() -> Path:
|
||||
global PRODUCTION, TESTING, BASE_DIR, DATA_DIR
|
||||
|
||||
if TESTING:
|
||||
return BASE_DIR.joinpath(DATA_DIR if DATA_DIR else "tests/.temp")
|
||||
|
||||
if PRODUCTION:
|
||||
return Path(DATA_DIR if DATA_DIR else "/app/data")
|
||||
|
||||
return BASE_DIR.joinpath("dev", "data")
|
||||
|
||||
@lru_cache
|
||||
def get_app_settings() -> AppSettings:
|
||||
return app_settings_constructor(env_file=ENV, production=PRODUCTION, data_dir=determine_data_dir())
|
1
backend/core/dependencies/__init__.py
Normal file
1
backend/core/dependencies/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .dependencies import *
|
76
backend/core/dependencies/dependencies.py
Normal file
76
backend/core/dependencies/dependencies.py
Normal file
@ -0,0 +1,76 @@
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from backend.core.config import get_app_settings
|
||||
from backend.core import MessageCode
|
||||
import jwt
|
||||
|
||||
from backend.services.user.user_service import UserService
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
||||
oauth2_scheme_soft_fail = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
|
||||
ALGORITHM = "HS256"
|
||||
settings = get_app_settings()
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
async def is_logged_in(token: str = Depends(oauth2_scheme_soft_fail)) -> bool:
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("sub")
|
||||
exp: int = payload.get("exp")
|
||||
|
||||
if exp is not None:
|
||||
try:
|
||||
user_service = UserService()
|
||||
user = user_service.get_by_id(user_id)
|
||||
if not user:
|
||||
raise credentials_exception
|
||||
if user.is_lock is True:
|
||||
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=MessageCode.ACCOUNT_LOCK)
|
||||
except Exception:
|
||||
return credentials_exception
|
||||
|
||||
return user
|
||||
except Exception:
|
||||
raise credentials_exception
|
||||
|
||||
async def get_current_user(request: Request, token: str | None = Depends(oauth2_scheme_soft_fail)):
|
||||
"""verify that user has a valid session"""
|
||||
if token is None and settings.COOKIE_KEY in request.cookies:
|
||||
# Try extract from cookie
|
||||
token = request.cookies.get(settings.COOKIE_KEY, "")
|
||||
else:
|
||||
token = token or ""
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("sub")
|
||||
exp: int = payload.get("exp")
|
||||
|
||||
if user_id is None or exp is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="credentials have expired",
|
||||
)
|
||||
|
||||
user_service = UserService()
|
||||
user = user_service.get_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
raise credentials_exception
|
||||
if user.is_lock is True:
|
||||
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=MessageCode.ACCOUNT_LOCK)
|
||||
|
||||
return user
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="credentials have expired",
|
||||
)
|
||||
except Exception:
|
||||
raise credentials_exception
|
66
backend/core/logger/config.py
Normal file
66
backend/core/logger/config.py
Normal file
@ -0,0 +1,66 @@
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import typing
|
||||
from logging import config as logging_config
|
||||
|
||||
__dir = pathlib.Path(__file__).parent
|
||||
__conf: dict[str, str] | None = None
|
||||
|
||||
def _load_config(path: pathlib.Path, substitutions: dict[str, str] | None = None) -> dict[str, typing.Any]:
|
||||
with open(path) as file:
|
||||
if substitutions:
|
||||
contents = file.read()
|
||||
for key, value in substitutions.items():
|
||||
# Replaces the key matches
|
||||
#
|
||||
# Example:
|
||||
# {"key": "value"}
|
||||
# "/path/to/${key}/file" -> "/path/to/value/file"
|
||||
contents = contents.replace(f"${{{key}}}", value)
|
||||
|
||||
json_data = json.loads(contents)
|
||||
|
||||
else:
|
||||
json_data = json.load(file)
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
def log_config() -> dict[str, str]:
|
||||
if __conf is None:
|
||||
raise ValueError("logger not configured, must call configured_logger first")
|
||||
|
||||
return __conf
|
||||
|
||||
|
||||
def configured_logger(
|
||||
*,
|
||||
mode: str,
|
||||
config_override: pathlib.Path | None = None,
|
||||
substitutions: dict[str, str] | None = None,
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
Configure the logger based on the mode and return the root logger
|
||||
|
||||
Args:
|
||||
mode (str): The mode to configure the logger for (production, development, testing)
|
||||
config_override (pathlib.Path, optional): A path to a custom logging config. Defaults to None.
|
||||
substitutions (dict[str, str], optional): A dictionary of substitutions to apply to the logging config.
|
||||
"""
|
||||
global __conf
|
||||
|
||||
if config_override:
|
||||
__conf = _load_config(config_override, substitutions)
|
||||
else:
|
||||
if mode == "production":
|
||||
__conf = _load_config(__dir / "logconf.prod.json", substitutions)
|
||||
elif mode == "development":
|
||||
__conf = _load_config(__dir / "logconf.dev.json", substitutions)
|
||||
elif mode == "testing":
|
||||
__conf = _load_config(__dir / "logconf.test.json", substitutions)
|
||||
else:
|
||||
raise ValueError(f"Invalid mode: {mode}")
|
||||
|
||||
logging_config.dictConfig(config=__conf)
|
||||
return logging.getLogger()
|
15
backend/core/logger/logconf.dev.json
Normal file
15
backend/core/logger/logconf.dev.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": false,
|
||||
"handlers": {
|
||||
"rich": {
|
||||
"class": "rich.logging.RichHandler"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"level": "DEBUG",
|
||||
"handlers": ["rich"]
|
||||
}
|
||||
}
|
||||
}
|
63
backend/core/logger/logconf.prod.json
Normal file
63
backend/core/logger/logconf.prod.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "%(levelname)-8s %(asctime)s - %(message)s",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S"
|
||||
},
|
||||
"detailed": {
|
||||
"format": "[%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s: %(message)s",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S"
|
||||
},
|
||||
"access": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": "%(levelname)-8s %(asctime)s - [%(client_addr)s] %(status_code)s \"%(request_line)s\"",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"stderr": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "WARNING",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stderr"
|
||||
},
|
||||
"stdout": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "${LOG_LEVEL}",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stdout"
|
||||
},
|
||||
"access": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "${LOG_LEVEL}",
|
||||
"formatter": "access",
|
||||
"stream": "ext://sys.stdout"
|
||||
},
|
||||
"file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "detailed",
|
||||
"filename": "${DATA_DIR}/mealie.log",
|
||||
"maxBytes": 10000,
|
||||
"backupCount": 3
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"level": "${LOG_LEVEL}",
|
||||
"handlers": ["stderr", "file", "stdout"]
|
||||
},
|
||||
"uvicorn.error": {
|
||||
"handlers": ["stderr", "file", "stdout"],
|
||||
"level": "${LOG_LEVEL}",
|
||||
"propagate": false
|
||||
},
|
||||
"uvicorn.access": {
|
||||
"handlers": ["access", "file"],
|
||||
"level": "${LOG_LEVEL}",
|
||||
"propagate": false
|
||||
}
|
||||
}
|
||||
}
|
24
backend/core/logger/logconf.test.json
Normal file
24
backend/core/logger/logconf.test.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"detailed": {
|
||||
"format": "[%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s: %(message)s",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"stdout": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "detailed",
|
||||
"stream": "ext://sys.stdout"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"level": "${LOG_LEVEL}",
|
||||
"handlers": ["stdout"]
|
||||
}
|
||||
}
|
||||
}
|
6
backend/core/message_code.py
Normal file
6
backend/core/message_code.py
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
class MessageCode():
|
||||
CREATED_USER: str = 'CREATED_USER'
|
||||
WRONG_INPUT: str = 'LOGIN_WRONG'
|
||||
ACCOUNT_LOCK: str = 'USER_LOCK'
|
||||
REFRESH_TOKEN_EXPIRED: str = 'REFRESH_TOKEN_EXPIRED'
|
43
backend/core/root_logger.py
Normal file
43
backend/core/root_logger.py
Normal file
@ -0,0 +1,43 @@
|
||||
import logging
|
||||
|
||||
from .config import get_app_settings
|
||||
from .logger.config import configured_logger
|
||||
|
||||
__root_logger: None | logging.Logger = None
|
||||
|
||||
|
||||
def get_logger(module=None) -> logging.Logger:
|
||||
"""
|
||||
Get a logger instance for a module, in most cases module should not be
|
||||
provided. Simply using the root logger is sufficient.
|
||||
|
||||
Cases where you would want to use a module specific logger might be a background
|
||||
task or a long running process where you want to easily identify the source of
|
||||
those messages
|
||||
"""
|
||||
global __root_logger
|
||||
|
||||
if __root_logger is None:
|
||||
app_settings = get_app_settings()
|
||||
|
||||
mode = "development"
|
||||
|
||||
if app_settings.TESTING:
|
||||
mode = "testing"
|
||||
elif app_settings.PRODUCTION:
|
||||
mode = "production"
|
||||
|
||||
substitutions = {
|
||||
"LOG_LEVEL": app_settings.LOG_LEVEL.upper(),
|
||||
}
|
||||
|
||||
__root_logger = configured_logger(
|
||||
mode=mode,
|
||||
config_override=app_settings.LOG_CONFIG_OVERRIDE,
|
||||
substitutions=substitutions,
|
||||
)
|
||||
|
||||
if module is None:
|
||||
return __root_logger
|
||||
|
||||
return __root_logger.getChild(module)
|
1
backend/core/security/__init__.py
Normal file
1
backend/core/security/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .security import *
|
34
backend/core/security/hasher.py
Normal file
34
backend/core/security/hasher.py
Normal file
@ -0,0 +1,34 @@
|
||||
from functools import lru_cache
|
||||
from typing import Protocol
|
||||
import bcrypt
|
||||
|
||||
from backend.core.config import get_app_settings
|
||||
|
||||
|
||||
class Hasher(Protocol):
|
||||
def hash(self, password: str) -> str: ...
|
||||
|
||||
def verify(self, password: str, hashed: str) -> bool: ...
|
||||
|
||||
class FakeHasher:
|
||||
def hash(self, password: str) -> str:
|
||||
return password
|
||||
|
||||
def verify(self, password: str, hashed: str) -> bool:
|
||||
return password == hashed
|
||||
|
||||
class BcryptHasher:
|
||||
def hash(self, password: str) -> str:
|
||||
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
def verify(self, password: str, hashed: str) -> bool:
|
||||
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_hasher() -> Hasher:
|
||||
settings = get_app_settings()
|
||||
|
||||
if settings.TESTING:
|
||||
return FakeHasher()
|
||||
|
||||
return BcryptHasher()
|
46
backend/core/security/security.py
Normal file
46
backend/core/security/security.py
Normal file
@ -0,0 +1,46 @@
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import jwt
|
||||
|
||||
from backend.core.config import get_app_settings
|
||||
from backend.core import root_logger
|
||||
from backend.core.security.hasher import get_hasher
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
logger = root_logger.get_logger("security")
|
||||
settings = get_app_settings()
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
||||
settings = get_app_settings()
|
||||
|
||||
to_encode = data.copy()
|
||||
expires_delta = expires_delta or timedelta(minutes=settings.EXP_TOKEN)
|
||||
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
|
||||
to_encode["exp"] = expire
|
||||
return jwt.encode(to_encode, settings.SECRET, algorithm=ALGORITHM)
|
||||
|
||||
def create_refresh_token(data: dict) -> str:
|
||||
return create_access_token(data, expires_delta=timedelta(days=settings.EXP_REFRESH))
|
||||
|
||||
def create_file_token(file_path: Path) -> str:
|
||||
token_data = {"file": str(file_path)}
|
||||
return create_access_token(token_data, expires_delta=timedelta(minutes=30))
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Takes in a raw password and hashes it. Used prior to saving a new password to the database."""
|
||||
return get_hasher().hash(password)
|
||||
|
||||
|
||||
def url_safe_token() -> str:
|
||||
"""Generates a cryptographic token without embedded data. Used for password reset tokens and invitation tokens"""
|
||||
return secrets.token_urlsafe(24)
|
||||
|
||||
def verify_token(exp: int):
|
||||
expried = datetime.fromtimestamp(exp / 1e3)
|
||||
return expried < datetime.now(timezone.utc)
|
1
backend/core/settings/__init__.py
Normal file
1
backend/core/settings/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .settings import *
|
28
backend/core/settings/db_providers.py
Normal file
28
backend/core/settings/db_providers.py
Normal file
@ -0,0 +1,28 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AbstractDBProvider(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def db_url(self) -> str: ...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def db_url_public(self) -> str: ...
|
||||
|
||||
class SQLiteProvider(AbstractDBProvider, BaseModel):
|
||||
data_dir: Path
|
||||
prefix: str = ""
|
||||
|
||||
@property
|
||||
def db_path(self):
|
||||
return self.data_dir / f"{self.prefix}fuware.db"
|
||||
|
||||
@property
|
||||
def db_url(self) -> str:
|
||||
return f"sqlite:///{str(self.db_path.absolute())}"
|
||||
|
||||
@property
|
||||
def db_url_public(self) -> str:
|
||||
return self.db_url
|
80
backend/core/settings/settings.py
Normal file
80
backend/core/settings/settings.py
Normal file
@ -0,0 +1,80 @@
|
||||
from pathlib import Path
|
||||
from backend.core.settings.db_providers import AbstractDBProvider, SQLiteProvider
|
||||
from pydantic_settings import BaseSettings # type: ignore
|
||||
|
||||
|
||||
def determine_secrets(production: bool) -> str:
|
||||
if not production:
|
||||
return "shh-secret-test-key"
|
||||
|
||||
return "1d00e664fb3b07aff5a191755ea72f9c4bc85a3f36868308d0b2c417aed3419e"
|
||||
|
||||
def determine_cookie(production: bool) -> str:
|
||||
if not production:
|
||||
return "logcook"
|
||||
|
||||
return "7fo24CMyIc"
|
||||
|
||||
class AppSettings(BaseSettings):
|
||||
PRODUCTION: bool
|
||||
TESTING: bool
|
||||
BASE_URL: str = "http://localhost:8080"
|
||||
"""trailing slashes are trimmed (ex. `http://localhost:8080/` becomes ``http://localhost:8080`)"""
|
||||
|
||||
HOST_IP: str = "*"
|
||||
|
||||
API_HOST: str = "0.0.0.0"
|
||||
API_PORT: int = 9000
|
||||
API_DOCS: bool = True
|
||||
|
||||
ALLOW_SIGNUP: bool = False
|
||||
|
||||
SECRET: str
|
||||
COOKIE_KEY: str
|
||||
EXP_TOKEN: int = 30
|
||||
"""in minutes, default is 30 minutes"""
|
||||
EXP_REFRESH: int = 7
|
||||
"""in days, default is 7 days"""
|
||||
|
||||
LOG_CONFIG_OVERRIDE: Path | None = None
|
||||
"""path to custom logging configuration file"""
|
||||
|
||||
LOG_LEVEL: str = "info"
|
||||
"""corresponds to standard Python log levels"""
|
||||
|
||||
@property
|
||||
def DOCS_URL(self) -> str | None:
|
||||
return "/docs" if self.API_DOCS else None
|
||||
|
||||
@property
|
||||
def REDOC_URL(self) -> str | None:
|
||||
return "/redoc" if self.API_DOCS else None
|
||||
|
||||
# ===============================================
|
||||
# Database Configuration
|
||||
|
||||
DB_PROVIDER: AbstractDBProvider | None = None
|
||||
|
||||
@property
|
||||
def DB_URL(self) -> str | None:
|
||||
return self.DB_PROVIDER.db_url if self.DB_PROVIDER else None
|
||||
|
||||
@property
|
||||
def DB_URL_PUBLIC(self) -> str | None:
|
||||
return self.DB_PROVIDER.db_url_public if self.DB_PROVIDER else None
|
||||
|
||||
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
||||
"""
|
||||
app_settings_constructor is a factory function that returns an AppSettings object. It is used to inject the
|
||||
required dependencies into the AppSettings object and nested child objects. AppSettings should not be substantiated
|
||||
directly, but rather through this factory function.
|
||||
"""
|
||||
app_settings = AppSettings(
|
||||
_env_file=env_file, # type: ignore
|
||||
_env_file_encoding=env_encoding, # type: ignore
|
||||
**{"SECRET": determine_secrets(production), 'COOKIE_KEY': determine_cookie(production)},
|
||||
)
|
||||
|
||||
app_settings.DB_PROVIDER = SQLiteProvider(data_dir=data_dir)
|
||||
|
||||
return app_settings
|
1
backend/db/__init__.py
Normal file
1
backend/db/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .db_setup import *
|
48
backend/db/db_setup.py
Normal file
48
backend/db/db_setup.py
Normal file
@ -0,0 +1,48 @@
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy import create_engine, event, Engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from backend.core.config import get_app_settings
|
||||
|
||||
settings = get_app_settings()
|
||||
|
||||
def sql_global_init(db_url: str):
|
||||
connect_args = {"check_same_thread": False}
|
||||
|
||||
engine = create_engine(db_url, echo=False, connect_args=connect_args, pool_pre_ping=True, future=True)
|
||||
|
||||
SessionLocal = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine, future=True))
|
||||
|
||||
return SessionLocal, engine
|
||||
|
||||
SessionLocal, engine = sql_global_init(settings.DB_URL)
|
||||
|
||||
@event.listens_for(Engine, "connect")
|
||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.close()
|
||||
|
||||
@contextmanager
|
||||
def session_context() -> Session: # type: ignore
|
||||
"""
|
||||
session_context() provides a managed session to the database that is automatically
|
||||
closed when the context is exited. This is the preferred method of accessing the
|
||||
database.
|
||||
|
||||
Note: use `generate_session` when using the `Depends` function from FastAPI
|
||||
"""
|
||||
global SessionLocal
|
||||
sess = SessionLocal()
|
||||
try:
|
||||
yield sess
|
||||
finally:
|
||||
sess.close()
|
||||
|
||||
def generate_session() -> Generator[Session, None, None]:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
86
backend/db/init_db.py
Normal file
86
backend/db/init_db.py
Normal file
@ -0,0 +1,86 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
from sqlalchemy import engine, orm, text
|
||||
|
||||
from alembic import command, config, script
|
||||
from alembic.config import Config
|
||||
from alembic.runtime import migration
|
||||
from backend.core import root_logger
|
||||
from backend.core.config import get_app_settings
|
||||
from backend.db.db_setup import session_context
|
||||
from backend.repos.repository_users import RepositoryUsers
|
||||
from backend.repos.seeder import default_users_init
|
||||
# from fuware.db.models import User
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
logger = root_logger.get_logger()
|
||||
|
||||
def init_db(db) -> None:
|
||||
logger.info("Initializing user data...")
|
||||
default_users_init(db)
|
||||
|
||||
def db_is_at_head(alembic_cfg: config.Config) -> bool:
|
||||
settings = get_app_settings()
|
||||
url = settings.DB_URL
|
||||
|
||||
if not url:
|
||||
raise ValueError("No database url found")
|
||||
|
||||
connectable = engine.create_engine(url)
|
||||
directory = script.ScriptDirectory.from_config(alembic_cfg)
|
||||
with connectable.begin() as connection:
|
||||
context = migration.MigrationContext.configure(connection)
|
||||
return set(context.get_current_heads()) == set(directory.get_heads())
|
||||
|
||||
def connect(session: orm.Session) -> bool:
|
||||
try:
|
||||
session.execute(text("SELECT 1"))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to database: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
max_retry = 10
|
||||
wait_seconds = 1
|
||||
|
||||
with session_context() as session:
|
||||
while True:
|
||||
if connect(session):
|
||||
logger.info("Database connection established.")
|
||||
break
|
||||
|
||||
logger.error(f"Database connection failed. Retrying in {wait_seconds} seconds...")
|
||||
max_retry -= 1
|
||||
|
||||
sleep(wait_seconds)
|
||||
|
||||
if max_retry == 0:
|
||||
raise ConnectionError("Database connection failed - exiting application.")
|
||||
|
||||
alembic_cfg_path = os.getenv("ALEMBIC_CONFIG_FILE", default=str(PROJECT_DIR / "alembic.ini"))
|
||||
if not os.path.isfile(alembic_cfg_path):
|
||||
raise Exception("Provided alembic config path doesn't exist")
|
||||
|
||||
alembic_cfg = Config(alembic_cfg_path)
|
||||
if db_is_at_head(alembic_cfg):
|
||||
logger.debug("Migration not needed.")
|
||||
else:
|
||||
logger.info("Migration needed. Performing migration...")
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
|
||||
if session.get_bind().name == "postgresql": # needed for fuzzy search and fast GIN text indices
|
||||
session.execute(text("CREATE EXTENSION IF NOT EXISTS pg_trgm;"))
|
||||
|
||||
users = RepositoryUsers()
|
||||
if users.get_all():
|
||||
logger.info("Database already seeded.")
|
||||
else:
|
||||
logger.info("Seeding database...")
|
||||
init_db(session)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1
backend/db/models/__init__.py
Normal file
1
backend/db/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .users import *
|
20
backend/db/models/_model_base.py
Normal file
20
backend/db/models/_model_base.py
Normal file
@ -0,0 +1,20 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy.orm import declarative_base, Mapped, mapped_column
|
||||
from text_unidecode import unidecode
|
||||
|
||||
from backend.db.db_setup import SessionLocal
|
||||
|
||||
Model = declarative_base()
|
||||
Model.query = SessionLocal.query_property()
|
||||
|
||||
class SqlAlchemyBase(Model):
|
||||
__abstract__ = True
|
||||
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.utcnow(), index=True)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.utcnow(), onupdate=datetime.utcnow())
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, val: str) -> str:
|
||||
return unidecode(val).lower().strip()
|
1
backend/db/models/users/__init__.py
Normal file
1
backend/db/models/users/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .users import *
|
20
backend/db/models/users/users.py
Normal file
20
backend/db/models/users/users.py
Normal file
@ -0,0 +1,20 @@
|
||||
from uuid import uuid4
|
||||
from sqlalchemy import Boolean, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from .._model_base import SqlAlchemyBase
|
||||
|
||||
class User(SqlAlchemyBase):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid4, index=True)
|
||||
username: Mapped[str | None] = mapped_column(String, unique=True, index=True, nullable=False)
|
||||
password: Mapped[str | None] = mapped_column(String, index=True, nullable=False)
|
||||
name: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
||||
is_admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
is_lock: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}, name: {self.name}, username: {self.username}"
|
10
backend/main.py
Normal file
10
backend/main.py
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
import uvicorn
|
||||
from backend.app import settings
|
||||
|
||||
|
||||
def main():
|
||||
uvicorn.run("app:app", host=settings.API_HOST, port=settings.API_PORT, reload=True, workers=1, forwarded_allow_ips=settings.HOST_IP)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1
backend/repos/__init__.py
Normal file
1
backend/repos/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .repository_users import *
|
36
backend/repos/repository_users.py
Normal file
36
backend/repos/repository_users.py
Normal file
@ -0,0 +1,36 @@
|
||||
from backend.core.config import get_app_settings
|
||||
from backend.core.security.security import hash_password
|
||||
from backend.db.models import User
|
||||
from backend.schemas import UserCreate
|
||||
from sqlalchemy.orm import Session
|
||||
from uuid import UUID
|
||||
|
||||
from backend.schemas.user.user import UserSeeds
|
||||
|
||||
settings = get_app_settings()
|
||||
|
||||
class RepositoryUsers:
|
||||
def __init__(self):
|
||||
self.user = User()
|
||||
|
||||
def get_all(self, skip: int = 0, limit: int = 100):
|
||||
return self.user.query.offset(skip).limit(limit).all()
|
||||
|
||||
def get_by_username(self, username: str):
|
||||
return self.user.query.filter_by(username=username).first()
|
||||
|
||||
def get_by_id(self, user_id: str):
|
||||
return self.user.query.filter_by(id=UUID(user_id)).first()
|
||||
|
||||
def create(self, db: Session, user: UserCreate | UserSeeds):
|
||||
try:
|
||||
password = getattr(user, "password")
|
||||
db_user = User(**user.dict(exclude={"password"}), password=hash_password(password))
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
db.refresh(db_user)
|
||||
return db_user
|
1
backend/repos/seeder/__init__.py
Normal file
1
backend/repos/seeder/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .init_users import default_users_init
|
33
backend/repos/seeder/init_users.py
Normal file
33
backend/repos/seeder/init_users.py
Normal file
@ -0,0 +1,33 @@
|
||||
from backend.core.config import get_app_settings
|
||||
from backend.core.root_logger import get_logger
|
||||
from backend.repos.repository_users import RepositoryUsers
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.schemas.user import UserSeeds
|
||||
|
||||
|
||||
logger = get_logger("init_users")
|
||||
settings = get_app_settings()
|
||||
|
||||
def dev_users() -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"username": "sam",
|
||||
"password": "admin",
|
||||
"name": "Sam",
|
||||
"is_admin": True,
|
||||
"is_lock": False,
|
||||
},
|
||||
{
|
||||
"username": "sam1",
|
||||
"password": "admin",
|
||||
"name": "Sam1",
|
||||
"is_admin": False,
|
||||
"is_lock": False,
|
||||
},
|
||||
]
|
||||
|
||||
def default_users_init(session: Session):
|
||||
users = RepositoryUsers()
|
||||
for user in dev_users():
|
||||
users.create(session, UserSeeds(**user))
|
9
backend/routes/__init__.py
Normal file
9
backend/routes/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
from . import (auth, user)
|
||||
|
||||
router = APIRouter(prefix='/api')
|
||||
|
||||
router.include_router(auth.router)
|
||||
router.include_router(user.router)
|
9
backend/routes/_base/routers.py
Normal file
9
backend/routes/_base/routers.py
Normal file
@ -0,0 +1,9 @@
|
||||
from enum import Enum
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from backend.core.dependencies import get_auth_user
|
||||
|
||||
|
||||
class PrivateAPIRouter(APIRouter):
|
||||
def __init__(self, tags: list[str | Enum] | None = None, prefix: str = "", **kwargs):
|
||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_auth_user)], **kwargs)
|
7
backend/routes/auth/__init__.py
Normal file
7
backend/routes/auth/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
from fastapi import APIRouter
|
||||
from . import auth
|
||||
|
||||
router = APIRouter(prefix='/auth')
|
||||
|
||||
router.include_router(auth.auth_router)
|
70
backend/routes/auth/auth.py
Normal file
70
backend/routes/auth/auth.py
Normal file
@ -0,0 +1,70 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
|
||||
# from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from backend.core.config import get_app_settings
|
||||
from backend.core.dependencies.dependencies import get_current_user
|
||||
from backend.core import MessageCode
|
||||
from backend.db.db_setup import generate_session
|
||||
from backend.schemas import ReturnValue, UserRequest, LoginResponse, UserCreate, PrivateUser
|
||||
from backend.services.user import UserService
|
||||
|
||||
|
||||
auth_router = APIRouter(tags=["Users: Authentication"])
|
||||
user_service = UserService()
|
||||
settings = get_app_settings()
|
||||
|
||||
db_dependency = Annotated[Session, Depends(generate_session)]
|
||||
current_user_token = Annotated[PrivateUser, Depends(get_current_user)]
|
||||
|
||||
@auth_router.post('/token')
|
||||
async def get_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: db_dependency):
|
||||
user = user_service.check_exist(user=UserRequest(username=form_data.username, password=form_data.password))
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=MessageCode.WRONG_INPUT)
|
||||
token = user_service.get_access_token(user_id=user.id)
|
||||
return {'access_token': token, 'token_type': 'bearer'}
|
||||
|
||||
|
||||
@auth_router.put('/register')
|
||||
def register_user(user: UserCreate, db: db_dependency) -> ReturnValue[Any]:
|
||||
db_user = user_service.get_by_username(username=user.username)
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail=MessageCode.CREATED_USER)
|
||||
user_service.create(db=db, user=user)
|
||||
return ReturnValue(status=200, data="created")
|
||||
|
||||
@auth_router.post('/login', response_model=ReturnValue[LoginResponse])
|
||||
def user_login(user: UserRequest, response: Response) -> ReturnValue[Any]:
|
||||
db_user = user_service.check_exist(user=user)
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=MessageCode.WRONG_INPUT)
|
||||
if db_user.is_lock is True:
|
||||
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=MessageCode.ACCOUNT_LOCK)
|
||||
access_token, refresh_token = user_service.generate_token(user_id=db_user.id)
|
||||
duration_access = datetime.now(timezone.utc) + timedelta(minutes=settings.EXP_TOKEN)
|
||||
duration_refresh = int(timedelta(days=settings.EXP_REFRESH).total_seconds())
|
||||
response.set_cookie(
|
||||
key=settings.COOKIE_KEY,
|
||||
value=refresh_token,
|
||||
max_age=duration_refresh,
|
||||
expires=duration_refresh,
|
||||
httponly=True,
|
||||
samesite="strict",
|
||||
)
|
||||
return ReturnValue(status=200, data=dict(access_token=access_token, exp=int(duration_access.timestamp()), name=db_user.name))
|
||||
|
||||
@auth_router.get('/refresh')
|
||||
def user_check(current_user: current_user_token) -> ReturnValue[Any]:
|
||||
access_token = user_service.get_access_token(user_id=current_user.id)
|
||||
duration_access = datetime.now(timezone.utc) + timedelta(minutes=settings.EXP_TOKEN)
|
||||
return ReturnValue(status=200, data=dict(accessToken=access_token, exp=int(duration_access.timestamp())))
|
||||
|
||||
@auth_router.get('/logout')
|
||||
def user_logout(response: Response, current_user: current_user_token) -> ReturnValue[Any]:
|
||||
if current_user:
|
||||
response.delete_cookie(key=settings.COOKIE_KEY)
|
||||
return ReturnValue(status=200, data='Logged out')
|
7
backend/routes/user/__init__.py
Normal file
7
backend/routes/user/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
from fastapi import APIRouter
|
||||
from . import user
|
||||
|
||||
router = APIRouter(prefix='/user')
|
||||
|
||||
router.include_router(user.public_router)
|
21
backend/routes/user/user.py
Normal file
21
backend/routes/user/user.py
Normal file
@ -0,0 +1,21 @@
|
||||
from typing import Annotated, Any
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from backend.core.config import get_app_settings
|
||||
from backend.core.dependencies import is_logged_in
|
||||
from backend.db.db_setup import generate_session
|
||||
from backend.schemas.common import ReturnValue
|
||||
from backend.schemas.user import ProfileResponse
|
||||
from backend.services.user import UserService
|
||||
|
||||
|
||||
public_router = APIRouter(tags=["Users: Info"])
|
||||
user_service = UserService()
|
||||
settings = get_app_settings()
|
||||
|
||||
db_dependency = Annotated[Session, Depends(generate_session)]
|
||||
current_user_token = Annotated[ProfileResponse, Depends(is_logged_in)]
|
||||
|
||||
@public_router.get("/me", response_model=ReturnValue[ProfileResponse])
|
||||
def get_user(current_user: current_user_token) -> ReturnValue[Any]:
|
||||
return ReturnValue(status=200, data=current_user)
|
2
backend/schemas/__init__.py
Normal file
2
backend/schemas/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .common import *
|
||||
from .user import *
|
8
backend/schemas/common.py
Normal file
8
backend/schemas/common.py
Normal file
@ -0,0 +1,8 @@
|
||||
from typing import Generic, TypeVar
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class ReturnValue(BaseModel, Generic[T]):
|
||||
status: int
|
||||
data: T
|
27
backend/schemas/fuware_model.py
Normal file
27
backend/schemas/fuware_model.py
Normal file
@ -0,0 +1,27 @@
|
||||
from typing import ClassVar, TypeVar
|
||||
from humps import camelize
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
class SearchType(Enum):
|
||||
fuzzy = "fuzzy"
|
||||
tokenized = "tokenized"
|
||||
|
||||
class FuwareModel(BaseModel):
|
||||
_searchable_properties: ClassVar[list[str]] = []
|
||||
"""
|
||||
Searchable properties for the search API.
|
||||
The first property will be used for sorting (order_by)
|
||||
"""
|
||||
model_config = ConfigDict(alias_generator=camelize, populate_by_name=True)
|
||||
|
||||
def cast(self, cls: type[T], **kwargs) -> T:
|
||||
"""
|
||||
Cast the current model to another with additional arguments. Useful for
|
||||
transforming DTOs into models that are saved to a database
|
||||
"""
|
||||
create_data = {field: getattr(self, field) for field in self.__fields__ if field in cls.__fields__}
|
||||
create_data.update(kwargs or {})
|
||||
return cls(**create_data)
|
1
backend/schemas/user/__init__.py
Normal file
1
backend/schemas/user/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .user import *
|
40
backend/schemas/user/user.py
Normal file
40
backend/schemas/user/user.py
Normal file
@ -0,0 +1,40 @@
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from pydantic import ConfigDict
|
||||
from fastapi import Form
|
||||
|
||||
from backend.schemas.fuware_model import FuwareModel
|
||||
|
||||
class UserBase(FuwareModel):
|
||||
username: str = Form(...)
|
||||
|
||||
class UserRequest(UserBase):
|
||||
password: str = Form(...)
|
||||
|
||||
class UserCreate(UserRequest):
|
||||
name: str
|
||||
|
||||
class UserSeeds(UserCreate):
|
||||
is_admin: bool
|
||||
is_lock: bool
|
||||
|
||||
class PrivateUser(UserBase):
|
||||
id: UUID
|
||||
name: str
|
||||
is_admin: bool
|
||||
is_lock: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class ProfileResponse(UserBase):
|
||||
name: str
|
||||
is_admin: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class LoginResponse(FuwareModel):
|
||||
access_token: str
|
||||
exp: int
|
||||
name: str
|
0
backend/services/__init__.py
Normal file
0
backend/services/__init__.py
Normal file
6
backend/services/_base_service/__init__.py
Normal file
6
backend/services/_base_service/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from backend.core.config import get_app_settings
|
||||
|
||||
|
||||
class BaseService:
|
||||
def __init__(self) -> None:
|
||||
self.setting = get_app_settings()
|
1
backend/services/user/__init__.py
Normal file
1
backend/services/user/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .user_service import *
|
39
backend/services/user/user_service.py
Normal file
39
backend/services/user/user_service.py
Normal file
@ -0,0 +1,39 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from backend.core.security.hasher import get_hasher
|
||||
from backend.core.security import create_access_token
|
||||
from backend.core.security.security import create_refresh_token
|
||||
from backend.repos import RepositoryUsers
|
||||
from backend.schemas import UserRequest, UserCreate
|
||||
from backend.services._base_service import BaseService
|
||||
|
||||
class UserService(BaseService):
|
||||
def __init__(self):
|
||||
self.repos = RepositoryUsers()
|
||||
|
||||
def get_all(self, skip: int = 0, limit: int = 100):
|
||||
return self.repos.get_all(skip=skip, limit=limit)
|
||||
|
||||
def get_by_username(self, username: str):
|
||||
return self.repos.get_by_username(username)
|
||||
|
||||
def get_by_id(self, user_id: str):
|
||||
return self.repos.get_by_id(user_id)
|
||||
|
||||
def create(self, db: Session, user: UserCreate):
|
||||
return self.repos.create(db=db, user=user)
|
||||
|
||||
def check_exist(self, user: UserRequest):
|
||||
db_user = self.get_by_username(username=user.username)
|
||||
if not db_user:
|
||||
return False
|
||||
if not get_hasher().verify(password=user.password, hashed=db_user.password):
|
||||
return False
|
||||
return db_user
|
||||
|
||||
def generate_token(self, user_id: str):
|
||||
access_token = create_access_token(data={"sub": str(user_id)})
|
||||
refresh_token = create_refresh_token(data={"sub": str(user_id)})
|
||||
return access_token, refresh_token
|
||||
|
||||
def get_access_token(self, user_id: str):
|
||||
return create_access_token(data={"sub": str(user_id)})
|
Reference in New Issue
Block a user