Skip to content

Commit f7a48ba

Browse files
committed
First commit
1 parent f96e1dd commit f7a48ba

27 files changed

+1181
-1208
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,4 @@ cython_debug/
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
#.idea/
161+
alembic/versions/.DS_Store

README.md

Lines changed: 3 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,7 @@
1-
# Hero API 🦸‍♂️
2-
A modern, production-ready FastAPI template for building scalable APIs.
1+
# To start on your local machine
32

4-
## Features ✨
5-
- 🔄 Complete CRUD operations for heroes
6-
- 📊 Async SQLAlchemy with PostgreSQL
7-
- 🔄 Automatic Alembic migrations
8-
- 🏗️ Clean architecture with repository pattern
9-
- ⚠️ Custom exception handling
10-
- 🔍 CI and testing pipeline
11-
- 🧹 Linter setup with pre-commit hooks
12-
- 🚂 One-click Railway deployment
13-
14-
## Deploy Now! 🚀
15-
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/wbTudS?referralCode=beBXJA)
16-
17-
## Project Structure 📁
18-
```
19-
api/
20-
├── core/ # Core functionality
21-
│ ├── config.py # Environment and app configuration
22-
│ ├── database.py # Database connection and sessions
23-
│ ├── exceptions.py # Global exception handlers
24-
│ ├── logging.py # Logging configuration
25-
│ └── security.py # Authentication and security
26-
├── src/
27-
│ ├── heroes/ # Heroes module
28-
│ │ ├── models.py # Database models
29-
│ │ ├── repository.py # Data access layer
30-
│ │ ├── routes.py # API endpoints
31-
│ │ └── schemas.py # Pydantic models
32-
│ └── users/ # Users module
33-
│ ├── models.py # User models
34-
│ ├── repository.py # User data access
35-
│ ├── routes.py # User endpoints
36-
│ └── schemas.py # User schemas
37-
├── utils/ # Utility functions
38-
└── main.py # Application entry point
39-
```
40-
41-
## Requirements 📋
42-
- Python 3.8+
43-
- PostgreSQL
44-
45-
## Setup 🛠️
46-
1. Install uv (follow instructions [here](https://docs.astral.sh/uv/#getting-started))
47-
48-
2. Clone the repository:
49-
```bash
50-
git clone https://github.com/yourusername/minimalistic-fastapi-template.git
51-
cd minimalistic-fastapi-template
523
```
53-
54-
3. Install dependencies with uv:
55-
```bash
4+
cp .env.example .env # edit this file for your config
565
uv sync
57-
```
58-
59-
4. Set up environment variables:
60-
```bash
61-
cp .env.example .env
62-
# Edit .env with your database credentials
63-
```
64-
65-
> 💡 **Important**:
66-
> - The DATABASE_URL must start with `postgresql+asyncpg://` (e.g., `postgresql+asyncpg://user:pass@localhost:5432/dbname`)
67-
> - After updating environment variables, close and reopen VS Code to reload the configuration properly. VS Code will automatically activate the virtual environment when you reopen.
68-
69-
5. Start the application:
70-
71-
Using terminal:
72-
```bash
736
uv run uvicorn api.main:app
74-
```
75-
76-
Using VS Code:
77-
> 💡 If you're using VS Code, we've included run configurations in the `.vscode` folder. Just press `F5` or use the "Run and Debug" panel to start the application!
78-
79-
6. (Optional) Enable pre-commit hooks for linting:
80-
```bash
81-
uv run pre-commit install
82-
```
83-
> 💡 This will enable automatic code formatting and linting checks before each commit
84-
85-
## Creating a Migration 🔄
86-
1. Make changes to your models
87-
2. Generate migration:
88-
```bash
89-
alembic revision --autogenerate -m "your migration message"
90-
```
91-
92-
Note: Migrations will be automatically applied when you start the application - no need to run `alembic upgrade head` manually!
93-
94-
## API Endpoints 📊
95-
### Heroes
96-
- `GET /heroes` - List all heroes
97-
- `GET /heroes/{id}` - Get a specific hero
98-
- `POST /heroes` - Create a new hero
99-
- `PATCH /heroes/{id}` - Update a hero
100-
- `DELETE /heroes/{id}` - Delete a hero
101-
102-
### Authentication
103-
- `POST /auth/register` - Register a new user
104-
- `POST /auth/login` - Login and get access token
105-
- `GET /auth/me` - Get current user profile
106-
107-
## Example Usage 📝
108-
Create a new hero:
109-
```bash
110-
curl -X POST "http://localhost:8000/heroes/" -H "Content-Type: application/json" -d '{
111-
"name": "Peter Parker",
112-
"alias": "Spider-Man",
113-
"powers": "Wall-crawling, super strength, spider-sense"
114-
}'
115-
```
7+
```

alembic.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[alembic]
22
script_location = alembic
3-
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/hero_db
3+
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/tasking_manager
44

55
[loggers]
66
keys = root,sqlalchemy,alembic

alembic/versions/3dee83604016_initial_migration.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

alembic/versions/add6266277c7_.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Add workspace long form quest type and URL fields
2+
3+
Revision ID: add6266277c7
4+
Revises: c5121cbba124
5+
Create Date: 2025-09-12 04:18:05.931799
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "add6266277c7"
14+
branch_labels = None
15+
depends_on = None
16+
down_revision = None
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
with op.batch_alter_table("workspaces_long_quests", schema=None) as batch_op:
21+
batch_op.add_column(sa.Column("type", sa.Integer(), nullable=False, server_default="0"))
22+
batch_op.add_column(sa.Column("url", sa.Unicode(), nullable=True))
23+
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade():
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
with op.batch_alter_table("workspaces_long_quests", schema=None) as batch_op:
30+
batch_op.drop_column("type")
31+
batch_op.drop_column("url")
32+
33+
# ### end Alembic commands ###

alembic/versions/ef2910566747_add_users_table.py

Lines changed: 0 additions & 41 deletions
This file was deleted.

api/core/config.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@
44
class Settings(BaseSettings):
55
"""Application settings."""
66

7-
PROJECT_NAME: str = "Hero API"
8-
DATABASE_URL: str
7+
PROJECT_NAME: str = "Workspaces API"
8+
DATABASE_URL: str = "postgresql+asyncpg://user:pass@localhost:5432/dbname"
99
DEBUG: bool = False
1010

11+
WS_LONGFORM_SCHEMA_URL: str = "https://raw.githubusercontent.com/TaskarCenterAtUW/asr-imagery-list/refs/heads/main/schema/schema.json"
12+
WS_OSM_HOST: str = "https://osm.workspaces-dev.sidewalks.washington.edu"
13+
1114
# JWT Settings
12-
JWT_SECRET: str # Change in production
15+
JWT_SECRET: str = "your-secret-key"
1316
JWT_ALGORITHM: str = "HS256"
14-
JWT_EXPIRATION: int = 30 # minutes
17+
JWT_EXPIRATION: int = 24 * 60 # 1d
1518

1619
model_config = SettingsConfigDict(
1720
env_file=".env",
1821
env_file_encoding="utf-8",
1922
)
2023

21-
2224
settings = Settings()

api/core/database.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# Create declarative base for models
1313
Base = declarative_base()
1414

15-
1615
async def get_session() -> AsyncSession:
1716
"""Dependency for getting async database session.
1817

api/core/security.py

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,65 @@
1-
from datetime import datetime, timedelta
2-
31
from fastapi import Depends, HTTPException, status
4-
from fastapi.security import OAuth2PasswordBearer
2+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
3+
import httpx
4+
import json
5+
import os
56
from jose import JWTError, jwt
6-
from passlib.context import CryptContext
7-
8-
from api.core.config import settings
9-
10-
# Password hashing context
11-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
12-
13-
# OAuth2 scheme for token authentication
14-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
15-
16-
17-
def verify_password(plain_password: str, hashed_password: str) -> bool:
18-
"""Verify a password against its hash."""
19-
return pwd_context.verify(plain_password, hashed_password)
207

8+
security = HTTPBearer()
9+
class UserInfo:
10+
scheme: str
11+
credentials: str
12+
projectGroups: list[str]
2113

22-
def get_password_hash(password: str) -> str:
23-
"""Generate password hash."""
24-
return pwd_context.hash(password)
25-
26-
27-
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
28-
"""Create JWT access token."""
29-
to_encode = data.copy()
30-
expire = datetime.utcnow() + (
31-
expires_delta or timedelta(minutes=settings.JWT_EXPIRATION)
32-
)
33-
to_encode.update({"exp": expire})
34-
return jwt.encode(to_encode, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
35-
36-
37-
async def get_current_user(token: str = Depends(oauth2_scheme)):
14+
async def validate_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> UserInfo:
3815
"""Dependency to get current authenticated user."""
16+
3917
credentials_exception = HTTPException(
4018
status_code=status.HTTP_401_UNAUTHORIZED,
4119
detail="Invalid authentication credentials",
4220
headers={"WWW-Authenticate": "Bearer"},
4321
)
4422

4523
try:
46-
payload = jwt.decode(
47-
token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM]
48-
)
49-
user_id: str = payload.get("sub")
24+
# FIXME: verify signature of JWT token
25+
payload = jwt.get_unverified_claims(credentials.credentials)
26+
user_id: str | None = payload.get("sub")
5027
if user_id is None:
5128
raise credentials_exception
29+
5230
except JWTError:
5331
raise credentials_exception
5432

55-
# Import here to avoid circular imports
56-
from api.core.database import get_session
57-
from api.src.users.service import UserService
33+
async with httpx.AsyncClient() as client:
34+
headers = {
35+
'Authorization': 'Bearer ' + credentials.credentials,
36+
'Content-Type': 'application/json',
37+
}
38+
39+
authorizationUrl = os.environ.get("TM_TDEI_BACKEND_URL", "https://portal-api-dev.tdei.us/api/v1/") + "/project-group-roles/" + user_id + "?page_no=1&page_size=50"
40+
response = await client.get(authorizationUrl, headers=headers)
41+
42+
# token is not valid or server unavailable
43+
if response.status_code != 200:
44+
raise credentials_exception
45+
46+
try:
47+
content = response.read()
48+
j = json.loads(content)
49+
except json.JSONDecodeError:
50+
raise credentials_exception
51+
52+
pgs = []
53+
for i in j:
54+
pgs.append(i["tdei_project_group_id"])
55+
56+
r = UserInfo()
57+
r.scheme = credentials.scheme
58+
r.credentials = credentials.credentials
59+
r.projectGroups = pgs
60+
61+
return r
62+
63+
64+
5865

59-
async for session in get_session():
60-
user = await UserService(session).get_user(int(user_id))
61-
if user is None:
62-
raise credentials_exception
63-
return user

0 commit comments

Comments
 (0)