Add initial API backend scaffold

This commit is contained in:
Thuvaraka Yogarajah 2026-04-06 22:34:22 +02:00
parent da52852fdf
commit 4f34251198
13 changed files with 809 additions and 0 deletions

75
API_ENDPOINTS.md Normal file
View File

@ -0,0 +1,75 @@
# API-Endpunkte
Diese Endpunkte decken die Kernfunktionen von OnlyPrompt ab.
## Auth
### `POST /api/v1/auth/register`
- Erstellt einen neuen Benutzer.
### `POST /api/v1/auth/login`
- Loggt einen Benutzer ein.
### `GET /api/v1/auth/me`
- Gibt den aktuell eingeloggten Benutzer zurueck.
## User / Profile
### `GET /api/v1/users/{id}`
- Ruft das oeffentliche Benutzerprofil anhand der ID ab.
### `GET /api/v1/profile/me`
- Ruft das eigene Profil des eingeloggten Benutzers ab.
### `PUT /api/v1/profile/me`
- Bearbeitet das eigene Profil des eingeloggten Benutzers.
## Prompts
### `GET /api/v1/prompts`
- Gibt alle veroeffentlichten Prompts zurueck.
### `GET /api/v1/prompts/{id}`
- Ruft einen einzelnen Prompt anhand der ID ab.
### `POST /api/v1/prompts`
- Erstellt einen neuen Prompt.
### `PUT /api/v1/prompts/{id}`
- Bearbeitet einen bestehenden Prompt.
### `DELETE /api/v1/prompts/{id}`
- Loescht einen Prompt.
## Filter fuer Marketplace
### `GET /api/v1/prompts?category=coding`
- Filtert Prompts nach Kategorie.
### `GET /api/v1/prompts?search=python`
- Sucht Prompts ueber einen Suchbegriff.
### `GET /api/v1/prompts?creatorId=5`
- Filtert Prompts nach einem bestimmten Creator.
## Kategorien
### `GET /api/v1/categories`
- Gibt alle verfuegbaren Kategorien zurueck.
## Kaeufe
### `POST /api/v1/purchases`
- Erstellt einen Kauf fuer einen Prompt.
### `GET /api/v1/purchases/me`
- Gibt alle eigenen Kaeufe des eingeloggten Benutzers zurueck.
## Reviews
### `GET /api/v1/prompts/{id}/reviews`
- Ruft alle Bewertungen fuer einen Prompt ab.
### `POST /api/v1/prompts/{id}/reviews`
- Erstellt eine neue Bewertung fuer einen Prompt.

78
KERNMODELL.md Normal file
View File

@ -0,0 +1,78 @@
# Kernmodell
Dieses Kernmodell beschreibt die wichtigsten Klassen, die OnlyPrompt fuer Login, Profile, Marketplace und Prompt-Kaeufe benoetigt.
## Klassen
### User
- `id: int`
- `username: string`
- `email: string`
- `passwordHash: string`
- `role: string`
- `createdAt: datetime`
### Profile
- `id: int`
- `userId: int`
- `displayName: string`
- `bio: string`
- `avatarUrl: string`
- `specialties: string`
### Prompt
- `id: int`
- `creatorId: int`
- `categoryId: int`
- `title: string`
- `description: string`
- `content: text`
- `price: decimal`
- `thumbnailUrl: string`
- `ratingAverage: float`
- `reviewCount: int`
- `status: string`
- `createdAt: datetime`
- `updatedAt: datetime`
### Category
- `id: int`
- `name: string`
- `slug: string`
### Purchase
- `id: int`
- `buyerId: int`
- `promptId: int`
- `pricePaid: decimal`
- `purchasedAt: datetime`
### Review
- `id: int`
- `promptId: int`
- `userId: int`
- `rating: int`
- `comment: string`
- `createdAt: datetime`
## Beziehungen
- Ein `User` hat genau ein `Profile`.
- Ein `User` kann viele `Prompts` erstellen.
- Ein `Prompt` gehoert zu genau einer `Category`.
- Ein `User` kann viele `Prompts` ueber `Purchase` kaufen.
- Ein `User` kann viele `Reviews` schreiben.
- Ein `Prompt` kann viele `Reviews` erhalten.
## UML-Kurzform
```text
User 1 --- 1 Profile
User 1 --- * Prompt
Category 1 --- * Prompt
User 1 --- * Purchase
Prompt 1 --- * Purchase
User 1 --- * Review
Prompt 1 --- * Review
```

1
backend/__init__.py Normal file
View File

@ -0,0 +1 @@

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

346
backend/main.py Normal file
View File

@ -0,0 +1,346 @@
from datetime import datetime
from fastapi import FastAPI, HTTPException, Query, Response, status
from .models import ChatMessage, Favorite, Follow, Prompt, Rating, User
from .schemas import (
AuthResponse,
ChatMessageCreateRequest,
ChatMessageResponse,
CreatorDetailResponse,
FavoriteCreateRequest,
FavoriteResponse,
FollowResponse,
LoginRequest,
ProfileUpdateRequest,
PromptCreateRequest,
PromptResponse,
PromptUpdateRequest,
RatingCreateRequest,
RatingResponse,
RegisterRequest,
UserResponse,
)
from .store import store
app = FastAPI(title="OnlyPrompt API", version="1.0.0")
def get_current_user() -> User:
user = store.users.get(store.current_user_id)
if user is None:
raise HTTPException(status_code=404, detail="Current user not found.")
return user
@app.post("/api/auth/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
def register(payload: RegisterRequest) -> AuthResponse:
if any(user.email == payload.email for user in store.users.values()):
raise HTTPException(status_code=400, detail="Email already exists.")
if any(user.username == payload.username for user in store.users.values()):
raise HTTPException(status_code=400, detail="Username already exists.")
now = datetime.utcnow()
user_id = store.next_id("user")
user = User(
id=user_id,
email=payload.email,
password_hash=payload.password,
full_name=payload.full_name,
username=payload.username,
bio="",
location="",
avatar_url="",
role=payload.role,
is_verified=False,
created_at=now,
)
store.users[user_id] = user
store.current_user_id = user_id
return AuthResponse(message="User created successfully.", user=UserResponse.model_validate(user))
@app.post("/api/auth/login", response_model=AuthResponse)
def login(payload: LoginRequest) -> AuthResponse:
user = next((item for item in store.users.values() if item.email == payload.email), None)
if user is None or user.password_hash != payload.password:
raise HTTPException(status_code=401, detail="Invalid email or password.")
store.current_user_id = user.id
return AuthResponse(message="Login successful.", user=UserResponse.model_validate(user))
@app.get("/api/prompts", response_model=list[PromptResponse])
def list_prompts(
category: str | None = Query(default=None),
search: str | None = Query(default=None),
) -> list[PromptResponse]:
prompts = list(store.prompts.values())
if category:
prompts = [prompt for prompt in prompts if prompt.category.lower() == category.lower()]
if search:
term = search.lower()
prompts = [
prompt
for prompt in prompts
if term in prompt.title.lower() or term in prompt.description.lower()
]
return [PromptResponse.model_validate(prompt) for prompt in prompts]
@app.get("/api/prompts/{prompt_id}", response_model=PromptResponse)
def get_prompt(prompt_id: int) -> PromptResponse:
prompt = store.prompts.get(prompt_id)
if prompt is None:
raise HTTPException(status_code=404, detail="Prompt not found.")
return PromptResponse.model_validate(prompt)
@app.post("/api/prompts", response_model=PromptResponse, status_code=status.HTTP_201_CREATED)
def create_prompt(payload: PromptCreateRequest) -> PromptResponse:
creator = store.users.get(payload.creator_id)
if creator is None or creator.role != "creator":
raise HTTPException(status_code=404, detail="Creator not found.")
prompt = Prompt(
id=store.next_id("prompt"),
title=payload.title,
description=payload.description,
content=payload.content,
image_url=payload.image_url,
category=payload.category,
price=payload.price,
creator_id=payload.creator_id,
created_at=datetime.utcnow(),
)
store.prompts[prompt.id] = prompt
return PromptResponse.model_validate(prompt)
@app.put("/api/prompts/{prompt_id}", response_model=PromptResponse)
def update_prompt(prompt_id: int, payload: PromptUpdateRequest) -> PromptResponse:
prompt = store.prompts.get(prompt_id)
if prompt is None:
raise HTTPException(status_code=404, detail="Prompt not found.")
updates = payload.model_dump(exclude_unset=True)
for field, value in updates.items():
setattr(prompt, field, value)
return PromptResponse.model_validate(prompt)
@app.delete("/api/prompts/{prompt_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_prompt(prompt_id: int) -> Response:
if prompt_id not in store.prompts:
raise HTTPException(status_code=404, detail="Prompt not found.")
del store.prompts[prompt_id]
return Response(status_code=status.HTTP_204_NO_CONTENT)
@app.get("/api/creators", response_model=list[UserResponse])
def list_creators(sort: str | None = Query(default=None)) -> list[UserResponse]:
creators = [user for user in store.users.values() if user.role == "creator"]
if sort == "new":
creators.sort(key=lambda item: item.created_at, reverse=True)
elif sort == "popular":
creators.sort(
key=lambda item: sum(1 for follow in store.follows.values() if follow.creator_id == item.id),
reverse=True,
)
elif sort == "top_rated":
def creator_rating(user: User) -> float:
creator_prompt_ids = [prompt.id for prompt in store.prompts.values() if prompt.creator_id == user.id]
ratings = [rating.score for rating in store.ratings.values() if rating.prompt_id in creator_prompt_ids]
return sum(ratings) / len(ratings) if ratings else 0
creators.sort(key=creator_rating, reverse=True)
return [UserResponse.model_validate(creator) for creator in creators]
@app.get("/api/creators/{creator_id}", response_model=CreatorDetailResponse)
def get_creator(creator_id: int) -> CreatorDetailResponse:
creator = store.users.get(creator_id)
if creator is None or creator.role != "creator":
raise HTTPException(status_code=404, detail="Creator not found.")
prompts = [prompt for prompt in store.prompts.values() if prompt.creator_id == creator_id]
return CreatorDetailResponse(
creator=UserResponse.model_validate(creator),
prompts=[PromptResponse.model_validate(prompt) for prompt in prompts],
)
@app.get("/api/prompts/{prompt_id}/ratings", response_model=list[RatingResponse])
def list_ratings(prompt_id: int) -> list[RatingResponse]:
if prompt_id not in store.prompts:
raise HTTPException(status_code=404, detail="Prompt not found.")
ratings = [rating for rating in store.ratings.values() if rating.prompt_id == prompt_id]
return [RatingResponse.model_validate(rating) for rating in ratings]
@app.post("/api/prompts/{prompt_id}/ratings", response_model=RatingResponse, status_code=status.HTTP_201_CREATED)
def create_rating(prompt_id: int, payload: RatingCreateRequest) -> RatingResponse:
if prompt_id not in store.prompts:
raise HTTPException(status_code=404, detail="Prompt not found.")
if payload.user_id not in store.users:
raise HTTPException(status_code=404, detail="User not found.")
rating = Rating(
id=store.next_id("rating"),
prompt_id=prompt_id,
user_id=payload.user_id,
score=payload.score,
comment=payload.comment,
created_at=datetime.utcnow(),
)
store.ratings[rating.id] = rating
return RatingResponse.model_validate(rating)
@app.get("/api/favorites", response_model=list[FavoriteResponse])
def list_favorites() -> list[FavoriteResponse]:
current_user = get_current_user()
favorites = [item for item in store.favorites.values() if item.user_id == current_user.id]
return [FavoriteResponse.model_validate(item) for item in favorites]
@app.post("/api/favorites", response_model=FavoriteResponse, status_code=status.HTTP_201_CREATED)
def create_favorite(payload: FavoriteCreateRequest) -> FavoriteResponse:
current_user = get_current_user()
if payload.prompt_id not in store.prompts:
raise HTTPException(status_code=404, detail="Prompt not found.")
existing = next(
(
item
for item in store.favorites.values()
if item.user_id == current_user.id and item.prompt_id == payload.prompt_id
),
None,
)
if existing is not None:
return FavoriteResponse.model_validate(existing)
favorite = Favorite(
id=store.next_id("favorite"),
user_id=current_user.id,
prompt_id=payload.prompt_id,
created_at=datetime.utcnow(),
)
store.favorites[favorite.id] = favorite
return FavoriteResponse.model_validate(favorite)
@app.delete("/api/favorites/{prompt_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_favorite(prompt_id: int) -> Response:
current_user = get_current_user()
favorite_id = next(
(
item.id
for item in store.favorites.values()
if item.user_id == current_user.id and item.prompt_id == prompt_id
),
None,
)
if favorite_id is None:
raise HTTPException(status_code=404, detail="Favorite not found.")
del store.favorites[favorite_id]
return Response(status_code=status.HTTP_204_NO_CONTENT)
@app.post("/api/follows/{creator_id}", response_model=FollowResponse, status_code=status.HTTP_201_CREATED)
def create_follow(creator_id: int) -> FollowResponse:
current_user = get_current_user()
creator = store.users.get(creator_id)
if creator is None or creator.role != "creator":
raise HTTPException(status_code=404, detail="Creator not found.")
existing = next(
(
item
for item in store.follows.values()
if item.follower_id == current_user.id and item.creator_id == creator_id
),
None,
)
if existing is not None:
return FollowResponse.model_validate(existing)
follow = Follow(
id=store.next_id("follow"),
follower_id=current_user.id,
creator_id=creator_id,
created_at=datetime.utcnow(),
)
store.follows[follow.id] = follow
return FollowResponse.model_validate(follow)
@app.delete("/api/follows/{creator_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_follow(creator_id: int) -> Response:
current_user = get_current_user()
follow_id = next(
(
item.id
for item in store.follows.values()
if item.follower_id == current_user.id and item.creator_id == creator_id
),
None,
)
if follow_id is None:
raise HTTPException(status_code=404, detail="Follow not found.")
del store.follows[follow_id]
return Response(status_code=status.HTTP_204_NO_CONTENT)
@app.get("/api/profile", response_model=UserResponse)
def get_profile() -> UserResponse:
return UserResponse.model_validate(get_current_user())
@app.put("/api/profile", response_model=UserResponse)
def update_profile(payload: ProfileUpdateRequest) -> UserResponse:
user = get_current_user()
updates = payload.model_dump(exclude_unset=True)
for field, value in updates.items():
setattr(user, field, value)
return UserResponse.model_validate(user)
@app.get("/api/chats/{user_id}", response_model=list[ChatMessageResponse])
def get_chat(user_id: int) -> list[ChatMessageResponse]:
current_user = get_current_user()
if user_id not in store.users:
raise HTTPException(status_code=404, detail="User not found.")
messages = [
message
for message in store.chat_messages.values()
if {message.sender_id, message.receiver_id} == {current_user.id, user_id}
]
messages.sort(key=lambda item: item.created_at)
return [ChatMessageResponse.model_validate(message) for message in messages]
@app.post("/api/chats", response_model=ChatMessageResponse, status_code=status.HTTP_201_CREATED)
def create_chat_message(payload: ChatMessageCreateRequest) -> ChatMessageResponse:
current_user = get_current_user()
if payload.receiver_id not in store.users:
raise HTTPException(status_code=404, detail="Receiver not found.")
message = ChatMessage(
id=store.next_id("chat_message"),
sender_id=current_user.id,
receiver_id=payload.receiver_id,
content=payload.content,
created_at=datetime.utcnow(),
)
store.chat_messages[message.id] = message
return ChatMessageResponse.model_validate(message)

65
backend/models.py Normal file
View File

@ -0,0 +1,65 @@
from dataclasses import dataclass
from datetime import datetime
@dataclass
class User:
id: int
email: str
password_hash: str
full_name: str
username: str
bio: str
location: str
avatar_url: str
role: str
is_verified: bool
created_at: datetime
@dataclass
class Prompt:
id: int
title: str
description: str
content: str
image_url: str
category: str
price: float
creator_id: int
created_at: datetime
@dataclass
class Rating:
id: int
prompt_id: int
user_id: int
score: int
comment: str
created_at: datetime
@dataclass
class Favorite:
id: int
user_id: int
prompt_id: int
created_at: datetime
@dataclass
class Follow:
id: int
follower_id: int
creator_id: int
created_at: datetime
@dataclass
class ChatMessage:
id: int
sender_id: int
receiver_id: int
content: str
created_at: datetime

137
backend/schemas.py Normal file
View File

@ -0,0 +1,137 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: EmailStr
full_name: str
username: str
bio: str
location: str
avatar_url: str
role: str
is_verified: bool
created_at: datetime
class RegisterRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=6)
full_name: str = Field(min_length=2, max_length=100)
username: str = Field(min_length=3, max_length=50)
role: str = Field(default="user")
class LoginRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=6)
class AuthResponse(BaseModel):
message: str
user: UserResponse
class ProfileUpdateRequest(BaseModel):
full_name: Optional[str] = Field(default=None, min_length=2, max_length=100)
bio: Optional[str] = Field(default=None, max_length=500)
location: Optional[str] = Field(default=None, max_length=120)
avatar_url: Optional[str] = Field(default=None, max_length=255)
is_verified: Optional[bool] = None
class PromptCreateRequest(BaseModel):
title: str = Field(min_length=3, max_length=120)
description: str = Field(min_length=10, max_length=500)
content: str = Field(min_length=10)
image_url: str = Field(max_length=255)
category: str = Field(min_length=2, max_length=50)
price: float = Field(ge=0)
creator_id: int
class PromptUpdateRequest(BaseModel):
title: Optional[str] = Field(default=None, min_length=3, max_length=120)
description: Optional[str] = Field(default=None, min_length=10, max_length=500)
content: Optional[str] = Field(default=None, min_length=10)
image_url: Optional[str] = Field(default=None, max_length=255)
category: Optional[str] = Field(default=None, min_length=2, max_length=50)
price: Optional[float] = Field(default=None, ge=0)
class PromptResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
title: str
description: str
content: str
image_url: str
category: str
price: float
creator_id: int
created_at: datetime
class CreatorDetailResponse(BaseModel):
creator: UserResponse
prompts: list[PromptResponse]
class RatingCreateRequest(BaseModel):
user_id: int
score: int = Field(ge=1, le=5)
comment: str = Field(default="", max_length=500)
class RatingResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
prompt_id: int
user_id: int
score: int
comment: str
created_at: datetime
class FavoriteCreateRequest(BaseModel):
prompt_id: int
class FavoriteResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
user_id: int
prompt_id: int
created_at: datetime
class FollowResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
follower_id: int
creator_id: int
created_at: datetime
class ChatMessageCreateRequest(BaseModel):
receiver_id: int
content: str = Field(min_length=1, max_length=2000)
class ChatMessageResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
sender_id: int
receiver_id: int
content: str
created_at: datetime

103
backend/store.py Normal file
View File

@ -0,0 +1,103 @@
from datetime import datetime
from .models import ChatMessage, Favorite, Follow, Prompt, Rating, User
class InMemoryStore:
def __init__(self) -> None:
now = datetime.utcnow()
self.users = {
1: User(
id=1,
email="jane@example.com",
password_hash="demo-password",
full_name="Jane Doe",
username="janedoe",
bio="AI creator focused on writing and coding prompts.",
location="Zurich",
avatar_url="/images/content/creator1.png",
role="creator",
is_verified=True,
created_at=now,
),
2: User(
id=2,
email="max@example.com",
password_hash="demo-password",
full_name="Max Muster",
username="maxm",
bio="Prompt buyer and tester.",
location="Chur",
avatar_url="/images/content/creator2.png",
role="user",
is_verified=False,
created_at=now,
),
}
self.prompts = {
1: Prompt(
id=1,
title="Python Code Assistant",
description="Efficiently debug and write Python code with AI assistance.",
content="You are an expert Python assistant...",
image_url="/images/content/prompt2.png",
category="Coding",
price=19.99,
creator_id=1,
created_at=now,
)
}
self.ratings = {
1: Rating(
id=1,
prompt_id=1,
user_id=2,
score=5,
comment="Very useful starter prompt.",
created_at=now,
)
}
self.favorites = {
1: Favorite(
id=1,
user_id=2,
prompt_id=1,
created_at=now,
)
}
self.follows = {
1: Follow(
id=1,
follower_id=2,
creator_id=1,
created_at=now,
)
}
self.chat_messages = {
1: ChatMessage(
id=1,
sender_id=2,
receiver_id=1,
content="Hi, I have a question about your prompt.",
created_at=now,
)
}
self.current_user_id = 2
self.next_ids = {
"user": 3,
"prompt": 2,
"rating": 2,
"favorite": 2,
"follow": 2,
"chat_message": 2,
}
def next_id(self, key: str) -> int:
value = self.next_ids[key]
self.next_ids[key] += 1
return value
store = InMemoryStore()

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
fastapi
uvicorn
pydantic
email-validator