diff --git a/.cursor/rules/routers.mdc b/.cursor/rules/routers.mdc index b94ff3b..3da27aa 100644 --- a/.cursor/rules/routers.mdc +++ b/.cursor/rules/routers.mdc @@ -1,12 +1,12 @@ --- description: Testing FastAPI routes -globs: routers/*.py +globs: routers/**/*.py alwaysApply: false --- Here are the five most critical patterns to maintain consistency when adding a new router: 1. **Authentication & Dependency Injection** - - Import `get_authenticated_user` from `utils.dependencies` and include `user: User = Depends(get_authenticated_user)` in the arguments of routes requiring authentication + - Import `get_authenticated_user` from `utils.core.dependencies` and include `user: User = Depends(get_authenticated_user)` in the arguments of routes requiring authentication - Similarly, use the `get_optional_user` dependency for public routes with potential auth status 2. **Validation Patterns** @@ -21,7 +21,7 @@ Here are the five most critical patterns to maintain consistency when adding a n - Check permissions at both route and template levels via `user_permissions` 4. **Database & Transaction Patterns** - - Inject session via `Depends(get_session)` + - Inject session via `Depends(get_session)` from `utils/core/dependencies.py` - Commit after writes and refresh objects where needed - Use `selectinload` for eager loading relationships - Follow PRG pattern with RedirectResponse after mutations diff --git a/.cursor/rules/routers_tests.mdc b/.cursor/rules/routers_tests.mdc index 65c4904..6c997db 100644 --- a/.cursor/rules/routers_tests.mdc +++ b/.cursor/rules/routers_tests.mdc @@ -1,6 +1,6 @@ --- description: -globs: tests/routers/test_*.py +globs: tests/routers/**/*.py alwaysApply: false --- # Setting test expectations regarding HTTP status codes diff --git a/.cursor/rules/tests.mdc b/.cursor/rules/tests.mdc index 9467dc9..5e5935e 100644 --- a/.cursor/rules/tests.mdc +++ b/.cursor/rules/tests.mdc @@ -1,6 +1,6 @@ --- description: Building, running, and debugging tests -globs: tests/*.py +globs: tests/**.py alwaysApply: false --- This project uses `uv` for dependency management, so tests must be run with `uv run pytest` to ensure they are run in the project's virtual environment. @@ -9,4 +9,4 @@ The project uses test-driven development, so failing tests are often what we wan Session-wide test setup is performed in `tests/conftest.py`. In that file, you will find fixtures that can and should be reused across the test suite, including fixtures for database setup and teardown. We have intentionally used PostgreSQL, not SQLite, in the test suite to keep the test environment as production-like as possible, and you should never change the database engine unless explicitly told to do so. -If you find that the test database is not available, you may need to start Docker Desktop with `systemctl --user start docker-desktop` or the database with `docker compose up`. You may `grep` the `DB_PORT=` line from `.env` if you need to know what port the database is available on. (This environment variable is used for port mapping in `docker-compose.yml` as well as in the `get_connection_url` function defined in `utils/db.py`.) If dropping tables fails during test setup due to changes to the database schema, `docker compose down -v && docker compose up` may resolve the issue. \ No newline at end of file +If you find that the test database is not available, you may need to start Docker Desktop with `systemctl --user start docker-desktop` or the database with `docker compose up`. You may `grep` the `DB_PORT=` line from `.env` if you need to know what port the database is available on. (This environment variable is used for port mapping in `docker-compose.yml` as well as in the `get_connection_url` function defined in `utils/core/db.py`.) If dropping tables fails during test setup due to changes to the database schema, `docker compose down -v && docker compose up` may resolve the issue. \ No newline at end of file diff --git a/docs/customization.qmd b/docs/customization.qmd index d9adf6f..265103a 100644 --- a/docs/customization.qmd +++ b/docs/customization.qmd @@ -22,6 +22,14 @@ If you are using VSCode or Cursor as your IDE, you will need to select the `uv`- If your IDE does not automatically detect and display this option, you can manually select the interpreter by selecting "Enter interpreter path" and then navigating to the `.venv/bin/python` subfolder in your project directory. +### Extending the template + +The `routers/core/` and `utils/core/` directories contain the core backend logic for the template. + +Your custom Python backend code should go primarily in the `routers/app/` and `utils/app/` directories. + +For the frontend, you will also need to develop custom Jinja2 templates in the `templates/` folder and add custom static assets in `static/`. + ### Testing The project uses Pytest for unit testing. It's highly recommended to write and run tests before committing code to ensure nothing is broken! @@ -57,11 +65,7 @@ We find that mypy is an enormous time-saver, catching many errors early and grea ### Developing with LLMs -In line with the [llms.txt standard](https://llmstxt.org/), we have provided a Markdown-formatted prompt—designed to help LLM agents understand how to work with this template—as a text file: [llms.txt](static/llms.txt). - -One use case for this file, if using the Cursor IDE, is to rename it to `.cursorrules` and place it in your project directory (see the [Cursor docs](https://docs.cursor.com/context/rules-for-ai) on this for more information). Alternatively, you could use it as a custom system prompt in the web interface for ChatGPT, Claude, or the LLM of your choice. - -We have also exposed the full Markdown-formatted project documentation as a [single text file](static/documentation.txt) for easy downloading and embedding for RAG workflows. +The `.cursor/rules` folder contains a set of AI rules for working on this codebase in the Cursor IDE. We have also provided an [llms.txt](static/llms.txt) system prompt file for use with other agentic LLM workflows and exposed the full Markdown-formatted project documentation as a [single text file](docs/static/documentation.txt) for easy downloading and embedding for RAG. ## Application architecture @@ -71,37 +75,40 @@ In this template, we use FastAPI to define the "API endpoints" of our applicatio We also create POST endpoints, which accept form submissions so the user can create, update, and delete data in the database. This template follows the Post-Redirect-Get (PRG) pattern to handle POST requests. When a form is submitted, the server processes the data and then returns a "redirect" response, which sends the user to a GET endpoint to re-render the page with the updated data. (See [Architecture](https://promptlytechnologies.com/fastapi-jinja2-postgres-webapp/docs/architecture.html) for more details.) -#### Customizable folders and files +#### File structure - FastAPI application entry point and homepage GET route: `main.py` -- FastAPI routes: `routers/` +- Template FastAPI routes: `routers/core/` - Account and authentication endpoints: `account.py` - User profile management endpoints: `user.py` - Organization management endpoints: `organization.py` - Role management endpoints: `role.py` - Dashboard page: `dashboard.py` - Static pages (e.g., about, privacy policy, terms of service): `static_pages.py` +- Custom FastAPI routes for your app: `routers/app/` - Jinja2 templates: `templates/` - Static assets: `static/` - Unit tests: `tests/` - Test database configuration: `docker-compose.yml` -- Helper functions: `utils/` +- Template helper functions: `utils/core/` - Auth helpers: `auth.py` - Database helpers: `db.py` - FastAPI dependencies: `dependencies.py` - Enums: `enums.py` - Image helpers: `images.py` - Database models: `models.py` +- Custom template helper functions for your app: `utils/app/` - Exceptions: `exceptions/` - HTTP exceptions: `http_exceptions.py` - Other custom exceptions: `exceptions.py` -- Environment variables: `.env.example` +- Environment variables: `.env.example`, `.env` - CI/CD configuration: `.github/` - Project configuration: `pyproject.toml` - Quarto documentation: - README source: `index.qmd` - Website source: `index.qmd` + `docs/` - - Configuration: `_quarto.yml` + - Configuration: `_quarto.yml` + `_environment` +- Rules for developing with LLMs in Cursor IDE: `.cursor/rules/` Most everything else is auto-generated and should not be manually modified. @@ -109,7 +116,7 @@ Most everything else is auto-generated and should not be manually modified. ### Code conventions -The GET route for the homepage is defined in the main entry point for the application, `main.py`. The entrypoint imports router modules from the `routers/` directory, which contain the other GET and POST routes for the application. In CRUD style, the router modules are named after the resource they manage, e.g., `account.py` for account management. +The GET route for the homepage is defined in the main entry point for the application, `main.py`. The entrypoint imports router modules from the `routers/core/` directory (for core/template logic) and `routers/app/` directory (for app-specific logic). In CRUD style, the core router modules are named after the resource they manage, e.g., `account.py` for account management. You should place your own endpoints in `routers/app/`. We name our GET routes using the convention `read_`, where `` is the name of the resource, to indicate that they are read-only endpoints that do not modify the database. In POST routes that modify the database, you can use the `get_session` dependency as an argument to get a database session. @@ -177,7 +184,7 @@ SQLModel is an Object-Relational Mapping (ORM) library that allows us to interac ### Models and relationships -Our database models are defined in `utils/models.py`. Each model is a Python class that inherits from `SQLModel` and represents a database table. The key models are: +Core database models are defined in `utils/core/models.py`. Each model is a Python class that inherits from `SQLModel` and represents a database table. The key core models are: - `Account`: Represents a user account with email and password hash - `User`: Represents a user profile with details like name and avatar; the email and password hash are stored in the related `Account` model @@ -192,15 +199,15 @@ Two additional models are used by SQLModel to manage many-to-many relationships; - `UserRoleLink`: Maps users to their roles (many-to-many relationship) - `RolePermissionLink`: Maps roles to their permissions (many-to-many relationship) -Here's an entity-relationship diagram (ERD) of the current database schema, automatically generated from our SQLModel definitions: +Here's an entity-relationship diagram (ERD) of the current core database schema, automatically generated from our SQLModel definitions: ```{python} #| echo: false #| warning: false import sys sys.path.append("..") -from utils.models import * -from utils.db import engine +from utils.core.models import * +from utils.core.db import engine from sqlalchemy import MetaData from sqlalchemy_schemadisplay import create_schema_graph @@ -220,16 +227,17 @@ graph.write_png('static/schema.png') ![Database Schema](static/schema.png) +To extend the database schema, define your own models in `utils/app/models.py` and import them in `utils/core/db.py` to make sure they are included in the `metadata` object in the `create_all` function. ### Database helpers -Database operations are facilitated by helper functions in `utils/db.py`. Key functions include: +Database operations are facilitated by helper functions in `utils/core/db.py` (for core logic) and `utils/app/` (for app-specific helpers). Key functions in the core utils include: - `set_up_db()`: Initializes the database schema and default data (which we do on every application start in `main.py`) - `get_connection_url()`: Creates a database connection URL from environment variables in `.env` - `get_session()`: Provides a database session for performing operations -To perform database operations in route handlers, inject the database session as a dependency: +To perform database operations in route handlers, inject the database session as a dependency (from `utils/core/db.py`): ```python @app.get("/users") @@ -240,7 +248,7 @@ async def get_users(session: Session = Depends(get_session)): The session automatically handles transaction management, ensuring that database operations are atomic and consistent. -There is also a helper method on the `User` model that checks if a user has a specific permission for a given organization. Its first argument must be a `ValidPermissions` enum value (from `utils/models.py`), and its second argument must be an `Organization` object or an `int` representing an organization ID: +There is also a helper method on the `User` model that checks if a user has a specific permission for a given organization. Its first argument must be a `ValidPermissions` enum value (from `utils/core/models.py`), and its second argument must be an `Organization` object or an `int` representing an organization ID: ```python permission = ValidPermissions.CREATE_ROLE @@ -249,7 +257,7 @@ organization = session.exec(select(Organization).where(Organization.name == "Acm user.has_permission(permission, organization) ``` -You should create custom `ValidPermissions` enum values for your application and validate that users have the necessary permissions before allowing them to modify organization data resources. +You should create custom `AppPermissions` enum values for your application in `utils/app/` (if needed) and validate that users have the necessary permissions before allowing them to modify organization data resources. ### Cascade deletes diff --git a/docs/static/documentation.txt b/docs/static/documentation.txt index cfda3b6..41128c2 100644 --- a/docs/static/documentation.txt +++ b/docs/static/documentation.txt @@ -785,8 +785,8 @@ Here's an entity-relationship diagram (ERD) of the current database schema, auto #| warning: false import sys sys.path.append("..") -from utils.models import * -from utils.db import engine +from utils.core.models import * +from utils.core.db import engine from sqlalchemy import MetaData from sqlalchemy_schemadisplay import create_schema_graph diff --git a/docs/static/llms.txt b/docs/static/llms.txt index adc1692..b2f2de3 100644 --- a/docs/static/llms.txt +++ b/docs/static/llms.txt @@ -1,75 +1,39 @@ # Project Architecture -- Keep GET routes in main.py and POST routes in routers/ directory -- Name GET routes using read_ convention +- Name GET routes using `read_` convention - Follow Post-Redirect-Get (PRG) pattern for all form submissions - Use Jinja2 HTML templates for server-side rendering and minimize client-side JavaScript - Use forms for all POST routes - Validate form data comprehensively on the client side as first line of defense, with server-side Pydantic validation as fallback - -# File Structure -- main.py: Application entry point and GET routes -- routers/: POST route modules -- templates/: Jinja2 templates -- static/: Static assets -- tests/: Unit tests -- utils/: Helper functions and models -- docker-compose.yml: Test database configuration -- .env: Environment variables - -# Python/FastAPI Guidelines -- For all POST routes, define request models in a separate section at the top of the router file -- Implement as_form() classmethod for all form-handling request models -- Use Pydantic for request/response models with @field_validator and custom exceptions for custom form validation - Use middleware defined in main.py for centralized exception handling -- Add type hints to all function signatures and variables -- Follow mypy type checking standards rigorously +- Add type hints to all function signatures and variables for static type checking -# Form Validation Strategy -- Implement thorough client-side validation via HTML pattern attributes where possible and Javascript otherwise -- Use Pydantic models with custom validators as server-side fallback -- Handle validation errors through middleware exception handlers -- Render validation_error.html template for failed server-side validation +# File Structure +- `main.py`: Application entry point and GET routes +- `routers/core`: Base webapp template API routes +- `routers/app`: Application API routes that extend the template +- `utils/core`, `utils/app`: Helper functions, FastAPI dependencies, database models +- `templates/`: Jinja2 templates +- `static/`: Static assets +- `tests/`: Unit tests +- `docker-compose.yml`: Test database configuration +- `.env`: Environment variables +- `docs/`: Quarto documentation website source files # Database Operations - Use SQLModel for all database interactions -- Use get_session() from utils/db.py for database connections -- Define database relational models explicitly in utils/models.py -- Inject database session as dependency in route handlers +- Use `get_session()` FastAPI dependency from `utils/core/dependencies.py` for database connections # Authentication System -- JWT-based token authentication with separate access/refresh tokens and bcrypt for password hashing are defined in utils/auth.py -- Password and email reset tokens with expiration and password reset email flow powered by Resend are defined in utils/auth.py -- HTTP-only cookies are implemented with secure flag and SameSite=strict -- Inject common_authenticated_parameters as a dependency in all authenticated GET routes -- Inject common_unauthenticated_parameters as a dependency in all unauthenticated GET routes -- Inject get_session as a dependency in all POST routes -- Handle security-related errors without leaking information - -# Testing -- Run mypy type checking before committing code -- Write comprehensive unit tests using pytest -- Test both success and error cases -- Use test fixtures from tests/conftest.py: engine, session, client, test_user -- set_up_database and clean_db fixtures are autoused by pytest to ensure clean database state - -# Error Handling -- Use middleware for centralized exception handling -- Define custom exception classes for specific error cases -- Return appropriate HTTP status codes and error messages -- Render error templates with context data -- Log errors with "uvicorn.error" logger - -# Template Structure -- Extend base.html for consistent layout -- Use block tags for content sections -- Include reusable components -- Pass request object and context data to all templates -- Keep form validation logic in corresponding templates -- Use Bootstrap for styling +- JWT-based token authentication with separate access/refresh tokens and bcrypt for password hashing are defined in `utils/core/auth.py` +- Password and email reset tokens with expiration and password reset email flow powered by Resend are defined in `utils/core/auth.py` +- HTTP-only cookies are implemented with secure flag and `SameSite=strict` +- Inject `common_authenticated_parameters` as a dependency in all authenticated GET routes +- Inject `common_unauthenticated_parameters` as a dependency in all unauthenticated GET routes # Contributing Guidelines - Follow existing code style and patterns - Preserve existing comments and docstrings - Ensure all tests pass before submitting PR - Update .qmd documentation files for significant changes -- Use uv for dependency management \ No newline at end of file +- Use uv for dependency management +- Run `uv run mypy .` to ensure code passes a static type check \ No newline at end of file diff --git a/exceptions/exceptions.py b/exceptions/exceptions.py index 3bf28cd..0172b88 100644 --- a/exceptions/exceptions.py +++ b/exceptions/exceptions.py @@ -1,4 +1,4 @@ -from utils.models import User +from utils.core.models import User class NeedsNewTokens(Exception): diff --git a/exceptions/http_exceptions.py b/exceptions/http_exceptions.py index 9420375..a2ff3d0 100644 --- a/exceptions/http_exceptions.py +++ b/exceptions/http_exceptions.py @@ -1,5 +1,5 @@ from fastapi import HTTPException, status -from utils.enums import ValidPermissions +from utils.core.enums import ValidPermissions class EmailAlreadyRegisteredError(HTTPException): def __init__(self): diff --git a/index.qmd b/index.qmd index ddd59f2..f63ffce 100644 --- a/index.qmd +++ b/index.qmd @@ -207,11 +207,7 @@ with open(output_path, 'w', encoding='utf-8') as f: f.write(final_content) ``` -In line with the [llms.txt standard](https://llmstxt.org/), we have provided a Markdown-formatted prompt—designed to help LLM agents understand how to work with this template—as a text file: [llms.txt](docs/static/llms.txt). - -One use case for this file, if using the Cursor IDE, is to rename it to `.cursorrules` and place it in your project directory (see the [Cursor docs](https://docs.cursor.com/context/rules-for-ai) on this for more information). Alternatively, you could use it as a custom system prompt in the web interface for ChatGPT, Claude, or the LLM of your choice. - -We have also exposed the full Markdown-formatted project documentation as a [single text file](docs/static/documentation.txt) for easy downloading and embedding for RAG workflows. +The `.cursor/rules` folder contains a set of AI rules for working on this codebase in the Cursor IDE. We have also provided an [llms.txt](static/llms.txt) system prompt file for use with other agentic LLM workflows and exposed the full Markdown-formatted project documentation as a [single text file](docs/static/documentation.txt) for easy downloading and embedding for RAG. ## Contributing diff --git a/main.py b/main.py index 6a640ab..e5c18aa 100644 --- a/main.py +++ b/main.py @@ -7,8 +7,8 @@ from fastapi.templating import Jinja2Templates from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException as StarletteHTTPException -from routers import account, dashboard, organization, role, user, static_pages, invitation -from utils.dependencies import ( +from routers.core import account, dashboard, organization, role, user, static_pages, invitation +from utils.core.dependencies import ( get_optional_user ) from exceptions.http_exceptions import ( @@ -18,8 +18,8 @@ from exceptions.exceptions import ( NeedsNewTokens ) -from utils.db import set_up_db -from utils.models import User +from utils.core.db import set_up_db +from utils.core.models import User logger = logging.getLogger("uvicorn.error") logger.setLevel(logging.DEBUG) diff --git a/routers/app/__init__.py b/routers/app/__init__.py new file mode 100644 index 0000000..4db90c6 --- /dev/null +++ b/routers/app/__init__.py @@ -0,0 +1 @@ +"""This folder is where you would define application-specific, auth-protected routers.""" \ No newline at end of file diff --git a/routers/core/__init__.py b/routers/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/account.py b/routers/core/account.py similarity index 98% rename from routers/account.py rename to routers/core/account.py index b5368f8..06fe929 100644 --- a/routers/account.py +++ b/routers/core/account.py @@ -8,9 +8,9 @@ from starlette.datastructures import URLPath from pydantic import EmailStr from sqlmodel import Session, select -from utils.models import User, DataIntegrityError, Account, Invitation -from utils.db import get_session -from utils.auth import ( +from utils.core.models import User, DataIntegrityError, Account, Invitation +from utils.core.dependencies import get_session +from utils.core.auth import ( HTML_PASSWORD_PATTERN, COMPILED_PASSWORD_PATTERN, oauth2_scheme_cookie, @@ -22,7 +22,7 @@ send_reset_email, send_email_update_confirmation ) -from utils.dependencies import ( +from utils.core.dependencies import ( get_authenticated_account, get_optional_user, get_account_from_reset_token, @@ -37,10 +37,10 @@ InvitationEmailMismatchError, InvitationProcessingError ) -from routers.dashboard import router as dashboard_router -from routers.user import router as user_router -from routers.organization import router as org_router -from utils.invitations import process_invitation +from routers.core.dashboard import router as dashboard_router +from routers.core.user import router as user_router +from routers.core.organization import router as org_router +from utils.core.invitations import process_invitation logger = getLogger("uvicorn.error") router = APIRouter(prefix="/account", tags=["account"]) diff --git a/routers/dashboard.py b/routers/core/dashboard.py similarity index 84% rename from routers/dashboard.py rename to routers/core/dashboard.py index ef60820..d349a1f 100644 --- a/routers/dashboard.py +++ b/routers/core/dashboard.py @@ -1,8 +1,8 @@ from typing import Optional from fastapi import APIRouter, Depends, Request from fastapi.templating import Jinja2Templates -from utils.dependencies import get_user_with_relations -from utils.models import User +from utils.core.dependencies import get_user_with_relations +from utils.core.models import User router = APIRouter(prefix="/dashboard", tags=["dashboard"]) templates = Jinja2Templates(directory="templates") diff --git a/routers/invitation.py b/routers/core/invitation.py similarity index 95% rename from routers/invitation.py rename to routers/core/invitation.py index 2b8b245..503fe48 100644 --- a/routers/invitation.py +++ b/routers/core/invitation.py @@ -7,10 +7,9 @@ from sqlmodel import Session, select from logging import getLogger -from utils.dependencies import get_authenticated_user, get_optional_user -from utils.db import get_session -from utils.models import User, Role, Account, Invitation, ValidPermissions, Organization -from utils.invitations import send_invitation_email, process_invitation +from utils.core.dependencies import get_authenticated_user, get_optional_user, get_session +from utils.core.models import User, Role, Account, Invitation, ValidPermissions, Organization +from utils.core.invitations import send_invitation_email, process_invitation from exceptions.http_exceptions import ( UserIsAlreadyMemberError, ActiveInvitationExistsError, @@ -22,8 +21,8 @@ ) from exceptions.exceptions import EmailSendFailedError # Import the account router to generate URLs for login/register -from routers.account import router as account_router -from routers.organization import router as org_router # Already imported, check usage +from routers.core.account import router as account_router +from routers.core.organization import router as org_router # Already imported, check usage # Setup logger logger = getLogger("uvicorn.error") diff --git a/routers/organization.py b/routers/core/organization.py similarity index 97% rename from routers/organization.py rename to routers/core/organization.py index e3989a1..601edcf 100644 --- a/routers/organization.py +++ b/routers/core/organization.py @@ -5,10 +5,10 @@ from fastapi.templating import Jinja2Templates from sqlmodel import Session, select from sqlalchemy.orm import selectinload -from utils.db import get_session, create_default_roles -from utils.dependencies import get_authenticated_user, get_user_with_relations -from utils.models import Organization, User, Role, Account, utc_now, Invitation -from utils.enums import ValidPermissions +from utils.core.db import create_default_roles +from utils.core.dependencies import get_authenticated_user, get_user_with_relations, get_session +from utils.core.models import Organization, User, Role, Account, utc_now, Invitation +from utils.core.enums import ValidPermissions from exceptions.http_exceptions import ( OrganizationNotFoundError, OrganizationNameTakenError, InsufficientPermissionsError, OrganizationSetupError, diff --git a/routers/role.py b/routers/core/role.py similarity index 96% rename from routers/role.py rename to routers/core/role.py index 7e9aa11..9246474 100644 --- a/routers/role.py +++ b/routers/core/role.py @@ -7,11 +7,10 @@ from sqlmodel import Session, select, col from sqlalchemy.orm import selectinload from sqlalchemy.exc import IntegrityError -from utils.db import get_session -from utils.dependencies import get_authenticated_user -from utils.models import Role, Permission, ValidPermissions, utc_now, User, DataIntegrityError +from utils.core.dependencies import get_authenticated_user, get_session +from utils.core.models import Role, Permission, ValidPermissions, utc_now, User, DataIntegrityError from exceptions.http_exceptions import InsufficientPermissionsError, InvalidPermissionError, RoleAlreadyExistsError, RoleNotFoundError, RoleHasUsersError, CannotModifyDefaultRoleError -from routers.organization import router as organization_router +from routers.core.organization import router as organization_router logger = getLogger("uvicorn.error") diff --git a/routers/static_pages.py b/routers/core/static_pages.py similarity index 93% rename from routers/static_pages.py rename to routers/core/static_pages.py index 207ee71..5d7543b 100644 --- a/routers/static_pages.py +++ b/routers/core/static_pages.py @@ -1,8 +1,8 @@ from typing import Optional from fastapi import APIRouter, Depends, Request, HTTPException from fastapi.templating import Jinja2Templates -from utils.dependencies import get_optional_user -from utils.models import User +from utils.core.dependencies import get_optional_user +from utils.core.models import User router = APIRouter(tags=["static_pages"]) templates = Jinja2Templates(directory="templates") diff --git a/routers/user.py b/routers/core/user.py similarity index 93% rename from routers/user.py rename to routers/core/user.py index 30f15b4..fd11d8f 100644 --- a/routers/user.py +++ b/routers/core/user.py @@ -4,17 +4,16 @@ from typing import Optional, List from fastapi.templating import Jinja2Templates from sqlalchemy.orm import selectinload -from utils.models import User, DataIntegrityError, Organization -from utils.db import get_session -from utils.dependencies import get_authenticated_user, get_user_with_relations -from utils.images import validate_and_process_image, MAX_FILE_SIZE, MIN_DIMENSION, MAX_DIMENSION, ALLOWED_CONTENT_TYPES -from utils.enums import ValidPermissions +from utils.core.models import User, DataIntegrityError, Organization +from utils.core.dependencies import get_authenticated_user, get_user_with_relations, get_session +from utils.core.images import validate_and_process_image, MAX_FILE_SIZE, MIN_DIMENSION, MAX_DIMENSION, ALLOWED_CONTENT_TYPES +from utils.core.enums import ValidPermissions from exceptions.http_exceptions import ( InsufficientPermissionsError, UserNotFoundError, OrganizationNotFoundError ) -from routers.organization import router as organization_router +from routers.core.organization import router as organization_router router = APIRouter(prefix="/user", tags=["user"]) templates = Jinja2Templates(directory="templates") diff --git a/tests/__init__.py b/tests/__init__.py index 0d659ac..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +0,0 @@ -""" -This file marks tests as a Python package. -""" diff --git a/tests/conftest.py b/tests/conftest.py index 97a8179..a64f8d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,9 +4,9 @@ from sqlalchemy import Engine from fastapi.testclient import TestClient from dotenv import load_dotenv -from utils.db import get_connection_url, tear_down_db, set_up_db, create_default_roles -from utils.models import User, PasswordResetToken, EmailUpdateToken, Organization, Role, Account, Invitation -from utils.auth import get_password_hash, create_access_token, create_refresh_token +from utils.core.db import get_connection_url, tear_down_db, set_up_db, create_default_roles +from utils.core.models import User, PasswordResetToken, EmailUpdateToken, Organization, Role, Account, Invitation +from utils.core.auth import get_password_hash, create_access_token, create_refresh_token from main import app from datetime import datetime, UTC, timedelta diff --git a/tests/routers/__init__.py b/tests/routers/__init__.py deleted file mode 100644 index 3a93170..0000000 --- a/tests/routers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -This file marks tests.routers as a Python package. -""" \ No newline at end of file diff --git a/tests/routers/core/__init__.py b/tests/routers/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/routers/test_account.py b/tests/routers/core/test_account.py similarity index 99% rename from tests/routers/test_account.py rename to tests/routers/core/test_account.py index f08c8cc..a7b9fe3 100644 --- a/tests/routers/test_account.py +++ b/tests/routers/core/test_account.py @@ -8,8 +8,8 @@ from sqlalchemy import inspect from main import app -from utils.models import User, PasswordResetToken, EmailUpdateToken, Account -from utils.auth import ( +from utils.core.models import User, PasswordResetToken, EmailUpdateToken, Account +from utils.core.auth import ( create_access_token, verify_password, validate_token, diff --git a/tests/routers/test_invitation.py b/tests/routers/core/test_invitation.py similarity index 99% rename from tests/routers/test_invitation.py rename to tests/routers/core/test_invitation.py index 4ac1bab..a0deb70 100644 --- a/tests/routers/test_invitation.py +++ b/tests/routers/core/test_invitation.py @@ -3,9 +3,9 @@ from unittest.mock import MagicMock, patch from sqlmodel import Session, select from tests.conftest import SetupError -from utils.models import Role, Permission, ValidPermissions, User, Invitation, Organization, Account +from utils.core.models import Role, Permission, ValidPermissions, User, Invitation, Organization, Account from main import app -from utils.invitations import generate_invitation_link +from utils.core.invitations import generate_invitation_link from exceptions.exceptions import EmailSendFailedError @pytest.fixture diff --git a/tests/routers/test_invitation_acceptance.py b/tests/routers/core/test_invitation_acceptance.py similarity index 99% rename from tests/routers/test_invitation_acceptance.py rename to tests/routers/core/test_invitation_acceptance.py index 2a1aabf..7ff48d3 100644 --- a/tests/routers/test_invitation_acceptance.py +++ b/tests/routers/core/test_invitation_acceptance.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse, parse_qs from main import app -from utils.models import User, Account, Invitation +from utils.core.models import User, Account, Invitation # --- Test Scenarios --- diff --git a/tests/routers/test_organization.py b/tests/routers/core/test_organization.py similarity index 99% rename from tests/routers/test_organization.py rename to tests/routers/core/test_organization.py index a70b303..424a666 100644 --- a/tests/routers/test_organization.py +++ b/tests/routers/core/test_organization.py @@ -1,5 +1,5 @@ -from utils.models import Organization, Role, Permission, ValidPermissions, User -from utils.db import create_default_roles +from utils.core.models import Organization, Role, Permission, ValidPermissions, User +from utils.core.db import create_default_roles from main import app from sqlmodel import select from tests.conftest import SetupError diff --git a/tests/routers/test_role.py b/tests/routers/core/test_role.py similarity index 99% rename from tests/routers/test_role.py rename to tests/routers/core/test_role.py index d73dfad..594d1ab 100644 --- a/tests/routers/test_role.py +++ b/tests/routers/core/test_role.py @@ -2,7 +2,7 @@ import pytest from tests.conftest import SetupError -from utils.models import Role, Permission, ValidPermissions, User +from utils.core.models import Role, Permission, ValidPermissions, User from sqlmodel import Session, select, col import re from main import app diff --git a/tests/routers/test_static_pages.py b/tests/routers/core/test_static_pages.py similarity index 95% rename from tests/routers/test_static_pages.py rename to tests/routers/core/test_static_pages.py index 0b91c6c..057431b 100644 --- a/tests/routers/test_static_pages.py +++ b/tests/routers/core/test_static_pages.py @@ -1,6 +1,6 @@ import pytest from fastapi.testclient import TestClient -from routers.static_pages import VALID_PAGES +from routers.core.static_pages import VALID_PAGES # Get valid page names from the router module valid_page_names = list(VALID_PAGES.keys()) diff --git a/tests/routers/test_user.py b/tests/routers/core/test_user.py similarity index 97% rename from tests/routers/test_user.py rename to tests/routers/core/test_user.py index 377046b..5125b1c 100644 --- a/tests/routers/test_user.py +++ b/tests/routers/core/test_user.py @@ -4,8 +4,8 @@ from unittest.mock import patch, MagicMock from tests.conftest import SetupError from main import app -from utils.models import User, Role, Organization -from utils.images import InvalidImageError +from utils.core.models import User, Role, Organization +from utils.core.images import InvalidImageError import re import pytest @@ -58,7 +58,7 @@ def test_update_profile_unauthorized(unauth_client: TestClient): assert response.headers["location"] == app.url_path_for("read_login") -@patch('routers.user.validate_and_process_image') +@patch('routers.core.user.validate_and_process_image') def test_update_profile_authorized( mock_validate: MagicMock, auth_client: TestClient, test_user: User, session: Session ): @@ -160,7 +160,7 @@ def test_delete_account_success(auth_client: TestClient, test_user: User, sessio assert user is None -@patch('routers.user.validate_and_process_image') +@patch('routers.core.user.validate_and_process_image') def test_get_avatar_authorized( mock_validate: MagicMock, auth_client: TestClient, test_user: User ): @@ -199,7 +199,7 @@ def test_get_avatar_unauthorized(unauth_client: TestClient): # Add new test for invalid image -@patch('routers.user.validate_and_process_image') +@patch('routers.core.user.validate_and_process_image') def test_update_profile_invalid_image( mock_validate: MagicMock, auth_client: TestClient ): diff --git a/tests/utils/test_auth.py b/tests/utils/test_auth.py index 67d05eb..d4aadd3 100644 --- a/tests/utils/test_auth.py +++ b/tests/utils/test_auth.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse, parse_qs from starlette.datastructures import URLPath from main import app -from utils.auth import ( +from utils.core.auth import ( create_access_token, create_refresh_token, verify_password, @@ -18,7 +18,7 @@ send_email_update_confirmation ) from unittest.mock import patch, MagicMock -from utils.models import EmailUpdateToken +from utils.core.models import EmailUpdateToken def test_convert_python_regex_to_html() -> None: diff --git a/tests/utils/test_db.py b/tests/utils/test_db.py index c593946..60bc7e7 100644 --- a/tests/utils/test_db.py +++ b/tests/utils/test_db.py @@ -1,7 +1,7 @@ from sqlmodel import Session, select, inspect from sqlalchemy import Engine -from utils.db import ( +from utils.core.db import ( get_connection_url, assign_permissions_to_role, create_default_roles, @@ -9,7 +9,7 @@ tear_down_db, set_up_db, ) -from utils.models import Role, Permission, Organization, RolePermissionLink, ValidPermissions +from utils.core.models import Role, Permission, Organization, RolePermissionLink, ValidPermissions from tests.conftest import SetupError def test_get_connection_url(): diff --git a/tests/utils/test_dependencies.py b/tests/utils/test_dependencies.py index 4e57f83..3d31c0f 100644 --- a/tests/utils/test_dependencies.py +++ b/tests/utils/test_dependencies.py @@ -1,7 +1,7 @@ from unittest.mock import MagicMock, patch from datetime import datetime, timedelta, UTC -from utils.models import EmailUpdateToken, Account, User, PasswordResetToken, Role -from utils.dependencies import ( +from utils.core.models import EmailUpdateToken, Account, User, PasswordResetToken, Role +from utils.core.dependencies import ( get_account_from_email_update_token, validate_token_and_get_account, get_account_from_credentials, get_account_from_tokens, get_authenticated_account, validate_token_and_get_user, get_user_from_tokens, get_authenticated_user, @@ -48,7 +48,7 @@ def test_validate_token_and_get_account() -> None: session.exec.return_value.first.return_value = mock_account # Test with valid access token - with patch('utils.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.validate_token') as mock_validate: mock_validate.return_value = {"sub": "test@example.com", "type": "access"} account, access_token, refresh_token = validate_token_and_get_account("valid_token", "access", session) assert account == mock_account @@ -57,9 +57,9 @@ def test_validate_token_and_get_account() -> None: mock_validate.assert_called_once_with("valid_token", token_type="access") # Test with valid refresh token - with patch('utils.dependencies.validate_token') as mock_validate: - with patch('utils.dependencies.create_access_token') as mock_access_token: - with patch('utils.dependencies.create_refresh_token') as mock_refresh_token: + with patch('utils.core.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.create_access_token') as mock_access_token: + with patch('utils.core.dependencies.create_refresh_token') as mock_refresh_token: mock_validate.return_value = {"sub": "test@example.com", "type": "refresh"} mock_access_token.return_value = "new_access_token" mock_refresh_token.return_value = "new_refresh_token" @@ -73,7 +73,7 @@ def test_validate_token_and_get_account() -> None: mock_refresh_token.assert_called_once_with(data={"sub": "test@example.com"}) # Test with invalid token - with patch('utils.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.validate_token') as mock_validate: mock_validate.return_value = None account, access_token, refresh_token = validate_token_and_get_account("invalid_token", "access", session) assert account is None @@ -81,7 +81,7 @@ def test_validate_token_and_get_account() -> None: assert refresh_token is None # Test with valid token but no account found - with patch('utils.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.validate_token') as mock_validate: mock_validate.return_value = {"sub": "nonexistent@example.com", "type": "access"} session.exec.return_value.first.return_value = None account, access_token, refresh_token = validate_token_and_get_account("valid_token", "access", session) @@ -99,7 +99,7 @@ def test_get_account_from_credentials() -> None: session.exec.return_value.first.return_value = mock_account # Test with valid credentials - with patch('utils.dependencies.verify_password') as mock_verify: + with patch('utils.core.dependencies.verify_password') as mock_verify: mock_verify.return_value = True account, returned_session = get_account_from_credentials("test@example.com", "password123", session) assert account == mock_account @@ -107,7 +107,7 @@ def test_get_account_from_credentials() -> None: mock_verify.assert_called_once_with("password123", "hashed_password") # Test with invalid password - with patch('utils.dependencies.verify_password') as mock_verify: + with patch('utils.core.dependencies.verify_password') as mock_verify: mock_verify.return_value = False with pytest.raises(CredentialsError): get_account_from_credentials("test@example.com", "wrong_password", session) @@ -125,7 +125,7 @@ def test_get_account_from_tokens() -> None: session = MagicMock() # Test with valid access token - with patch('utils.dependencies.validate_token_and_get_account') as mock_validate: + with patch('utils.core.dependencies.validate_token_and_get_account') as mock_validate: mock_account = Account(id=1, email="test@example.com") mock_validate.return_value = (mock_account, None, None) @@ -136,7 +136,7 @@ def test_get_account_from_tokens() -> None: mock_validate.assert_called_once_with("valid_access", "access", session) # Test with invalid access token but valid refresh token - with patch('utils.dependencies.validate_token_and_get_account') as mock_validate: + with patch('utils.core.dependencies.validate_token_and_get_account') as mock_validate: mock_account = Account(id=1, email="test@example.com") # First call returns None (invalid access token) # Second call returns account and new tokens (valid refresh token) @@ -152,7 +152,7 @@ def test_get_account_from_tokens() -> None: assert mock_validate.call_count == 2 # Test with both tokens invalid - with patch('utils.dependencies.validate_token_and_get_account') as mock_validate: + with patch('utils.core.dependencies.validate_token_and_get_account') as mock_validate: mock_validate.return_value = (None, None, None) account, access_token, refresh_token = get_account_from_tokens(("invalid_access", "invalid_refresh"), session) @@ -176,7 +176,7 @@ def test_get_authenticated_account() -> None: tokens = ("access_token", "refresh_token") # Test with valid account, no new tokens - with patch('utils.dependencies.get_account_from_tokens') as mock_get_account: + with patch('utils.core.dependencies.get_account_from_tokens') as mock_get_account: mock_account = Account(id=1, email="test@example.com") mock_get_account.return_value = (mock_account, None, None) @@ -184,7 +184,7 @@ def test_get_authenticated_account() -> None: assert account == mock_account # Test with valid account, new tokens needed - with patch('utils.dependencies.get_account_from_tokens') as mock_get_account: + with patch('utils.core.dependencies.get_account_from_tokens') as mock_get_account: mock_account = Account(id=1, email="test@example.com", user=User(id=1, name="Test User")) mock_get_account.return_value = (mock_account, "new_access", "new_refresh") @@ -196,7 +196,7 @@ def test_get_authenticated_account() -> None: assert exc_info.value.refresh_token == "new_refresh" # Test with no valid account - with patch('utils.dependencies.get_account_from_tokens') as mock_get_account: + with patch('utils.core.dependencies.get_account_from_tokens') as mock_get_account: mock_get_account.return_value = (None, None, None) with pytest.raises(AuthenticationError): @@ -213,7 +213,7 @@ def test_validate_token_and_get_user() -> None: session.exec.return_value.first.return_value = mock_account # Test with valid access token - with patch('utils.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.validate_token') as mock_validate: mock_validate.return_value = {"sub": "test@example.com", "type": "access"} user, access_token, refresh_token = validate_token_and_get_user("valid_token", "access", session) assert user == mock_user @@ -222,9 +222,9 @@ def test_validate_token_and_get_user() -> None: mock_validate.assert_called_once_with("valid_token", token_type="access") # Test with valid refresh token - with patch('utils.dependencies.validate_token') as mock_validate: - with patch('utils.dependencies.create_access_token') as mock_access_token: - with patch('utils.dependencies.create_refresh_token') as mock_refresh_token: + with patch('utils.core.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.create_access_token') as mock_access_token: + with patch('utils.core.dependencies.create_refresh_token') as mock_refresh_token: mock_validate.return_value = {"sub": "test@example.com", "type": "refresh"} mock_access_token.return_value = "new_access_token" mock_refresh_token.return_value = "new_refresh_token" @@ -238,7 +238,7 @@ def test_validate_token_and_get_user() -> None: mock_refresh_token.assert_called_once_with(data={"sub": "test@example.com"}) # Test with invalid token - with patch('utils.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.validate_token') as mock_validate: mock_validate.return_value = None user, access_token, refresh_token = validate_token_and_get_user("invalid_token", "access", session) assert user is None @@ -246,7 +246,7 @@ def test_validate_token_and_get_user() -> None: assert refresh_token is None # Test with valid token but no account found - with patch('utils.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.validate_token') as mock_validate: mock_validate.return_value = {"sub": "nonexistent@example.com", "type": "access"} session.exec.return_value.first.return_value = None user, access_token, refresh_token = validate_token_and_get_user("valid_token", "access", session) @@ -255,7 +255,7 @@ def test_validate_token_and_get_user() -> None: assert refresh_token is None # Test with valid token and account but no user - with patch('utils.dependencies.validate_token') as mock_validate: + with patch('utils.core.dependencies.validate_token') as mock_validate: mock_validate.return_value = {"sub": "test@example.com", "type": "access"} mock_account_no_user = Account(id=1, email="test@example.com", user=None) session.exec.return_value.first.return_value = mock_account_no_user @@ -272,7 +272,7 @@ def test_get_user_from_tokens() -> None: session = MagicMock() # Test with valid access token - with patch('utils.dependencies.validate_token_and_get_user') as mock_validate: + with patch('utils.core.dependencies.validate_token_and_get_user') as mock_validate: mock_user = User(id=1, name="Test User") mock_validate.return_value = (mock_user, None, None) @@ -283,7 +283,7 @@ def test_get_user_from_tokens() -> None: mock_validate.assert_called_once_with("valid_access", "access", session) # Test with invalid access token but valid refresh token - with patch('utils.dependencies.validate_token_and_get_user') as mock_validate: + with patch('utils.core.dependencies.validate_token_and_get_user') as mock_validate: mock_user = User(id=1, name="Test User") # First call returns None (invalid access token) # Second call returns user and new tokens (valid refresh token) @@ -299,7 +299,7 @@ def test_get_user_from_tokens() -> None: assert mock_validate.call_count == 2 # Test with both tokens invalid - with patch('utils.dependencies.validate_token_and_get_user') as mock_validate: + with patch('utils.core.dependencies.validate_token_and_get_user') as mock_validate: mock_validate.return_value = (None, None, None) user, access_token, refresh_token = get_user_from_tokens(("invalid_access", "invalid_refresh"), session) @@ -323,7 +323,7 @@ def test_get_authenticated_user() -> None: tokens = ("access_token", "refresh_token") # Test with valid user, no new tokens - with patch('utils.dependencies.get_user_from_tokens') as mock_get_user: + with patch('utils.core.dependencies.get_user_from_tokens') as mock_get_user: mock_user = User(id=1, name="Test User") mock_get_user.return_value = (mock_user, None, None) @@ -331,7 +331,7 @@ def test_get_authenticated_user() -> None: assert user == mock_user # Test with valid user, new tokens needed - with patch('utils.dependencies.get_user_from_tokens') as mock_get_user: + with patch('utils.core.dependencies.get_user_from_tokens') as mock_get_user: mock_user = User(id=1, name="Test User") mock_get_user.return_value = (mock_user, "new_access", "new_refresh") @@ -343,7 +343,7 @@ def test_get_authenticated_user() -> None: assert exc_info.value.refresh_token == "new_refresh" # Test with no valid user - with patch('utils.dependencies.get_user_from_tokens') as mock_get_user: + with patch('utils.core.dependencies.get_user_from_tokens') as mock_get_user: mock_get_user.return_value = (None, None, None) with pytest.raises(AuthenticationError): @@ -358,7 +358,7 @@ def test_get_optional_user() -> None: tokens = ("access_token", "refresh_token") # Test with valid user, no new tokens - with patch('utils.dependencies.get_user_from_tokens') as mock_get_user: + with patch('utils.core.dependencies.get_user_from_tokens') as mock_get_user: mock_user = User(id=1, name="Test User") mock_get_user.return_value = (mock_user, None, None) @@ -366,7 +366,7 @@ def test_get_optional_user() -> None: assert user == mock_user # Test with valid user, new tokens needed - with patch('utils.dependencies.get_user_from_tokens') as mock_get_user: + with patch('utils.core.dependencies.get_user_from_tokens') as mock_get_user: mock_user = User(id=1, name="Test User") mock_get_user.return_value = (mock_user, "new_access", "new_refresh") @@ -378,7 +378,7 @@ def test_get_optional_user() -> None: assert exc_info.value.refresh_token == "new_refresh" # Test with no valid user - with patch('utils.dependencies.get_user_from_tokens') as mock_get_user: + with patch('utils.core.dependencies.get_user_from_tokens') as mock_get_user: mock_get_user.return_value = (None, None, None) user = get_optional_user(tokens, session) diff --git a/tests/utils/test_images.py b/tests/utils/test_images.py index e4e80d2..3e627a6 100644 --- a/tests/utils/test_images.py +++ b/tests/utils/test_images.py @@ -1,7 +1,7 @@ import pytest from PIL import Image import io -from utils.images import ( +from utils.core.images import ( validate_and_process_image, InvalidImageError, MAX_FILE_SIZE, diff --git a/tests/utils/test_models.py b/tests/utils/test_models.py index 39a069c..886d989 100644 --- a/tests/utils/test_models.py +++ b/tests/utils/test_models.py @@ -3,7 +3,7 @@ from sqlmodel import select, Session from sqlalchemy.exc import IntegrityError import pytest -from utils.models import ( +from utils.core.models import ( Permission, Role, RolePermissionLink, diff --git a/utils/app/__init__.py b/utils/app/__init__.py new file mode 100644 index 0000000..f29e11e --- /dev/null +++ b/utils/app/__init__.py @@ -0,0 +1 @@ +"""This folder is where you would define application-specific, auth-protected utilities.""" diff --git a/utils/app/enums.py b/utils/app/enums.py new file mode 100644 index 0000000..f7cbdad --- /dev/null +++ b/utils/app/enums.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class AppPermissions(Enum): + pass diff --git a/utils/core/__init__.py b/utils/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/auth.py b/utils/core/auth.py similarity index 98% rename from utils/auth.py rename to utils/core/auth.py index c17a1ef..5ebde54 100644 --- a/utils/auth.py +++ b/utils/core/auth.py @@ -1,4 +1,4 @@ -# utils.py +# utils.core.py import os import re import jwt @@ -13,7 +13,7 @@ from jinja2.environment import Template from fastapi.templating import Jinja2Templates from fastapi import Cookie -from utils.models import PasswordResetToken, EmailUpdateToken, Account +from utils.core.models import PasswordResetToken, EmailUpdateToken, Account load_dotenv(override=True) resend.api_key = os.environ["RESEND_API_KEY"] diff --git a/utils/db.py b/utils/core/db.py similarity index 93% rename from utils/db.py rename to utils/core/db.py index 0157b50..4049442 100644 --- a/utils/db.py +++ b/utils/core/db.py @@ -1,11 +1,11 @@ import os import logging -from typing import Generator, Union, Sequence +from typing import Union, Sequence from dotenv import load_dotenv from sqlalchemy.engine import URL from sqlmodel import create_engine, Session, SQLModel, select -from utils.models import Role, Permission, RolePermissionLink -from utils.enums import ValidPermissions +from utils.core.models import Role, Permission, RolePermissionLink +from utils.core.enums import ValidPermissions # Load environment variables from a .env file load_dotenv() @@ -53,17 +53,6 @@ def get_connection_url() -> URL: engine = create_engine(get_connection_url()) -def get_session() -> Generator[Session, None, None]: - """ - Provides a database session for executing queries. - - Yields: - Session: A SQLModel session object for database operations. - """ - with Session(engine) as session: - yield session - - def assign_permissions_to_role( session: Session, role: Role, diff --git a/utils/dependencies.py b/utils/core/dependencies.py similarity index 95% rename from utils/dependencies.py rename to utils/core/dependencies.py index fbfc3b8..8ea0db3 100644 --- a/utils/dependencies.py +++ b/utils/core/dependencies.py @@ -3,17 +3,28 @@ from sqlmodel import Session, select from sqlalchemy.orm import selectinload from datetime import UTC, datetime -from typing import Optional, Tuple -from utils.auth import ( +from typing import Optional, Tuple, Generator +from utils.core.auth import ( validate_token, create_access_token, create_refresh_token, oauth2_scheme_cookie, verify_password ) -from utils.db import get_session -from utils.models import User, Role, PasswordResetToken, EmailUpdateToken, Account +from utils.core.db import engine +from utils.core.models import User, Role, PasswordResetToken, EmailUpdateToken, Account from exceptions.http_exceptions import AuthenticationError, CredentialsError, DataIntegrityError from exceptions.exceptions import NeedsNewTokens +def get_session() -> Generator[Session, None, None]: + """ + Provides a database session for executing queries. + + Yields: + Session: A SQLModel session object for database operations. + """ + with Session(engine) as session: + yield session + + def validate_token_and_get_account( token: str, token_type: str, diff --git a/utils/enums.py b/utils/core/enums.py similarity index 77% rename from utils/enums.py rename to utils/core/enums.py index 878d9ef..ffc4277 100644 --- a/utils/enums.py +++ b/utils/core/enums.py @@ -1,6 +1,6 @@ -from enum import Enum +from utils.app.enums import AppPermissions -class ValidPermissions(Enum): +class ValidPermissions(AppPermissions): DELETE_ORGANIZATION = "Delete Organization" EDIT_ORGANIZATION = "Edit Organization" INVITE_USER = "Invite User" diff --git a/utils/images.py b/utils/core/images.py similarity index 100% rename from utils/images.py rename to utils/core/images.py diff --git a/utils/invitations.py b/utils/core/invitations.py similarity index 98% rename from utils/invitations.py rename to utils/core/invitations.py index 51e53f2..1b091fd 100644 --- a/utils/invitations.py +++ b/utils/core/invitations.py @@ -6,7 +6,7 @@ from jinja2.environment import Template from fastapi.templating import Jinja2Templates -from utils.models import utc_now, Invitation, Organization, User +from utils.core.models import utc_now, Invitation, Organization, User from exceptions.exceptions import EmailSendFailedError from exceptions.http_exceptions import DataIntegrityError diff --git a/utils/models.py b/utils/core/models.py similarity index 99% rename from utils/models.py rename to utils/core/models.py index e6946e7..8708741 100644 --- a/utils/models.py +++ b/utils/core/models.py @@ -6,7 +6,7 @@ from sqlmodel import SQLModel, Field, Relationship, Session, select from sqlalchemy import Column, Enum as SQLAlchemyEnum, LargeBinary, UniqueConstraint from sqlalchemy.orm import Mapped -from utils.enums import ValidPermissions +from utils.core.enums import ValidPermissions from exceptions.http_exceptions import DataIntegrityError logger = getLogger("uvicorn.error")