diff --git a/.env.example b/.env.example index bc721a1..b317169 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ PRODUCTION=false +TESTING=false diff --git a/.gitignore b/.gitignore index 1a145b2..fc6a7f0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules node_modules/ .sqlite +*.db diff --git a/Taskfile.yaml b/Taskfile.yaml index 37a952b..8832313 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -12,7 +12,11 @@ dotenv: - .env - .dev.env tasks: - py: + py:setupdb: + desc: runs it first for init db + cmds: + - poetry run python fuware/db/init_db.py + py:dev: desc: runs the backend server cmds: - - poetry run python fuware/main.py + - poetry run python fuware/app.py diff --git a/dev/data/.gitkeep b/dev/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/fuware/app.py b/fuware/app.py new file mode 100644 index 0000000..6f1c60f --- /dev/null +++ b/fuware/app.py @@ -0,0 +1,60 @@ +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware + +from fuware.core.config import get_app_settings +from fuware import __version__ +from fuware.routes import router +import uvicorn + +settings = get_app_settings() + +description = f""" +fuware is a web application for managing your hours items and tracking them. +""" + +# event.listen(models.User.__table__, 'after_create', initialize_table) + +app = FastAPI( + title="Fuware", + description=description, + version=__version__, + docs_url=settings.DOCS_URL, + redoc_url=settings.REDOC_URL +) + +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() + +# app.include_router(authR.authRouter) +# app.include_router(userR.userRouter) + +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() diff --git a/fuware/const.py b/fuware/const.py deleted file mode 100644 index e590d37..0000000 --- a/fuware/const.py +++ /dev/null @@ -1,15 +0,0 @@ -import os -from dotenv import load_dotenv - -load_dotenv() - -SERCET_KEY = b"oWNhXlfo666JlMHk6UHYxeNB6z_CA2MisDDZJe4N0yc=" -COOKIE_KEY = os.getenv('VITE_LOGIN_KEY') or '7fo24CMyIc' -# URL_DATABASE = "postgresql://{0}:{1}@{2}:{3}/{4}".format( -# os.getenv('LOL_DB_USER'), -# os.getenv('LOL_DB_PASSWORD'), -# os.getenv('LOL_DB_HOST'), -# os.getenv('LOL_DB_PORT'), -# os.getenv('LOL_DB_NAME'), -# ) -URL_DATABASE = "sqlite:///./test.db" diff --git a/fuware/core/config.py b/fuware/core/config.py index cf6e3a7..0d5ebe9 100644 --- a/fuware/core/config.py +++ b/fuware/core/config.py @@ -4,8 +4,7 @@ from pathlib import Path from dotenv import load_dotenv - -from .settings import AppDirectories, AppSettings +from fuware.core.settings.settings import AppSettings, app_settings_constructor CWD = Path(__file__).parent BASE_DIR = CWD.parent.parent @@ -27,14 +26,6 @@ def determine_data_dir() -> Path: return BASE_DIR.joinpath("dev", "data") -@lru_cache -def get_app_dirs() -> AppDirectories: - return AppDirectories(determine_data_dir()) - - @lru_cache def get_app_settings() -> AppSettings: return app_settings_constructor(env_file=ENV, production=PRODUCTION, data_dir=determine_data_dir()) - - -print(get_app_settings()) diff --git a/fuware/core/dependencies/__init__.py b/fuware/core/dependencies/__init__.py new file mode 100644 index 0000000..c9753bd --- /dev/null +++ b/fuware/core/dependencies/__init__.py @@ -0,0 +1 @@ +from .dependencies import * diff --git a/fuware/core/dependencies/dependencies.py b/fuware/core/dependencies/dependencies.py new file mode 100644 index 0000000..cf579b9 --- /dev/null +++ b/fuware/core/dependencies/dependencies.py @@ -0,0 +1,20 @@ + +from fastapi import Depends, HTTPException, Request +from sqlalchemy.orm import Session +from fuware.core.config import get_app_settings +from fuware.db.db_setup import generate_session + +settings = get_app_settings() + +async def get_auth_user(request: Request, db: Session = Depends(generate_session)): + """verify that user has a valid session""" + session_id = request.cookies.get(settings.COOKIE_KEY) + if not session_id: + raise HTTPException(status_code=401, detail="Unauthorized") + # decrypt_user = decryptString(session_id).split(',') + # db_user = get_user_by_username(db, decrypt_user[0]) + # if not db_user: + # raise HTTPException(status_code=403) + # if not verify_password(decrypt_user[1], db_user.password): + # raise HTTPException(status_code=401, detail="Your username or password input is wrong!") + return True diff --git a/fuware/core/logger/config.py b/fuware/core/logger/config.py new file mode 100644 index 0000000..65598e2 --- /dev/null +++ b/fuware/core/logger/config.py @@ -0,0 +1,67 @@ +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() diff --git a/fuware/core/security/__init__.py b/fuware/core/security/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fuware/core/security/hasher.py b/fuware/core/security/hasher.py new file mode 100644 index 0000000..f8044fa --- /dev/null +++ b/fuware/core/security/hasher.py @@ -0,0 +1,34 @@ +from functools import lru_cache +from typing import Protocol +import bcrypt + +from fuware.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() diff --git a/fuware/core/settings/db_providers.py b/fuware/core/settings/db_providers.py index dcae60f..95e3dd7 100644 --- a/fuware/core/settings/db_providers.py +++ b/fuware/core/settings/db_providers.py @@ -17,7 +17,7 @@ class SQLiteProvider(AbstractDBProvider, BaseModel): @property def db_path(self): - return self.data_dir / f"{self.prefix}mealie.db" + return self.data_dir / f"{self.prefix}fuware.db" @property def db_url(self) -> str: diff --git a/fuware/core/settings/settings.py b/fuware/core/settings/settings.py index 2e4adaa..f42e89b 100644 --- a/fuware/core/settings/settings.py +++ b/fuware/core/settings/settings.py @@ -1,26 +1,23 @@ -import secrets from pathlib import Path from fuware.core.settings.db_providers import AbstractDBProvider, SQLiteProvider from pydantic_settings import BaseSettings # type: ignore -def determine_secrets(data_dir: Path, production: bool) -> str: +def determine_secrets(production: bool) -> str: if not production: return "shh-secret-test-key" - secrets_file = data_dir.joinpath(".secret") - if secrets_file.is_file(): - with open(secrets_file) as f: - return f.read() - else: - data_dir.mkdir(parents=True, exist_ok=True) - with open(secrets_file, "w") as f: - new_secret = secrets.token_hex(32) - f.write(new_secret) - return new_secret + return "oWNhXlfo666JlMHk6UHYxeNB6z_CA2MisDDZJe4N0yc=" + +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`)""" @@ -32,6 +29,9 @@ class AppSettings(BaseSettings): ALLOW_SIGNUP: bool = False + SECRET: str + COOKIE_KEY: str + @property def DOCS_URL(self) -> str | None: return "/docs" if self.API_DOCS else None @@ -43,7 +43,6 @@ class AppSettings(BaseSettings): # =============================================== # Database Configuration - DB_ENGINE: str = "sqlite" # Options: 'sqlite', 'postgres' DB_PROVIDER: AbstractDBProvider | None = None @property @@ -63,7 +62,7 @@ def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, e app_settings = AppSettings( _env_file=env_file, # type: ignore _env_file_encoding=env_encoding, # type: ignore - **{"SECRET": determine_secrets(data_dir, production)}, + **{"SECRET": determine_secrets(production), 'COOKIE_KEY': determine_cookie(production)}, ) app_settings.DB_PROVIDER = SQLiteProvider(data_dir=data_dir) diff --git a/fuware/core/settings/static.py b/fuware/core/settings/static.py deleted file mode 100644 index 40a3144..0000000 --- a/fuware/core/settings/static.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -from dotenv import load_dotenv - -from pathlib import Path -from fuware import __version__ - -load_dotenv() - -APP_VERSION = __version__ -CWD = Path(__file__).parent -BASE_DIR = CWD.parent.parent.parent - -SERCET_KEY = b"oWNhXlfo666JlMHk6UHYxeNB6z_CA2MisDDZJe4N0yc=" -COOKIE_KEY = os.getenv('VITE_LOGIN_KEY') or '7fo24CMyIc' -# URL_DATABASE = "postgresql://{0}:{1}@{2}:{3}/{4}".format( -# os.getenv('LOL_DB_USER'), -# os.getenv('LOL_DB_PASSWORD'), -# os.getenv('LOL_DB_HOST'), -# os.getenv('LOL_DB_PORT'), -# os.getenv('LOL_DB_NAME'), -# ) -URL_DATABASE = "sqlite:///./test.db" diff --git a/fuware/db/controller/user.py b/fuware/db/controller/user.py deleted file mode 100644 index 737fc64..0000000 --- a/fuware/db/controller/user.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy.orm import Session -from db.models import User -from ultis import get_password_hash -import schemas - -def get_user(db: Session, user_id: str): - return db.query(User).filter(User.id == user_id).first() - -def get_user_by_username(db: Session, usn: str): - return db.query(User).filter(User.username == usn).first() - -def get_users(db: Session, skip: int = 0, limit: int = 100): - return db.query(User).offset(skip).limit(limit).all() - -def create_user(db: Session, user: schemas.UserCreate): - db_user = User(username=user.username, password=get_password_hash(user.password), name=user.name) - db.add(db_user) - db.commit() - db.refresh(db_user) - return db_user diff --git a/fuware/db/db_setup.py b/fuware/db/db_setup.py index d5dae9a..a51e60e 100644 --- a/fuware/db/db_setup.py +++ b/fuware/db/db_setup.py @@ -1,17 +1,37 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from sqlalchemy.ext.declarative import declarative_base -from const import URL_DATABASE +from collections.abc import Generator +from sqlalchemy.orm.session import Session +from sqlalchemy import create_engine, event, Engine, text +from sqlalchemy.orm import scoped_session, sessionmaker -engine = create_engine(URL_DATABASE) +from fuware.core.config import get_app_settings -SessionLocal = sessionmaker(autocommit=False ,autoflush=False, bind=engine) +settings = get_app_settings() -Base = declarative_base() +def sql_global_init(db_url: str): + connect_args = {"check_same_thread": False} -def get_db(): + engine = create_engine(db_url, echo=True, 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) # type: ignore + +@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() + +# with engine.connect() as connection: +# result = connection.execute(text('select "Hello"')) + +# print(result.all()) + +def generate_session() -> Generator[Session, None, None]: db = SessionLocal() try: - yield db + yield db finally: - db.close() + db.close() diff --git a/fuware/db/init_db.py b/fuware/db/init_db.py new file mode 100644 index 0000000..edd9484 --- /dev/null +++ b/fuware/db/init_db.py @@ -0,0 +1,5 @@ +from db_setup import engine +from models._model_base import Model +from models.users import * + +Model.metadata.create_all(bind=engine) diff --git a/fuware/db/models/__init__.py b/fuware/db/models/__init__.py index f4a2da0..9917a30 100644 --- a/fuware/db/models/__init__.py +++ b/fuware/db/models/__init__.py @@ -1 +1 @@ -from .user import * +from .users import * diff --git a/fuware/db/models/_model_base.py b/fuware/db/models/_model_base.py new file mode 100644 index 0000000..4b6288a --- /dev/null +++ b/fuware/db/models/_model_base.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from sqlalchemy import DateTime, Integer +from sqlalchemy.orm import declarative_base, Mapped, mapped_column +from text_unidecode import unidecode + +from fuware.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) + update_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() diff --git a/fuware/db/models/mixins.py b/fuware/db/models/mixins.py deleted file mode 100644 index 5b39ff3..0000000 --- a/fuware/db/models/mixins.py +++ /dev/null @@ -1,8 +0,0 @@ -from datetime import datetime -from sqlalchemy import Column, DateTime -from sqlalchemy.orm import declarative_mixin - -@declarative_mixin -class Timestamp: - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - updated_at = Column(DateTime, default=datetime.utcnow, nullable=False) diff --git a/fuware/db/models/user.py b/fuware/db/models/user.py deleted file mode 100644 index aaef777..0000000 --- a/fuware/db/models/user.py +++ /dev/null @@ -1,15 +0,0 @@ -from db import Base -from sqlalchemy import Boolean, Column, String -from .mixins import Timestamp -from sqlalchemy.dialects.postgresql import UUID -import uuid - -class User(Base, Timestamp): - __tablename__ = 'users' - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) - username = Column(String(100), unique=True, index=True, nullable=False) - password = Column(String, index=True, nullable=False) - name = Column(String, index=True, nullable=True) - is_admin = Column(Boolean, default=False) - is_lock = Column(Boolean, default=False) diff --git a/fuware/db/models/users/__init__.py b/fuware/db/models/users/__init__.py new file mode 100644 index 0000000..9917a30 --- /dev/null +++ b/fuware/db/models/users/__init__.py @@ -0,0 +1 @@ +from .users import * diff --git a/fuware/db/models/users/users.py b/fuware/db/models/users/users.py new file mode 100644 index 0000000..c0c7228 --- /dev/null +++ b/fuware/db/models/users/users.py @@ -0,0 +1,20 @@ +import uuid +from sqlalchemy import Boolean, Column, String +from sqlalchemy.orm import Mapped, mapped_column + +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=uuid.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}" diff --git a/fuware/main.py b/fuware/main.py index 6732504..bf762c7 100644 --- a/fuware/main.py +++ b/fuware/main.py @@ -1,34 +1,10 @@ -from fastapi import FastAPI, Request, HTTPException -from fastapi.responses import JSONResponse -from routes import authR, userR -# from db import engine, models -# from sqlalchemy import event -# from db.seeds import initialize_table + import uvicorn +from fuware.app import settings -# event.listen(models.User.__table__, 'after_create', initialize_table) - -app = FastAPI() - -# models.Base.metadata.create_all(bind=engine) - -@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}, - ) - -app.include_router(authR.authRouter) -app.include_router(userR.userRouter) def main(): - uvicorn.run( - "main:app", - port=8000, - host="0.0.0.0", - reload=True - ) + 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__': +if __name__ == "__main__": main() diff --git a/fuware/repos/__init__.py b/fuware/repos/__init__.py new file mode 100644 index 0000000..bdb81bd --- /dev/null +++ b/fuware/repos/__init__.py @@ -0,0 +1 @@ +from .repository_users import * diff --git a/fuware/repos/repository_users.py b/fuware/repos/repository_users.py new file mode 100644 index 0000000..ae9d773 --- /dev/null +++ b/fuware/repos/repository_users.py @@ -0,0 +1,28 @@ +from fuware.core.security.hasher import get_hasher +from fuware.db.models.users.users import User +from fuware.schemas import UserCreate +from sqlalchemy.orm import Session + + +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 create(self, db: Session, user: UserCreate): + try: + hasher = get_hasher() + db_user = User(username=user.username, password=hasher.hash(user.password), name=user.name) + db.add(db_user) + db.commit() + except Exception: + db.rollback() + raise + + db.refresh(db_user) + return db_user diff --git a/fuware/routes/__init__.py b/fuware/routes/__init__.py new file mode 100644 index 0000000..5ca2a2d --- /dev/null +++ b/fuware/routes/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +from . import (auth) + +router = APIRouter(prefix='/api') + +router.include_router(auth.router) diff --git a/fuware/routes/_base/routers.py b/fuware/routes/_base/routers.py new file mode 100644 index 0000000..9324abb --- /dev/null +++ b/fuware/routes/_base/routers.py @@ -0,0 +1,9 @@ +from enum import Enum +from fastapi import APIRouter, Depends + +from fuware.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) diff --git a/fuware/routes/auth/__init__.py b/fuware/routes/auth/__init__.py new file mode 100644 index 0000000..e89c859 --- /dev/null +++ b/fuware/routes/auth/__init__.py @@ -0,0 +1,7 @@ + +from fastapi import APIRouter +from . import auth + +router = APIRouter(prefix='/auth') + +router.include_router(auth.public_router) diff --git a/fuware/routes/auth/auth.py b/fuware/routes/auth/auth.py new file mode 100644 index 0000000..0ee809a --- /dev/null +++ b/fuware/routes/auth/auth.py @@ -0,0 +1,35 @@ +from typing import Any +from fastapi import APIRouter, Depends, HTTPException, Response + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session +from fuware.db.db_setup import generate_session +from fuware.schemas import ReturnValue, PrivateUser, UserRequest +from fuware.schemas.user.user import UserCreate +from fuware.services import UserService + + +public_router = APIRouter(tags=["Users: Authentication"]) + +user_service = UserService() + +@public_router.put('/register') +def register_user(user: UserCreate, db: Session = Depends(generate_session)) -> ReturnValue[Any]: + db_user = user_service.get_by_username(username=user.username) + if db_user: + raise HTTPException(status_code=400, detail="Username already registered!") + user_return = user_service.create(db=db, user=user) + return ReturnValue(status=200, data=jsonable_encoder(user_return)) + +# @public_router.post('/login', response_model=ReturnValue[PrivateUser]) +# def user_login(user: UserRequest, response: Response) -> ReturnValue[Any]: +# db_user = UserService.get_by_username(user.username) +# if not db_user: +# raise HTTPException(status_code=401, detail="Your username or password input is wrong!") + # if not verify_password(user.password, db_user.password): + # raise HTTPException(status_code=401, detail="Your username or password input is wrong!") + # if db_user.is_lock is True: + # raise HTTPException(status_code=401, detail="Your Account is banned") + # cookieEncode = encryptString(user.username + ',' + user.password) + # response.set_cookie(key=COOKIE_KEY, value=cookieEncode.decode('utf-8')) + # return ReturnValue(status=200, data=jsonable_encoder(db_user)) diff --git a/fuware/routes/authR.py b/fuware/routes/authR.py deleted file mode 100644 index 47f192c..0000000 --- a/fuware/routes/authR.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import Any -from fastapi import APIRouter, HTTPException, Response, Request, Depends -from fastapi.encoders import jsonable_encoder -from schemas import ReturnValue, User, UserCreate, UserRequest -from ultis import root_api_path_build, encryptString, decryptString, verify_password -from const import COOKIE_KEY -from sqlalchemy.orm import Session -from db.controller import get_user_by_username, create_user -from db import get_db - -authRouter=APIRouter(prefix=root_api_path_build('/auth')) - -@authRouter.put('/register') -def register_user(user: UserCreate, db: Session = Depends(get_db)) -> ReturnValue[Any]: - db_user = get_user_by_username(db=db, usn=user.username) - if db_user: - raise HTTPException(status_code=400, detail="Username already registered!") - user_return = create_user(db=db, user=user) - return ReturnValue(status=200, data=jsonable_encoder(user_return)) - -@authRouter.post('/login', response_model=ReturnValue[User]) -def user_login(user: UserRequest, response: Response, db: Session = Depends(get_db)) -> ReturnValue[Any]: - db_user = get_user_by_username(db, user.username) - if not db_user: - raise HTTPException(status_code=401, detail="Your username or password input is wrong!") - if not verify_password(user.password, db_user.password): - raise HTTPException(status_code=401, detail="Your username or password input is wrong!") - if db_user.is_lock is True: - raise HTTPException(status_code=401, detail="Your Account is banned") - cookieEncode = encryptString(user.username + ',' + user.password) - response.set_cookie(key=COOKIE_KEY, value=cookieEncode.decode('utf-8')) - return ReturnValue(status=200, data=jsonable_encoder(db_user)) - -@authRouter.get('/logout') -def user_logout(response: Response) -> ReturnValue[Any]: - response.delete_cookie(key=COOKIE_KEY) - return ReturnValue(status=200, data='Logged out') - -def get_auth_user(request: Request, db: Session = Depends(get_db)): - """verify that user has a valid session""" - session_id = request.cookies.get(COOKIE_KEY) - if not session_id: - raise HTTPException(status_code=401, detail="Unauthorized") - decrypt_user = decryptString(session_id).split(',') - db_user = get_user_by_username(db, decrypt_user[0]) - if not db_user: - raise HTTPException(status_code=403) - if not verify_password(decrypt_user[1], db_user.password): - raise HTTPException(status_code=401, detail="Your username or password input is wrong!") - return True diff --git a/fuware/routes/userR.py b/fuware/routes/userR.py deleted file mode 100644 index b9f31d1..0000000 --- a/fuware/routes/userR.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Any -from fastapi import APIRouter, Depends -from schemas import ReturnValue -from ultis import root_api_path_build -from routes import authR - -userRouter=APIRouter(prefix=root_api_path_build('/user')) - -@userRouter.get('/get-data/', dependencies=[Depends(authR.get_auth_user)]) -def get_data(url: str = '') -> ReturnValue[Any]: - return ReturnValue(status=200, data=url) diff --git a/fuware/schemas/fuware_model.py b/fuware/schemas/fuware_model.py new file mode 100644 index 0000000..2fc6fcd --- /dev/null +++ b/fuware/schemas/fuware_model.py @@ -0,0 +1,27 @@ +from typing import ClassVar, Protocol, TypeVar +from humps import camelize +from enum import Enum +from pydantic import UUID4, 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) diff --git a/fuware/db/controller/__init__.py b/fuware/schemas/user/__init__.py similarity index 100% rename from fuware/db/controller/__init__.py rename to fuware/schemas/user/__init__.py diff --git a/fuware/schemas/user.py b/fuware/schemas/user/user.py similarity index 60% rename from fuware/schemas/user.py rename to fuware/schemas/user/user.py index 7f41620..a887804 100644 --- a/fuware/schemas/user.py +++ b/fuware/schemas/user/user.py @@ -1,8 +1,10 @@ from datetime import datetime -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from fastapi import Form -class UserBase(BaseModel): +from fuware.schemas.fuware_model import FuwareModel + +class UserBase(FuwareModel): username: str = Form(...) class UserRequest(UserBase): @@ -12,13 +14,11 @@ class UserCreate(UserRequest): password: str = Form(...) name: str -class User(UserBase): +class PrivateUser(UserBase): id: str name: str is_admin: bool is_lock: bool created_at: datetime updated_at: datetime - - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/fuware/services/__init__.py b/fuware/services/__init__.py new file mode 100644 index 0000000..f4a2da0 --- /dev/null +++ b/fuware/services/__init__.py @@ -0,0 +1 @@ +from .user import * diff --git a/fuware/services/_base_service/__init__.py b/fuware/services/_base_service/__init__.py new file mode 100644 index 0000000..9334f71 --- /dev/null +++ b/fuware/services/_base_service/__init__.py @@ -0,0 +1,6 @@ +from fuware.core.config import get_app_settings + + +class BaseService: + def __init__(self) -> None: + self.setting = get_app_settings() diff --git a/fuware/services/user/__init__.py b/fuware/services/user/__init__.py new file mode 100644 index 0000000..dda534e --- /dev/null +++ b/fuware/services/user/__init__.py @@ -0,0 +1 @@ +from .user_service import * diff --git a/fuware/services/user/user_service.py b/fuware/services/user/user_service.py new file mode 100644 index 0000000..323f724 --- /dev/null +++ b/fuware/services/user/user_service.py @@ -0,0 +1,19 @@ + +from sqlalchemy.orm import Session +from fuware.repos import RepositoryUsers +from fuware.services._base_service import BaseService +from fuware.schemas import UserCreate + + +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 create(self, db: Session, user: UserCreate): + return self.repos.create(db=db, user=user) diff --git a/poetry.lock b/poetry.lock index 003240e..7dba2ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -33,6 +33,46 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "bcrypt" +version = "4.1.3" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:48429c83292b57bf4af6ab75809f8f4daf52aa5d480632e53707805cc1ce9b74"}, + {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a8bea4c152b91fd8319fef4c6a790da5c07840421c2b785084989bf8bbb7455"}, + {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d3b317050a9a711a5c7214bf04e28333cf528e0ed0ec9a4e55ba628d0f07c1a"}, + {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:094fd31e08c2b102a14880ee5b3d09913ecf334cd604af27e1013c76831f7b05"}, + {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4fb253d65da30d9269e0a6f4b0de32bd657a0208a6f4e43d3e645774fb5457f3"}, + {file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:193bb49eeeb9c1e2db9ba65d09dc6384edd5608d9d672b4125e9320af9153a15"}, + {file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8cbb119267068c2581ae38790e0d1fbae65d0725247a930fc9900c285d95725d"}, + {file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6cac78a8d42f9d120b3987f82252bdbeb7e6e900a5e1ba37f6be6fe4e3848286"}, + {file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01746eb2c4299dd0ae1670234bf77704f581dd72cc180f444bfe74eb80495b64"}, + {file = "bcrypt-4.1.3-cp37-abi3-win32.whl", hash = "sha256:037c5bf7c196a63dcce75545c8874610c600809d5d82c305dd327cd4969995bf"}, + {file = "bcrypt-4.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:8a893d192dfb7c8e883c4576813bf18bb9d59e2cfd88b68b725990f033f1b978"}, + {file = "bcrypt-4.1.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d4cf6ef1525f79255ef048b3489602868c47aea61f375377f0d00514fe4a78c"}, + {file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5698ce5292a4e4b9e5861f7e53b1d89242ad39d54c3da451a93cac17b61921a"}, + {file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec3c2e1ca3e5c4b9edb94290b356d082b721f3f50758bce7cce11d8a7c89ce84"}, + {file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3a5be252fef513363fe281bafc596c31b552cf81d04c5085bc5dac29670faa08"}, + {file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5f7cd3399fbc4ec290378b541b0cf3d4398e4737a65d0f938c7c0f9d5e686611"}, + {file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:c4c8d9b3e97209dd7111bf726e79f638ad9224b4691d1c7cfefa571a09b1b2d6"}, + {file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:31adb9cbb8737a581a843e13df22ffb7c84638342de3708a98d5c986770f2834"}, + {file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:551b320396e1d05e49cc18dd77d970accd52b322441628aca04801bbd1d52a73"}, + {file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6717543d2c110a155e6821ce5670c1f512f602eabb77dba95717ca76af79867d"}, + {file = "bcrypt-4.1.3-cp39-abi3-win32.whl", hash = "sha256:6004f5229b50f8493c49232b8e75726b568535fd300e5039e255d919fc3a07f2"}, + {file = "bcrypt-4.1.3-cp39-abi3-win_amd64.whl", hash = "sha256:2505b54afb074627111b5a8dc9b6ae69d0f01fea65c2fcaea403448c503d3991"}, + {file = "bcrypt-4.1.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:cb9c707c10bddaf9e5ba7cdb769f3e889e60b7d4fea22834b261f51ca2b89fed"}, + {file = "bcrypt-4.1.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9f8ea645eb94fb6e7bea0cf4ba121c07a3a182ac52876493870033141aa687bc"}, + {file = "bcrypt-4.1.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f44a97780677e7ac0ca393bd7982b19dbbd8d7228c1afe10b128fd9550eef5f1"}, + {file = "bcrypt-4.1.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d84702adb8f2798d813b17d8187d27076cca3cd52fe3686bb07a9083930ce650"}, + {file = "bcrypt-4.1.3.tar.gz", hash = "sha256:2ee15dd749f5952fe3f0430d0ff6b74082e159c50332a1413d51b5689cf06623"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "cffi" version = "1.16.0" @@ -488,6 +528,36 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-settings" +version = "2.2.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, +] + +[package.dependencies] +pydantic = ">=2.3.0" +python-dotenv = ">=0.21.0" + +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pyhumps" +version = "3.8.0" +description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node" +optional = false +python-versions = "*" +files = [ + {file = "pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6"}, + {file = "pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3"}, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -703,6 +773,17 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + [[package]] name = "typing-extensions" version = "4.11.0" @@ -955,4 +1036,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "539143b0355e37d8d3941ee1bbe1011faa541e29c88aebdc30b8ba813ab15d7f" +content-hash = "22414c8adf366f3f1121008853a3209f20781962a1eda18e4d8b8e4f551dc28a" diff --git a/pyproject.toml b/pyproject.toml index 239c87e..4b766dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,11 @@ sqlalchemy = "^2.0.29" cryptography = "^42.0.5" passlib = "^1.7.4" uvicorn = {version = "^0.29.0", extras = ["standard"]} +pydantic = "^2.7.1" +pydantic-settings = "^2.2.1" +text-unidecode = "^1.3" +pyhumps = "^3.8.0" +bcrypt = "^4.1.3" [tool.poetry.group.dev.dependencies] ruff = "^0.4.1"