Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

- name: Run tests
run: docker compose build
- name: Docker Compose remove old containers and volumes
- name: Docker Compose remove old containers and volumes
run: docker compose down -v --remove-orphans
- name: Docker Compose up
run: docker compose up -d
Expand Down
2 changes: 1 addition & 1 deletion src/backend/app/alembic/README
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Generic single-database configuration.
Generic single-database configuration.
8 changes: 4 additions & 4 deletions src/backend/app/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from __future__ import with_statement

import os
from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
Expand Down Expand Up @@ -69,7 +67,9 @@ def run_migrations_online():
configuration = config.get_section(config.config_ini_section)
configuration["sqlalchemy.url"] = get_url()
connectable = engine_from_config(
configuration, prefix="sqlalchemy.", poolclass=pool.NullPool,
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
Expand Down
52 changes: 29 additions & 23 deletions src/backend/app/alembic/versions/e2412789c190_initialize_models.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,54 @@
"""Initialize models

Revision ID: e2412789c190
Revises:
Revises:
Create Date: 2023-11-24 22:55:43.195942

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes

from alembic import op

# revision identifiers, used by Alembic.
revision = 'e2412789c190'
revision = "e2412789c190"
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('is_superuser', sa.Boolean(), nullable=False),
sa.Column('full_name', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('hashed_password', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id')
op.create_table(
"user",
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("is_superuser", sa.Boolean(), nullable=False),
sa.Column("full_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
op.create_table('item',
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('owner_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
op.create_table(
"item",
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("owner_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["owner_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('item')
op.drop_index(op.f('ix_user_email'), table_name='user')
op.drop_table('user')
op.drop_table("item")
op.drop_index(op.f("ix_user_email"), table_name="user")
op.drop_table("user")
# ### end Alembic commands ###
6 changes: 3 additions & 3 deletions src/backend/app/api/api_v1/endpoints/items.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Any

from fastapi import APIRouter, HTTPException
from sqlmodel import select, func
from sqlmodel import func, select

from app.api.deps import CurrentUser, SessionDep
from app.models import Item, ItemCreate, ItemOut, ItemUpdate, Message, ItemsOut
from app.models import Item, ItemCreate, ItemOut, ItemsOut, ItemUpdate, Message

router = APIRouter()

Expand All @@ -22,7 +22,7 @@ def read_items(

if current_user.is_superuser:
statement = select(Item).offset(skip).limit(limit)
items = session.exec(statement).all()
items = session.exec(statement).all()
else:
statement = (
select(Item)
Expand Down
8 changes: 3 additions & 5 deletions src/backend/app/api/api_v1/endpoints/users.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Any, List
from typing import Any

from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import select, func
from sqlmodel import func, select

from app import crud
from app.api.deps import (
Expand All @@ -28,9 +28,7 @@


@router.get(
"/",
dependencies=[Depends(get_current_active_superuser)],
response_model=UsersOut
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UsersOut
)
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
"""
Expand Down
3 changes: 2 additions & 1 deletion src/backend/app/api/deps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Annotated, Generator
from collections.abc import Generator
from typing import Annotated

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
Expand Down
32 changes: 16 additions & 16 deletions src/backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import secrets
from typing import Any, Dict, List, Optional, Union
from typing import Any

from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator

Expand All @@ -14,21 +14,21 @@ class Settings(BaseSettings):
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
# e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
BACKEND_CORS_ORIGINS: list[AnyHttpUrl] = []

@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
elif isinstance(v, list | str):
return v
raise ValueError(v)

PROJECT_NAME: str
SENTRY_DSN: Optional[HttpUrl] = None
SENTRY_DSN: HttpUrl | None = None

@validator("SENTRY_DSN", pre=True)
def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]:
def sentry_dsn_can_be_blank(cls, v: str) -> str | None:
if len(v) == 0:
return None
return v
Expand All @@ -37,10 +37,10 @@ def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]:
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None

@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
def assemble_db_connection(cls, v: str | None, values: dict[str, Any]) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
Expand All @@ -52,15 +52,15 @@ def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any
)

SMTP_TLS: bool = True
SMTP_PORT: Optional[int] = None
SMTP_HOST: Optional[str] = None
SMTP_USER: Optional[str] = None
SMTP_PASSWORD: Optional[str] = None
EMAILS_FROM_EMAIL: Optional[EmailStr] = None
EMAILS_FROM_NAME: Optional[str] = None
SMTP_PORT: int | None = None
SMTP_HOST: str | None = None
SMTP_USER: str | None = None
SMTP_PASSWORD: str | None = None
EMAILS_FROM_EMAIL: EmailStr | None = None
EMAILS_FROM_NAME: str | None = None

@validator("EMAILS_FROM_NAME")
def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str:
def get_project_name(cls, v: str | None, values: dict[str, Any]) -> str:
if not v:
return values["PROJECT_NAME"]
return v
Expand All @@ -70,7 +70,7 @@ def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str:
EMAILS_ENABLED: bool = False

@validator("EMAILS_ENABLED", pre=True)
def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool:
def get_emails_enabled(cls, v: bool, values: dict[str, Any]) -> bool:
return bool(
values.get("SMTP_HOST")
and values.get("SMTP_PORT")
Expand Down
6 changes: 2 additions & 4 deletions src/backend/app/core/security.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime, timedelta
from typing import Any, Union
from typing import Any

from jose import jwt
from passlib.context import CryptContext
Expand All @@ -12,9 +12,7 @@
ALGORITHM = "HS256"


def create_access_token(
subject: Union[str, Any], expires_delta: timedelta = None
) -> str:
def create_access_token(subject: str | Any, expires_delta: timedelta = None) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
Expand Down
19 changes: 9 additions & 10 deletions src/backend/app/crud/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
from .crud_item import item
from .crud_user import user

# For a new basic set of CRUD operations you could just do

# from .base import CRUDBase
# from app.models.item import Item
# from app.schemas.item import ItemCreate, ItemUpdate

# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item)
from sqlmodel import Session, select

from app.core.security import get_password_hash, verify_password
from app.models import UserCreate, User
from app.models import User, UserCreate

from .crud_item import item as item
from .crud_user import user as user


def create_user(*, session: Session, user_create: UserCreate) -> User:
Expand All @@ -30,9 +29,9 @@ def get_user_by_email(*, session: Session, email: str) -> User | None:


def authenticate(*, session: Session, email: str, password: str) -> User | None:
user = get_user_by_email(session=session, email=email)
if not user:
db_user = get_user_by_email(session=session, email=email)
if not db_user:
return None
if not verify_password(password, user.hashed_password):
if not verify_password(password, db_user.hashed_password):
return None
return user
return db_user
8 changes: 4 additions & 4 deletions src/backend/app/crud/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from typing import Any, Generic, TypeVar

from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
Expand All @@ -10,7 +10,7 @@


class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
def __init__(self, model: type[ModelType]):
"""
CRUD object with default methods to Create, Read, Update, Delete (CRUD).

Expand All @@ -21,7 +21,7 @@ def __init__(self, model: Type[ModelType]):
"""
self.model = model

def get(self, db: Session, id: Any) -> Optional[ModelType]:
def get(self, db: Session, id: Any) -> ModelType | None:
return db.query(self.model).filter(self.model.id == id).first()

def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
Expand All @@ -37,7 +37,7 @@ def update(
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
obj_in: UpdateSchemaType | dict[str, Any],
) -> ModelType:
obj_data = jsonable_encoder(db_obj)
if isinstance(obj_in, dict):
Expand Down
4 changes: 1 addition & 3 deletions src/backend/app/crud/crud_item.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import List

from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session

Expand All @@ -21,7 +19,7 @@ def create_with_owner(

def get_multi_by_owner(
self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100
) -> List[Item]:
) -> list[Item]:
return (
db.query(self.model)
.filter(Item.owner_id == owner_id)
Expand Down
8 changes: 4 additions & 4 deletions src/backend/app/crud/crud_user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, Union
from typing import Any

from sqlalchemy.orm import Session

Expand All @@ -9,7 +9,7 @@


class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
def get_by_email(self, db: Session, *, email: str) -> User | None:
return db.query(User).filter(User.email == email).first()

def create(self, db: Session, *, obj_in: UserCreate) -> User:
Expand All @@ -25,7 +25,7 @@ def create(self, db: Session, *, obj_in: UserCreate) -> User:
return db_obj

def update(
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
self, db: Session, *, db_obj: User, obj_in: UserUpdate | dict[str, Any]
) -> User:
if isinstance(obj_in, dict):
update_data = obj_in
Expand All @@ -37,7 +37,7 @@ def update(
update_data["hashed_password"] = hashed_password
return super().update(db, db_obj=db_obj, obj_in=update_data)

def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]:
def authenticate(self, db: Session, *, email: str, password: str) -> User | None:
user = self.get_by_email(db, email=email)
if not user:
return None
Expand Down
2 changes: 1 addition & 1 deletion src/backend/app/email-templates/build/new_account.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }} - New Account</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">You have a new account:</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Username: {{ username }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Password: {{ password }}</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:50px 0px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;padding:10px 25px;background:#414141;" valign="middle"><a href="{{ link }}" style="background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Go to Dashboard</a></td></tr></table></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
Loading