diff --git a/api/kiwi_vpn_api/config.py b/api/kiwi_vpn_api/config.py index 4b2cf24..5c8672f 100644 --- a/api/kiwi_vpn_api/config.py +++ b/api/kiwi_vpn_api/config.py @@ -180,6 +180,13 @@ class CryptoConfig(BaseModel): schemes: list[str] = ["bcrypt"] + @property + def crypt_context_sync(self) -> CryptContext: + return CryptContext( + schemes=self.schemes, + deprecated="auto", + ) + @property async def crypt_context(self) -> CryptContext: return CryptContext( @@ -197,6 +204,19 @@ class Config(BaseModel): jwt: JWTConfig = Field(default_factory=JWTConfig) crypto: CryptoConfig = Field(default_factory=CryptoConfig) + @staticmethod + def load_sync() -> Config | None: + """ + Load configuration from config file + """ + + try: + with open(Settings.get().config_file, "r") as config_file: + return Config.parse_obj(json.load(config_file)) + + except FileNotFoundError: + return None + @staticmethod async def load() -> Config | None: """ diff --git a/api/kiwi_vpn_api/db_new/__init__.py b/api/kiwi_vpn_api/db_new/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/kiwi_vpn_api/db_new/connection.py b/api/kiwi_vpn_api/db_new/connection.py new file mode 100644 index 0000000..ff9341a --- /dev/null +++ b/api/kiwi_vpn_api/db_new/connection.py @@ -0,0 +1,30 @@ +from sqlmodel import Session, SQLModel, create_engine + + +class Connection: + """ + Namespace for the database connection. + """ + + engine = None + + @classmethod + def connect(cls, connection_url: str) -> None: + """ + Connect ORM to a database engine. + """ + + cls.engine = create_engine(connection_url) + SQLModel.metadata.create_all(cls.engine) + + @classmethod + @property + def session(cls) -> Session | None: + """ + Create an ORM session using a context manager. + """ + + if cls.engine is None: + return None + + return Session(cls.engine) diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py new file mode 100644 index 0000000..935d9a7 --- /dev/null +++ b/api/kiwi_vpn_api/db_new/user.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import root_validator +from sqlalchemy.exc import IntegrityError +from sqlmodel import Field, SQLModel + +from ..config import Config +from .connection import Connection + + +class UserBase(SQLModel): + name: str = Field(primary_key=True) + email: str + + country: str | None = Field(default=None) + state: str | None = Field(default=None) + city: str | None = Field(default=None) + organization: str | None = Field(default=None) + organizational_unit: str | None = Field(default=None) + + +class User(UserBase, table=True): + password: str + + @classmethod + def create(cls, **kwargs) -> User | None: + """ + Create a new user in the database. + """ + + try: + with Connection.session as db: + user = User.from_orm(UserCreate(**kwargs)) + + db.add(user) + db.commit() + db.refresh(user) + + return user + + except IntegrityError: + # user already existed + return None + + @classmethod + def get(cls, name: str) -> User | None: + with Connection.session as db: + return db.get(cls, name) + + +class UserCreate(UserBase): + password: str | None = Field(default=None) + password_clear: str | None = Field(default=None) + + @root_validator + @classmethod + def hash_password(cls, values: dict[str, Any]) -> dict[str, Any]: + if (values.get("password")) is not None: + # password is set + return values + + if (password_clear := values.get("password_clear")) is None: + raise ValueError("No password to hash") + + if (current_config := Config.load_sync()) is None: + raise ValueError("Not configured") + + values["password"] = current_config.crypto.crypt_context_sync.hash( + password_clear) + + return values + + +class UserRead(UserBase): + pass diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 9ed5b77..70d6915 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -15,6 +15,7 @@ from fastapi import FastAPI from .config import Config, Settings from .db import Connection from .db.schemata import User +from .db_new import connection, user from .routers import main_router settings = Settings.get() @@ -51,6 +52,16 @@ async def on_startup() -> None: print(User.from_db(db, "admin")) print(User.from_db(db, "nonexistent")) + connection.Connection.connect("sqlite:///tmp/v2.db") + + user.User.create( + name="Uwe", + password_clear="ulf", + email="uwe@feh.de", + ) + + print(user.User.get("Uwe")) + def main() -> None: uvicorn.run( diff --git a/api/poetry.lock b/api/poetry.lock index 3f5e84d..b778921 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -428,6 +428,30 @@ postgresql_psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql (<1)", "pymysql"] sqlcipher = ["sqlcipher3-binary"] +[[package]] +name = "sqlalchemy2-stubs" +version = "0.0.2a21" +description = "Typing Stubs for SQLAlchemy 1.4" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=3.7.4" + +[[package]] +name = "sqlmodel" +version = "0.0.6" +description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." +category = "main" +optional = false +python-versions = ">=3.6.1,<4.0.0" + +[package.dependencies] +pydantic = ">=1.8.2,<2.0.0" +SQLAlchemy = ">=1.4.17,<1.5.0" +sqlalchemy2-stubs = "*" + [[package]] name = "starlette" version = "0.17.1" @@ -477,7 +501,7 @@ standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "p [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "432d2933102f8a0091cec1b5484944a0211ca74c5dc9b65877d99d7bd160e4bb" +content-hash = "a580f9fe4c68667e4cbdf385ac11d5c7a2925e3c990b7164faa922ec8b6f9555" [metadata.files] anyio = [ @@ -834,6 +858,14 @@ sqlalchemy = [ {file = "SQLAlchemy-1.4.32-cp39-cp39-win_amd64.whl", hash = "sha256:b3f1d9b3aa09ab9adc7f8c4b40fc3e081eb903054c9a6f9ae1633fe15ae503b4"}, {file = "SQLAlchemy-1.4.32.tar.gz", hash = "sha256:6fdd2dc5931daab778c2b65b03df6ae68376e028a3098eb624d0909d999885bc"}, ] +sqlalchemy2-stubs = [ + {file = "sqlalchemy2-stubs-0.0.2a21.tar.gz", hash = "sha256:207e3d8a36fc032d325f4eec89e0c6760efe81d07e978513d8c9b14f108dcd0c"}, + {file = "sqlalchemy2_stubs-0.0.2a21-py3-none-any.whl", hash = "sha256:bd4a3d5ca7ff9d01b2245e1b26304d6b2ec4daf43a01faf40db9e09245679433"}, +] +sqlmodel = [ + {file = "sqlmodel-0.0.6-py3-none-any.whl", hash = "sha256:c5fd8719e09da348cd32ce2a5b6a44f289d3029fa8f1c9818229b6f34f1201b4"}, + {file = "sqlmodel-0.0.6.tar.gz", hash = "sha256:3b4f966b9671b24d85529d274e6c4dbc7753b468e35d2d6a40bd75cad1f66813"}, +] starlette = [ {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, diff --git a/api/pyproject.toml b/api/pyproject.toml index a105d62..a9a7855 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -13,6 +13,7 @@ python-jose = {extras = ["cryptography"], version = "^3.3.0"} passlib = {extras = ["argon2", "bcrypt"], version = "^1.7.4"} SQLAlchemy = "^1.4.32" pyOpenSSL = "^22.0.0" +sqlmodel = "^0.0.6" [tool.poetry.dev-dependencies] pytest = "^7.1.0"