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
22 changes: 0 additions & 22 deletions Makefile

This file was deleted.

10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ Further improvements are planned to make this a cloud-native application.

# Local development

For automated recipes, this project uses `Just`. Ensure you have it installed.

To start the backend, frontend and supabase, issue:

```
make dev
just dev
```

To stop these services, do:

```
make stop
just stop
```

# Docs
Expand All @@ -51,11 +53,11 @@ $ uv pip install mkdocs
Lastly, build the documentation:

```bash
$ make docs-build
$ just docs-build
```

This will output a `site/` directory containing the static site. To serve the website in localhost:

```bash
$ make docs-serve
$ just docs-serve
```
4 changes: 2 additions & 2 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SUPABASE_URL=http://127.0.0.1:54321
# Required only while supporting legacy HS256 Supabase tokens.
SUPABASE_JWT_SECRET=jwt_secret_here
DATABASE_URL=postgresql+asyncpg://postgres:postgres@127.0.0.1:54322/postgres
ENVIRONMENT=development
CORS_ORIGINS=https://app.example.com,https://admin.example.com
21 changes: 7 additions & 14 deletions backend/app/auth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,18 @@ def verify_jwt_token(
token: str, # self contained token that already has info like id, email, etc
settings: Settings,
) -> AuthUser:
"""Verify either legacy HS256 or current Supabase ES256 access tokens."""
"""Verify Supabase ES256 access tokens."""
try:
algorithm = jwt.get_unverified_header(token).get("alg")

if algorithm == "HS256":
key = settings.SUPABASE_JWT_SECRET.get_secret_value()
elif algorithm == "ES256":
key = (
get_jwk_client(str(settings.SUPABASE_URL))
.get_signing_key_from_jwt(token)
.key
)
else:
raise TokenInvalidError("Unsupported access token algorithm")
key = (
get_jwk_client(str(settings.SUPABASE_URL))
.get_signing_key_from_jwt(token)
.key
)

payload = jwt.decode(
token,
key,
algorithms=[algorithm],
algorithms=["ES256"],
audience="authenticated",
)

Expand Down
16 changes: 13 additions & 3 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@

class Settings(BaseSettings):
# app name
APP_NAME: str = "Meu App FastAPI"
APP_NAME: str = "WebMUN API"
ENVIRONMENT: str = "development"

# list of origins by separated commas (e.g., "app.com,app.xyz")
CORS_ORIGINS: str = ""

# db config
DATABASE_URL: SecretStr

# supabase config
SUPABASE_URL: AnyHttpUrl
SUPABASE_JWT_SECRET: SecretStr
JWT_ALGORITHM: str = "HS256"

@property
def list_cors_origins(self) -> list[str]:
origins = [origin.strip() for origin in self.CORS_ORIGINS.split(",")]

if self.ENVIRONMENT == "development":
return ["http://localhost:5173"]

return origins

# Host development uses backend/.env; containers and cloud inject process env.
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
Expand Down
17 changes: 12 additions & 5 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@
from app.session.manager import ConnectionManager
from app.session.views import router as session_router


# Startup and shutdown logic for shared variables, such as
# (db session, settings, connection manager, etc)
# You can view more of this on "FastAPI Lifespan"

settings = get_settings()


@asynccontextmanager
async def lifespan(app: FastAPI):
# startup phase
settings = get_settings()
engine, session_factory = create_db(settings)
app.state.db_engine = engine
app.state.db_session_factory = session_factory
Expand All @@ -48,9 +50,14 @@ async def app_exception_handler(request: Request, exc: AppException):
# CORS config for Vite
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
allow_origins=settings.list_cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
if settings.ENVIRONMENT == "production"
else ["*"],
allow_headers=["Authorization", "Content-Type", "Accept"]
if settings.ENVIRONMENT == "production"
else ["*"],
)

# include commitees here?
Expand Down
35 changes: 19 additions & 16 deletions backend/app/tests/auth/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,46 @@

import jwt
import pytest
from cryptography.hazmat.primitives.asymmetric import ec

import app.auth.service as auth_service
from app.auth.service import TokenExpiredError, TokenInvalidError, verify_jwt_token


class FakeSecret:
def __init__(self, value: str):
self.value = value

def get_secret_value(self) -> str:
return self.value
@pytest.fixture
def signing_key(monkeypatch):
private_key = ec.generate_private_key(ec.SECP256R1())
jwk_client = SimpleNamespace(
get_signing_key_from_jwt=lambda _: SimpleNamespace(key=private_key.public_key())
)
monkeypatch.setattr(auth_service, "get_jwk_client", lambda _: jwk_client)
return private_key


@pytest.fixture
def settings():
return SimpleNamespace(
SUPABASE_JWT_SECRET=FakeSecret("test-secret-that-is-at-least-32-bytes"),
JWT_ALGORITHM="HS256",
)
return SimpleNamespace(SUPABASE_URL="https://supabase.example.test")


def make_token(*, user_id, expires_at: datetime) -> str:
def make_token(*, user_id, expires_at: datetime, signing_key) -> str:
return jwt.encode(
{
"sub": str(user_id),
"email": "delegate@example.test",
"aud": "authenticated",
"exp": expires_at,
},
"test-secret-that-is-at-least-32-bytes",
algorithm="HS256",
signing_key,
algorithm="ES256",
)


def test_verifies_valid_supabase_style_token(settings):
def test_verifies_valid_supabase_style_token(settings, signing_key):
user_id = uuid4()
token = make_token(
user_id=user_id,
expires_at=datetime.now(UTC) + timedelta(minutes=5),
signing_key=signing_key,
)

user = verify_jwt_token(token, settings)
Expand All @@ -50,16 +52,17 @@ def test_verifies_valid_supabase_style_token(settings):
assert user.email == "delegate@example.test"


def test_rejects_expired_token(settings):
def test_rejects_expired_token(settings, signing_key):
token = make_token(
user_id=uuid4(),
expires_at=datetime.now(UTC) - timedelta(minutes=1),
signing_key=signing_key,
)

with pytest.raises(TokenExpiredError):
verify_jwt_token(token, settings)


def test_rejects_invalid_token(settings):
def test_rejects_invalid_token(settings, signing_key):
with pytest.raises(TokenInvalidError):
verify_jwt_token("not-a-jwt", settings)
9 changes: 9 additions & 0 deletions backend/app/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
# Environment configuration file for testing

import os
from datetime import datetime
from uuid import UUID

import pytest

# `app.main` builds middleware at import time, which loads Settings before the
# OpenAPI test can run. These are non-secret placeholders; database access is
# mocked in unit tests and no application lifespan is started by that test.
os.environ.setdefault(
"DATABASE_URL", "postgresql+asyncpg://postgres:postgres@localhost:5432/webmun_test"
)
os.environ.setdefault("SUPABASE_URL", "https://supabase.example.test")

from app.session.engine import SessionEngine
from app.session.enums import (
SessionRole,
Expand Down
17 changes: 13 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ services:
build:
context: ./frontend
dockerfile: Dockerfile
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
env_file:
- ./frontend/.env.local
network_mode: host
environment:
OPENAPI_URL: http://backend:8000/openapi.json
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./frontend:/app
- packages:/app/node_modules
ports:
- "5173:5173"
depends_on:
backend:
condition: service_healthy
Expand Down Expand Up @@ -40,8 +44,13 @@ services:
dockerfile: Dockerfile
env_file:
- ./backend/.env
# Supabase CLI binds locally to the host. This local stack targets Linux.
network_mode: host
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "8000:8000"
environment:
- SUPABASE_URL=http://host.docker.internal:54321
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@host.docker.internal:54322/postgres
volumes:
- ./backend:/app
- backend_packages:/app/.venv
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/schemas/types.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

export type ClientOptions = {
baseUrl: 'http://localhost:8000' | (string & {});
baseUrl: 'http://backend:8000' | (string & {});

Check warning on line 4 in frontend/src/schemas/types.gen.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using http protocol is insecure. Use https instead.

See more on https://sonarcloud.io/project/issues?id=uspcodelab_webmun&issues=AaBTebAW3f23caWm5C5g&open=AaBTebAW3f23caWm5C5g&pullRequest=104
};

/**
Expand Down
27 changes: 27 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
set default-list := true

MKDOCS := "mkdocs"

# start the dev environment
dev:
echo "Starting dev environment"
supabase start || npx supabase start
docker compose up --watch

# stop dev environment
stop:
echo "Stopping dev environment"
docker compose down
supabase stop || npx supabase stop

# serve documentation using mkdocs
docs-serve:
{{MKDOCS}} serve --config-file mkdocs.yml

# build documentation using mkdocs
docs-build:
{{MKDOCS}} build --config-file mkdocs.yml

# cleanup documentation
docs-clean:
rm -rf site/
Loading