diff --git a/apps/api/cli.py b/apps/api/cli.py new file mode 100644 index 00000000..9988b131 --- /dev/null +++ b/apps/api/cli.py @@ -0,0 +1,112 @@ +from typing import Annotated +from pydantic import EmailStr +from sqlalchemy import create_engine +from sqlmodel import SQLModel, Session +import typer + +from config.config import get_learnhouse_config +from src.db.organizations import OrganizationCreate +from src.db.users import UserCreate +from src.services.install.install import ( + install_create_organization, + install_create_organization_user, + install_default_elements, +) + +cli = typer.Typer() + + +@cli.command() +def install( + short: Annotated[bool, typer.Option(help="Install with predefined values")] = False +): + # Get the database session + learnhouse_config = get_learnhouse_config() + engine = create_engine( + learnhouse_config.database_config.sql_connection_string, echo=False, pool_pre_ping=True # type: ignore + ) + SQLModel.metadata.create_all(engine) + + db_session = Session(engine) + + if short: + # Install the default elements + print("Installing default elements...") + install_default_elements(db_session) + print("Default elements installed ✅") + + # Create the Organization + print("Creating default organization...") + org = OrganizationCreate( + name="Default Organization", + description="Default Organization", + slug="default", + email="", + logo_image="", + ) + install_create_organization(org, db_session) + print("Default organization created ✅") + + # Create Organization User + print("Creating default organization user...") + user = UserCreate( + username="admin", email=EmailStr("admin@school.io"), password="adminsecret" + ) + install_create_organization_user(user, "default", db_session) + print("Default organization user created ✅") + + # Show the user how to login + print("Installation completed ✅") + print("") + print("Login with the following credentials:") + print("email: admin@school.io") + print("password: adminsecret") + + else: + # Install the default elements + print("Installing default elements...") + install_default_elements(db_session) + print("Default elements installed ✅") + + # Create the Organization + print("Creating your organization...") + orgname = typer.prompt("What's shall we call your organization?") + slug = typer.prompt( + "What's the slug for your organization? (e.g. school, acme)" + ) + org = OrganizationCreate( + name=orgname, + description="Default Organization", + slug=slug.lower(), + email="", + logo_image="", + ) + install_create_organization(org, db_session) + print(orgname + " Organization created ✅") + + # Create Organization User + print("Creating your organization user...") + username = typer.prompt("What's the username for the user?") + email = typer.prompt("What's the email for the user?") + password = typer.prompt("What's the password for the user?", hide_input=True) + user = UserCreate(username=username, email=EmailStr(email), password=password) + install_create_organization_user(user, slug, db_session) + print(username + " user created ✅") + + # Show the user how to login + print("Installation completed ✅") + print("") + print("Login with the following credentials:") + print("email: " + email) + print("password: The password you entered") + + + + +@cli.command() +def main(): + cli() + + +if __name__ == "__main__": + cli() diff --git a/apps/api/poetry.lock b/apps/api/poetry.lock index d8a658e3..37f5d46c 100644 --- a/apps/api/poetry.lock +++ b/apps/api/poetry.lock @@ -3186,54 +3186,21 @@ telegram = ["requests"] [[package]] name = "typer" -version = "0.12.0" +version = "0.12.3" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.0-py3-none-any.whl", hash = "sha256:0441a0bb8962fb4383b8537ada9f7eb2d0deda0caa2cfe7387cc221290f617e4"}, - {file = "typer-0.12.0.tar.gz", hash = "sha256:900fe786ce2d0ea44653d3c8ee4594a22a496a3104370ded770c992c5e3c542d"}, -] - -[package.dependencies] -typer-cli = "0.12.0" -typer-slim = {version = "0.12.0", extras = ["standard"]} - -[[package]] -name = "typer-cli" -version = "0.12.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -files = [ - {file = "typer_cli-0.12.0-py3-none-any.whl", hash = "sha256:7b7e2dd49f59974bb5a869747045d5444b17bffb851e006cd424f602d3578104"}, - {file = "typer_cli-0.12.0.tar.gz", hash = "sha256:603ed3d5a278827bd497e4dc73a39bb714b230371c8724090b0de2abdcdd9f6e"}, -] - -[package.dependencies] -typer-slim = {version = "0.12.0", extras = ["standard"]} - -[[package]] -name = "typer-slim" -version = "0.12.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -files = [ - {file = "typer_slim-0.12.0-py3-none-any.whl", hash = "sha256:ddd7042b29a32140528caa415750bcae54113ba0c32270ca11a6f64069ddadf9"}, - {file = "typer_slim-0.12.0.tar.gz", hash = "sha256:3e8a3f17286b173d76dca0fd4e02651c9a2ce1467b3754876b1ac4bd72572beb"}, + {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, + {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, ] [package.dependencies] click = ">=8.0.0" -rich = {version = ">=10.11.0", optional = true, markers = "extra == \"standard\""} -shellingham = {version = ">=1.3.0", optional = true, markers = "extra == \"standard\""} +rich = ">=10.11.0" +shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" -[package.extras] -all = ["rich (>=10.11.0)", "shellingham (>=1.3.0)"] -standard = ["rich (>=10.11.0)", "shellingham (>=1.3.0)"] - [[package]] name = "typing-extensions" version = "4.10.0" @@ -3730,4 +3697,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "0f7dbd7ca5511470158142c475dab8fad69ffb35fb31da437847bda67f3efbcb" +content-hash = "849c99445d4d0dc5fb06a50359c4e166b357f3705003c2989103a86fd388decb" diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml index 6de4ab7f..8d1fd38c 100644 --- a/apps/api/pyproject.toml +++ b/apps/api/pyproject.toml @@ -37,6 +37,7 @@ sentry-sdk = {extras = ["fastapi"], version = "^1.45.0"} sqlmodel = "^0.0.16" tiktoken = "^0.6.0" uvicorn = "0.29.0" +typer = "^0.12.3" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/apps/api/src/routers/install/install.py b/apps/api/src/routers/install/install.py index 40d9202c..11f8b847 100644 --- a/apps/api/src/routers/install/install.py +++ b/apps/api/src/routers/install/install.py @@ -3,7 +3,6 @@ from src.db.install import InstallRead from src.core.events.database import get_db_session from src.db.organizations import OrganizationCreate from src.db.users import UserCreate - from src.services.install.install import ( create_install_instance, get_latest_install_instance, @@ -43,7 +42,7 @@ async def api_get_latest_install_instance( async def api_install_def_elements( db_session=Depends(get_db_session), ): - elements = await install_default_elements(db_session) + elements = install_default_elements(db_session) return elements @@ -53,7 +52,7 @@ async def api_install_org( org: OrganizationCreate, db_session=Depends(get_db_session), ): - organization = await install_create_organization(org, db_session) + organization = install_create_organization(org, db_session) return organization @@ -64,7 +63,7 @@ async def api_install_user( org_slug: str, db_session=Depends(get_db_session), ): - user = await install_create_organization_user(data, org_slug, db_session) + user = install_create_organization_user(data, org_slug, db_session) return user diff --git a/apps/api/src/security/auth.py b/apps/api/src/security/auth.py index dc9c6fd7..e85818df 100644 --- a/apps/api/src/security/auth.py +++ b/apps/api/src/security/auth.py @@ -59,7 +59,7 @@ async def authenticate_user( user = await security_get_user(request, db_session, email) if not user: return False - if not await security_verify_password(password, user.password): + if not security_verify_password(password, user.password): return False return user diff --git a/apps/api/src/security/security.py b/apps/api/src/security/security.py index 0972df02..c3734f38 100644 --- a/apps/api/src/security/security.py +++ b/apps/api/src/security/security.py @@ -17,11 +17,11 @@ ALGORITHM = "HS256" ### 🔒 Passwords Hashing ############################################################## -async def security_hash_password(password: str): +def security_hash_password(password: str): return pbkdf2_sha256.hash(password) -async def security_verify_password(plain_password: str, hashed_password: str): +def security_verify_password(plain_password: str, hashed_password: str): return pbkdf2_sha256.verify(plain_password, hashed_password) diff --git a/apps/api/src/services/install/install.py b/apps/api/src/services/install/install.py index 3b298cda..c54580eb 100644 --- a/apps/api/src/services/install/install.py +++ b/apps/api/src/services/install/install.py @@ -95,7 +95,7 @@ async def update_install_instance( # Install Default roles -async def install_default_elements(db_session: Session): +def install_default_elements(db_session: Session): """ """ # remove all default roles @@ -300,7 +300,7 @@ async def install_default_elements(db_session: Session): # Organization creation -async def install_create_organization( +def install_create_organization( org_object: OrganizationCreate, db_session: Session ): org = Organization.model_validate(org_object) @@ -364,14 +364,14 @@ async def install_create_organization( return org -async def install_create_organization_user( +def install_create_organization_user( user_object: UserCreate, org_slug: str, db_session: Session ): user = User.model_validate(user_object) # Complete the user object user.user_uuid = f"user_{uuid4()}" - user.password = await security_hash_password(user_object.password) + user.password = security_hash_password(user_object.password) user.email_verified = False user.creation_date = str(datetime.now()) user.update_date = str(datetime.now()) diff --git a/apps/api/src/services/users/password_reset.py b/apps/api/src/services/users/password_reset.py index 092d50cc..3d72ed43 100644 --- a/apps/api/src/services/users/password_reset.py +++ b/apps/api/src/services/users/password_reset.py @@ -190,7 +190,7 @@ async def change_password_with_reset_code( ) # Change password - user.password = await security_hash_password(new_password) + user.password = security_hash_password(new_password) db_session.add(user) db_session.commit() diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index f96c94b6..a0a716f6 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -44,7 +44,7 @@ async def create_user( # Complete the user object user.user_uuid = f"user_{uuid4()}" - user.password = await security_hash_password(user_object.password) + user.password = security_hash_password(user_object.password) user.email_verified = False user.creation_date = str(datetime.now()) user.update_date = str(datetime.now()) @@ -164,7 +164,7 @@ async def create_user_without_org( # Complete the user object user.user_uuid = f"user_{uuid4()}" - user.password = await security_hash_password(user_object.password) + user.password = security_hash_password(user_object.password) user.email_verified = False user.creation_date = str(datetime.now()) user.update_date = str(datetime.now()) @@ -340,13 +340,13 @@ async def update_user_password( # RBAC check await rbac_check(request, current_user, "update", user.user_uuid, db_session) - if not await security_verify_password(form.old_password, user.password): + if not security_verify_password(form.old_password, user.password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password" ) # Update user - user.password = await security_hash_password(form.new_password) + user.password = security_hash_password(form.new_password) user.update_date = str(datetime.now()) # Update user in database