Now we have an API
This commit is contained in:
@@ -16,4 +16,7 @@ MYSQL_PORT=3306
|
|||||||
MYSQL_DATABASE=maillist
|
MYSQL_DATABASE=maillist
|
||||||
MYSQL_USER=maillist
|
MYSQL_USER=maillist
|
||||||
MYSQL_PASSWORD=change_this_password
|
MYSQL_PASSWORD=change_this_password
|
||||||
MYSQL_ROOT_PASSWORD=change_this_root_password
|
MYSQL_ROOT_PASSWORD=change_this_root_password
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
API_TOKEN=change_this_to_a_secure_random_token
|
||||||
16
api/Dockerfile
Normal file
16
api/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
222
api/README.md
Normal file
222
api/README.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# Mailing List API Documentation
|
||||||
|
|
||||||
|
REST API for managing mailing lists and members with token-based authentication.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All endpoints (except `/` and `/health`) require Bearer token authentication.
|
||||||
|
|
||||||
|
Add the token to request headers:
|
||||||
|
```
|
||||||
|
Authorization: Bearer your_api_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
Set your API token in `.env`:
|
||||||
|
```
|
||||||
|
API_TOKEN=your_secure_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
**GET** `/health`
|
||||||
|
- No authentication required
|
||||||
|
- Returns API and database status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mailing Lists
|
||||||
|
|
||||||
|
**GET** `/lists`
|
||||||
|
- Get all mailing lists
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer your_token" http://localhost:8000/lists
|
||||||
|
```
|
||||||
|
|
||||||
|
**GET** `/lists/{list_id}`
|
||||||
|
- Get a specific mailing list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer your_token" http://localhost:8000/lists/1
|
||||||
|
```
|
||||||
|
|
||||||
|
**POST** `/lists`
|
||||||
|
- Create a new mailing list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/lists \
|
||||||
|
-H "Authorization: Bearer your_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"list_name": "Developers",
|
||||||
|
"list_email": "dev@lists.sasalliance.org",
|
||||||
|
"description": "Developer discussions",
|
||||||
|
"active": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**PATCH** `/lists/{list_id}`
|
||||||
|
- Update a mailing list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PATCH http://localhost:8000/lists/1 \
|
||||||
|
-H "Authorization: Bearer your_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"description": "Updated description",
|
||||||
|
"active": false
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**DELETE** `/lists/{list_id}`
|
||||||
|
- Delete a mailing list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE http://localhost:8000/lists/1 \
|
||||||
|
-H "Authorization: Bearer your_token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Members
|
||||||
|
|
||||||
|
**GET** `/members`
|
||||||
|
- Get all members
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer your_token" http://localhost:8000/members
|
||||||
|
```
|
||||||
|
|
||||||
|
**GET** `/members/{member_id}`
|
||||||
|
- Get a specific member
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer your_token" http://localhost:8000/members/1
|
||||||
|
```
|
||||||
|
|
||||||
|
**POST** `/members`
|
||||||
|
- Create a new member
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/members \
|
||||||
|
-H "Authorization: Bearer your_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john.doe@example.com",
|
||||||
|
"active": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**PATCH** `/members/{member_id}`
|
||||||
|
- Update a member
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PATCH http://localhost:8000/members/1 \
|
||||||
|
-H "Authorization: Bearer your_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "John Q. Doe",
|
||||||
|
"active": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**DELETE** `/members/{member_id}`
|
||||||
|
- Delete a member
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE http://localhost:8000/members/1 \
|
||||||
|
-H "Authorization: Bearer your_token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscriptions
|
||||||
|
|
||||||
|
**GET** `/lists/{list_id}/members`
|
||||||
|
- Get all members of a specific list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer your_token" http://localhost:8000/lists/1/members
|
||||||
|
```
|
||||||
|
|
||||||
|
**POST** `/subscriptions`
|
||||||
|
- Subscribe a member to a list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/subscriptions \
|
||||||
|
-H "Authorization: Bearer your_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"list_email": "community@lists.sasalliance.org",
|
||||||
|
"member_email": "john.doe@example.com",
|
||||||
|
"active": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**DELETE** `/subscriptions?list_email=X&member_email=Y`
|
||||||
|
- Unsubscribe a member from a list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE "http://localhost:8000/subscriptions?list_email=community@lists.sasalliance.org&member_email=john.doe@example.com" \
|
||||||
|
-H "Authorization: Bearer your_token"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interactive Documentation
|
||||||
|
|
||||||
|
Once the API is running, visit:
|
||||||
|
|
||||||
|
- **Swagger UI**: http://localhost:8000/docs
|
||||||
|
- **ReDoc**: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
These provide interactive API documentation where you can test endpoints directly in your browser.
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
1. **Create a new member:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/members \
|
||||||
|
-H "Authorization: Bearer your_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name": "Jane Smith", "email": "jane@example.com", "active": true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create a new mailing list:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/lists \
|
||||||
|
-H "Authorization: Bearer your_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"list_name": "Marketing", "list_email": "marketing@lists.sasalliance.org", "active": true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Subscribe the member to the list:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/subscriptions \
|
||||||
|
-H "Authorization: Bearer your_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"list_email": "marketing@lists.sasalliance.org", "member_email": "jane@example.com"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify the subscription:**
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer your_token" http://localhost:8000/lists/1/members
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
- `401 Unauthorized` - Invalid or missing authentication token
|
||||||
|
- `404 Not Found` - Resource not found
|
||||||
|
- `400 Bad Request` - Invalid request data
|
||||||
|
- `500 Internal Server Error` - Database or server error
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All changes take effect immediately in Postfix (no reload needed)
|
||||||
|
- Email validation is enforced on all email fields
|
||||||
|
- Deleting a list or member also removes associated subscriptions (CASCADE)
|
||||||
360
api/main.py
Normal file
360
api/main.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
"""
|
||||||
|
Mailing List Management API
|
||||||
|
FastAPI-based REST API for managing mailing lists and members
|
||||||
|
"""
|
||||||
|
from fastapi import FastAPI, HTTPException, Depends, Header
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from typing import List, Optional
|
||||||
|
import mysql.connector
|
||||||
|
from mysql.connector import Error
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
API_TOKEN = os.getenv('API_TOKEN', 'change-this-token')
|
||||||
|
MYSQL_HOST = os.getenv('MYSQL_HOST', 'mysql')
|
||||||
|
MYSQL_PORT = int(os.getenv('MYSQL_PORT', 3306))
|
||||||
|
MYSQL_DATABASE = os.getenv('MYSQL_DATABASE', 'maillist')
|
||||||
|
MYSQL_USER = os.getenv('MYSQL_USER', 'maillist')
|
||||||
|
MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD', '')
|
||||||
|
|
||||||
|
# FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title="Mailing List Manager API",
|
||||||
|
description="API for managing mailing lists and members",
|
||||||
|
version="1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
@contextmanager
|
||||||
|
def get_db():
|
||||||
|
"""Database connection context manager"""
|
||||||
|
connection = None
|
||||||
|
try:
|
||||||
|
connection = mysql.connector.connect(
|
||||||
|
host=MYSQL_HOST,
|
||||||
|
port=MYSQL_PORT,
|
||||||
|
database=MYSQL_DATABASE,
|
||||||
|
user=MYSQL_USER,
|
||||||
|
password=MYSQL_PASSWORD
|
||||||
|
)
|
||||||
|
yield connection
|
||||||
|
except Error as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||||
|
finally:
|
||||||
|
if connection and connection.is_connected():
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||||
|
"""Verify API token"""
|
||||||
|
if credentials.credentials != API_TOKEN:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
||||||
|
return credentials.credentials
|
||||||
|
|
||||||
|
# Pydantic models
|
||||||
|
class MailingList(BaseModel):
|
||||||
|
list_id: Optional[int] = None
|
||||||
|
list_name: str
|
||||||
|
list_email: EmailStr
|
||||||
|
description: Optional[str] = None
|
||||||
|
active: bool = True
|
||||||
|
|
||||||
|
class MailingListUpdate(BaseModel):
|
||||||
|
list_name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
active: Optional[bool] = None
|
||||||
|
|
||||||
|
class Member(BaseModel):
|
||||||
|
member_id: Optional[int] = None
|
||||||
|
name: str
|
||||||
|
email: EmailStr
|
||||||
|
active: bool = True
|
||||||
|
|
||||||
|
class MemberUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
active: Optional[bool] = None
|
||||||
|
|
||||||
|
class Subscription(BaseModel):
|
||||||
|
list_email: EmailStr
|
||||||
|
member_email: EmailStr
|
||||||
|
active: bool = True
|
||||||
|
|
||||||
|
# Routes
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""API information"""
|
||||||
|
return {
|
||||||
|
"name": "Mailing List Manager API",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"status": "running"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
try:
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT 1")
|
||||||
|
cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
return {"status": "healthy", "database": "connected"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=503, detail=f"Unhealthy: {str(e)}")
|
||||||
|
|
||||||
|
# Mailing Lists endpoints
|
||||||
|
@app.get("/lists", response_model=List[MailingList])
|
||||||
|
async def get_lists(token: str = Depends(verify_token)):
|
||||||
|
"""Get all mailing lists"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT * FROM lists ORDER BY list_name")
|
||||||
|
lists = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
return lists
|
||||||
|
|
||||||
|
@app.get("/lists/{list_id}", response_model=MailingList)
|
||||||
|
async def get_list(list_id: int, token: str = Depends(verify_token)):
|
||||||
|
"""Get a specific mailing list"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT * FROM lists WHERE list_id = %s", (list_id,))
|
||||||
|
mailing_list = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if not mailing_list:
|
||||||
|
raise HTTPException(status_code=404, detail="List not found")
|
||||||
|
return mailing_list
|
||||||
|
|
||||||
|
@app.post("/lists", response_model=MailingList, status_code=201)
|
||||||
|
async def create_list(mailing_list: MailingList, token: str = Depends(verify_token)):
|
||||||
|
"""Create a new mailing list"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO lists (list_name, list_email, description, active) VALUES (%s, %s, %s, %s)",
|
||||||
|
(mailing_list.list_name, mailing_list.list_email, mailing_list.description, mailing_list.active)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
mailing_list.list_id = cursor.lastrowid
|
||||||
|
cursor.close()
|
||||||
|
return mailing_list
|
||||||
|
except Error as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Failed to create list: {str(e)}")
|
||||||
|
|
||||||
|
@app.patch("/lists/{list_id}", response_model=MailingList)
|
||||||
|
async def update_list(list_id: int, updates: MailingListUpdate, token: str = Depends(verify_token)):
|
||||||
|
"""Update a mailing list"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# Build update query dynamically
|
||||||
|
update_fields = []
|
||||||
|
values = []
|
||||||
|
|
||||||
|
if updates.list_name is not None:
|
||||||
|
update_fields.append("list_name = %s")
|
||||||
|
values.append(updates.list_name)
|
||||||
|
if updates.description is not None:
|
||||||
|
update_fields.append("description = %s")
|
||||||
|
values.append(updates.description)
|
||||||
|
if updates.active is not None:
|
||||||
|
update_fields.append("active = %s")
|
||||||
|
values.append(updates.active)
|
||||||
|
|
||||||
|
if not update_fields:
|
||||||
|
raise HTTPException(status_code=400, detail="No fields to update")
|
||||||
|
|
||||||
|
values.append(list_id)
|
||||||
|
query = f"UPDATE lists SET {', '.join(update_fields)} WHERE list_id = %s"
|
||||||
|
|
||||||
|
cursor.execute(query, values)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Return updated list
|
||||||
|
cursor.execute("SELECT * FROM lists WHERE list_id = %s", (list_id,))
|
||||||
|
updated_list = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if not updated_list:
|
||||||
|
raise HTTPException(status_code=404, detail="List not found")
|
||||||
|
return updated_list
|
||||||
|
|
||||||
|
@app.delete("/lists/{list_id}", status_code=204)
|
||||||
|
async def delete_list(list_id: int, token: str = Depends(verify_token)):
|
||||||
|
"""Delete a mailing list"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("DELETE FROM lists WHERE list_id = %s", (list_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="List not found")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
# Members endpoints
|
||||||
|
@app.get("/members", response_model=List[Member])
|
||||||
|
async def get_members(token: str = Depends(verify_token)):
|
||||||
|
"""Get all members"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT * FROM members ORDER BY name")
|
||||||
|
members = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
return members
|
||||||
|
|
||||||
|
@app.get("/members/{member_id}", response_model=Member)
|
||||||
|
async def get_member(member_id: int, token: str = Depends(verify_token)):
|
||||||
|
"""Get a specific member"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT * FROM members WHERE member_id = %s", (member_id,))
|
||||||
|
member = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if not member:
|
||||||
|
raise HTTPException(status_code=404, detail="Member not found")
|
||||||
|
return member
|
||||||
|
|
||||||
|
@app.post("/members", response_model=Member, status_code=201)
|
||||||
|
async def create_member(member: Member, token: str = Depends(verify_token)):
|
||||||
|
"""Create a new member"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO members (name, email, active) VALUES (%s, %s, %s)",
|
||||||
|
(member.name, member.email, member.active)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
member.member_id = cursor.lastrowid
|
||||||
|
cursor.close()
|
||||||
|
return member
|
||||||
|
except Error as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Failed to create member: {str(e)}")
|
||||||
|
|
||||||
|
@app.patch("/members/{member_id}", response_model=Member)
|
||||||
|
async def update_member(member_id: int, updates: MemberUpdate, token: str = Depends(verify_token)):
|
||||||
|
"""Update a member"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
update_fields = []
|
||||||
|
values = []
|
||||||
|
|
||||||
|
if updates.name is not None:
|
||||||
|
update_fields.append("name = %s")
|
||||||
|
values.append(updates.name)
|
||||||
|
if updates.active is not None:
|
||||||
|
update_fields.append("active = %s")
|
||||||
|
values.append(updates.active)
|
||||||
|
|
||||||
|
if not update_fields:
|
||||||
|
raise HTTPException(status_code=400, detail="No fields to update")
|
||||||
|
|
||||||
|
values.append(member_id)
|
||||||
|
query = f"UPDATE members SET {', '.join(update_fields)} WHERE member_id = %s"
|
||||||
|
|
||||||
|
cursor.execute(query, values)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM members WHERE member_id = %s", (member_id,))
|
||||||
|
updated_member = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if not updated_member:
|
||||||
|
raise HTTPException(status_code=404, detail="Member not found")
|
||||||
|
return updated_member
|
||||||
|
|
||||||
|
@app.delete("/members/{member_id}", status_code=204)
|
||||||
|
async def delete_member(member_id: int, token: str = Depends(verify_token)):
|
||||||
|
"""Delete a member"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("DELETE FROM members WHERE member_id = %s", (member_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Member not found")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
# Subscription endpoints
|
||||||
|
@app.get("/lists/{list_id}/members", response_model=List[Member])
|
||||||
|
async def get_list_members(list_id: int, token: str = Depends(verify_token)):
|
||||||
|
"""Get all members of a specific list"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT m.*
|
||||||
|
FROM members m
|
||||||
|
JOIN list_members lm ON m.member_id = lm.member_id
|
||||||
|
WHERE lm.list_id = %s AND lm.active = 1
|
||||||
|
ORDER BY m.name
|
||||||
|
""", (list_id,))
|
||||||
|
members = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
return members
|
||||||
|
|
||||||
|
@app.post("/subscriptions", status_code=201)
|
||||||
|
async def subscribe_member(subscription: Subscription, token: str = Depends(verify_token)):
|
||||||
|
"""Subscribe a member to a list"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
# Get list_id and member_id
|
||||||
|
cursor.execute("SELECT list_id FROM lists WHERE list_email = %s", (subscription.list_email,))
|
||||||
|
list_result = cursor.fetchone()
|
||||||
|
if not list_result:
|
||||||
|
raise HTTPException(status_code=404, detail="List not found")
|
||||||
|
|
||||||
|
cursor.execute("SELECT member_id FROM members WHERE email = %s", (subscription.member_email,))
|
||||||
|
member_result = cursor.fetchone()
|
||||||
|
if not member_result:
|
||||||
|
raise HTTPException(status_code=404, detail="Member not found")
|
||||||
|
|
||||||
|
list_id = list_result[0]
|
||||||
|
member_id = member_result[0]
|
||||||
|
|
||||||
|
# Insert subscription
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO list_members (list_id, member_id, active) VALUES (%s, %s, %s)",
|
||||||
|
(list_id, member_id, subscription.active)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return {"message": "Subscription created", "list_email": subscription.list_email, "member_email": subscription.member_email}
|
||||||
|
except Error as e:
|
||||||
|
if "Duplicate entry" in str(e):
|
||||||
|
raise HTTPException(status_code=400, detail="Member already subscribed to this list")
|
||||||
|
raise HTTPException(status_code=400, detail=f"Failed to create subscription: {str(e)}")
|
||||||
|
|
||||||
|
@app.delete("/subscriptions")
|
||||||
|
async def unsubscribe_member(list_email: EmailStr, member_email: EmailStr, token: str = Depends(verify_token)):
|
||||||
|
"""Unsubscribe a member from a list"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE lm FROM list_members lm
|
||||||
|
JOIN lists l ON lm.list_id = l.list_id
|
||||||
|
JOIN members m ON lm.member_id = m.member_id
|
||||||
|
WHERE l.list_email = %s AND m.email = %s
|
||||||
|
""", (list_email, member_email))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Subscription not found")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
return {"message": "Unsubscribed successfully"}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
7
api/requirements.txt
Normal file
7
api/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
mysql-connector-python==8.2.0
|
||||||
|
pydantic==2.5.0
|
||||||
|
pydantic-settings==2.1.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
email-validator==2.1.0
|
||||||
30
api/test_api.sh
Executable file
30
api/test_api.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick test script for the Mailing List API
|
||||||
|
|
||||||
|
API_URL="http://localhost:8000"
|
||||||
|
TOKEN="your_api_token_here_change_this"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}Testing Mailing List API${NC}\n"
|
||||||
|
|
||||||
|
echo -e "${GREEN}1. Health Check:${NC}"
|
||||||
|
curl -s $API_URL/health | jq .
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo -e "${GREEN}2. Get All Lists:${NC}"
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" $API_URL/lists | jq .
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo -e "${GREEN}3. Get All Members:${NC}"
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" $API_URL/members | jq .
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo -e "${GREEN}4. Get Members of Community List:${NC}"
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" $API_URL/lists/1/members | jq .
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo -e "${BLUE}Visit http://localhost:8000/docs for interactive API documentation${NC}"
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
version: "3.9"
|
version: "3.9"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
maillist-internal:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
@@ -11,8 +16,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
- ./database/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
- ./database/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||||
ports:
|
networks:
|
||||||
- "3307:3306"
|
- maillist-internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -28,6 +33,20 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "25:25"
|
- "25:25"
|
||||||
|
networks:
|
||||||
|
- maillist-internal
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: ./api
|
||||||
|
container_name: maillist-api
|
||||||
|
env_file: .env
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
networks:
|
||||||
|
- maillist-internal
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user