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
8 changes: 8 additions & 0 deletions .devcontainer/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# This file is used by ./docker-compose.yml.

TIME_ZONE=US/Eastern # Shell environment
GITHUB_TOKEN= # GitHub CLI

# Host ports
#
# For avoiding conflicts on the host machine.
Expand All @@ -16,3 +19,8 @@ HOST_NEO4J_BROWSER_PORT=7474
STRIPE_PUBLIC_KEY=pk_test_ # From Stripe dashboard
STRIPE_SECRET_KEY=sk_test_ # From Stripe dashboard
STRIPE_WEBHOOK_SECRET=whsec_ # From Stripe CLI

# Firebase config
FIREBASE_PROJECT_ID=api-python # Any project ID will work locally, but cannot be blank.
HOST_FIREBASE_AUTH_EMULATOR_PORT=9099
HOST_FIREBASE_EMULATOR_UI_PORT=4000
3 changes: 3 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ RUN VERSION=$(basename $(curl -Ls -o /dev/null -w %{url_effective} https://githu
unzip -jo nmig.zip "*/completion/neo4j-migrations_completion" -d ~/.local/share/bash-completion/completions/ && \
mv ~/.local/share/bash-completion/completions/neo4j-migrations_completion ~/.local/share/bash-completion/completions/neo4j-migrations && \
rm nmig.zip

# Install firebase-tools (The npm installer on https://containers.dev/features is extremely slow)
RUN curl -sL https://firebase.tools | bash
11 changes: 8 additions & 3 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ services:
- ${HOST_API_DEV_PORT}:8000
- ${HOST_API_DOCKER_PORT}:8001
- ${HOST_API_SAM_PORT}:8002
- ${HOST_FIREBASE_AUTH_EMULATOR_PORT}:9099
- ${HOST_FIREBASE_EMULATOR_UI_PORT}:4000
command: sleep infinity
depends_on:
- postgres
- neo4j
volumes:
- ..:/api-python:cached
environment:
# From host machine
TZ: ${TZ} # Timezone
GITHUB_TOKEN: ${GITHUB_TOKEN} # Github CLI
TZ: ${TIME_ZONE}
GITHUB_TOKEN: ${GITHUB_TOKEN}
# Python
LOGGING_LEVEL: DEBUG
# Postgres
Expand All @@ -28,6 +29,10 @@ services:
STRIPE_PUBLIC_KEY: ${STRIPE_PUBLIC_KEY}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
# Firebase
FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID}
FIREBASE_AUTH_EMULATOR_HOST: localhost:9099
VERIFY_TOKEN_SIGNATURE: 0

postgres:
image: postgres
Expand Down
7 changes: 4 additions & 3 deletions .devcontainer/host-init.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/env bash

# Copy .env.example to .env if necessary.
if [ ! -f ".env" ]; then
cp .env.example .env
fi
if [ ! -f ".devcontainer/.env" ]; then
cp .devcontainer/.env.example .devcontainer/.env
fi
if [ ! -f "scripts/admin/.env" ]; then
cp scripts/admin/.env.example scripts/admin/.env
fi

10 changes: 1 addition & 9 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,6 @@ ln -s "$(pwd)/.devcontainer/.bash_history" ~/.bash_history
# Install the user's dotfiles from GitHub.
gh repo clone dotfiles ~/.dotfiles && ~/.dotfiles/install.sh

# Copy .env.example to .env if necessary.
if [ ! -f ".env" ]; then
cp .env.example .env
fi
if [ ! -f ".devcontainer/.env" ]; then
cp .devcontainer/.env.example .devcontainer/.env
fi

# Create a virtual environment for the project if one doesn't exist.
if [ ! -d ".venv" ]; then
python3 -m venv .venv
Expand All @@ -24,5 +16,5 @@ echo "source \"$(pwd)/.venv/bin/activate\"" >> ~/.bashrc

# Add bash completion for the Stripe CLI.
mkdir -p ~/.local/share/bash-completion/completions && \
stripe completion --shell bash && \
stripe completion --shell bash > /dev/null && \
mv stripe-completion.bash ~/.local/share/bash-completion/completions/stripe
2 changes: 1 addition & 1 deletion .devcontainer/post-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

# Make sure containerd and dockerd are running.
sudo nohup containerd &
sudo nohup dockerd &
sudo nohup dockerd &
4 changes: 3 additions & 1 deletion .github/workflows/deploy_lambda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ jobs:
pip install poetry
poetry install --no-interaction
- name: Run tests
run: poetry run pytest
env:
VERIFY_TOKEN_SIGNATURE: 0
run: ./scripts/test.sh unit

- uses: aws-actions/configure-aws-credentials@v4
with:
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
- "app/**"
- ".github/workflows/test.yml"
jobs:
cargo-test:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -24,4 +24,6 @@ jobs:
pip install poetry
poetry install --no-interaction
- name: Run tests
run: poetry run pytest
env:
VERIFY_TOKEN_SIGNATURE: 0
run: ./scripts/test.sh unit
10 changes: 6 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ __pycache__
.venv
.bash_history
.Trash-*
.env
.env.*
!.env.example
**/.env
**/.env.*
!**/.env.example
.aws-sam
Pipfile*
Pipfile*
firebase_emulator_data
firebase-debug.log
32 changes: 28 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@
"commands": [
"./scripts/run.sh"
]
}
]
},
{
"splitTerminals": [
{
"name": "test unit",
"commands": [
"./scripts/test.sh unit"
]
},
{
"name": "test integration",
"commands": [
"./scripts/test.sh integration"
]
},
{
"name": "test e2e",
"commands": [
"./scripts/test.sh e2e"
]
},
]
},
Expand All @@ -51,11 +73,11 @@
{
"splitTerminals": [
{
"name": "stripe",
"name": "stripe :8000",
"commands": [
"./scripts/stripe-listen.sh"
"./scripts/stripe-listen.sh 8000"
]
},
}
]
},
{
Expand Down Expand Up @@ -88,6 +110,8 @@
"!Join sequence"
],
"python.testing.pytestArgs": [
"tests"
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
}
4 changes: 2 additions & 2 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def __post_init__(self):
self.user,
]
for service in self._services:
service._set_app(self) # type: ignore
service._app = self # type: ignore

async def shutdown(self):
async def shutdown(self) -> None:
for service in self._services:
await service.destroy()
9 changes: 5 additions & 4 deletions tests/conftest.py → app/app_e2e_test.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import pytest

from app.app import App
from app.auth.repo_firebase import AuthRepoFirebase
from app.auth.service import AuthService
from app.subscription.service_stripe import SubscriptionServiceStripe
from app.subscription_portal.service_stripe import SubscriptionPortalServiceStripe
from app.user.repo_in_mem import UserRepoInMem
from app.user.service import UserService


@pytest.fixture
def app():
from app.app import App

return App(
auth=AuthService(),
auth=AuthService(repo=AuthRepoFirebase()),
subscription=SubscriptionServiceStripe(),
subscription_portal=SubscriptionPortalServiceStripe(),
user=UserService(),
user=UserService(repo=UserRepoInMem()),
)
58 changes: 54 additions & 4 deletions app/auth/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,56 @@
from pydantic import BaseModel, EmailStr
from http import HTTPStatus
from typing import Literal

from app.error import AppError
from app.subscription.models import SubscriptionLevel
from app.user.models import User

class SignUpData(BaseModel):
email: EmailStr
password: str
type Role = Literal["admin"]
__all__ = ["Role"]


class AuthUser(User):
email_verified: bool = False
password: str | None = None
disabled: bool = False
role: Role | None = None
level: SubscriptionLevel | None = None

def is_verified(self) -> bool:
return self.email_verified or (self.phone != None and len(self.phone) > 0)

def is_admin(self) -> bool:
return self.role == "admin"

def is_subscribed(self) -> bool:
return self.level is not None


class InvalidTokenError(AppError):
def __init__(self):
super().__init__(code="auth/invalid-token", status=HTTPStatus.UNAUTHORIZED)


class UnauthorizedError(AppError):
def __init__(self):
super().__init__(code="auth/unauthorized", status=HTTPStatus.UNAUTHORIZED)


class AuthUserNotFoundError(AppError):
def __init__(self):
super().__init__(code="auth/user-not-found", status=HTTPStatus.NOT_FOUND)


class AuthUserDisabledError(AppError):
def __init__(self):
super().__init__(code="auth/user-disabled", status=HTTPStatus.FORBIDDEN)


class AuthUserAlreadyExistsError(AppError):
def __init__(self):
super().__init__(code="auth/user-already-exists", status=HTTPStatus.BAD_REQUEST)


class AuthInvalidUpdateError(AppError):
def __init__(self):
super().__init__(code="auth/invalid-update", status=HTTPStatus.BAD_REQUEST)
33 changes: 33 additions & 0 deletions app/auth/repo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from abc import ABC, abstractmethod
from typing import Any

from app.auth.models import AuthUser
from app.user.models import UserId


class AuthRepo(ABC):
@abstractmethod
async def create_user(self, user: AuthUser) -> None:
raise NotImplementedError()

@abstractmethod
async def get_user_by_id(self, id: UserId) -> AuthUser:
raise NotImplementedError()

@abstractmethod
async def update_user(self, id: UserId, data: dict[str, Any]) -> None:
"""
Update the properties on a user.

Only the properties that are provided will be changed.
To remove a property, set its value to None.
"""
raise NotImplementedError()

@abstractmethod
async def delete_user(self, id: UserId) -> None:
raise NotImplementedError()

@abstractmethod
async def is_only_user(self, id: UserId) -> bool:
raise NotImplementedError()
Loading
Loading