changes :(

This commit is contained in:
Micha R. Albert 2025-06-25 11:49:01 -04:00
parent 96f95b5cd1
commit 62da10b69c
Signed by: mra
SSH key fingerprint: SHA256:2JB0fGfy7m2HQXAzvSXXKm7wPTj9Z60MOjFOQGM2Y/E
5 changed files with 249 additions and 122 deletions

View file

@ -1,60 +1,187 @@
from logging import Logger
import os
import secrets
from contextlib import asynccontextmanager
from typing import Annotated
from fastapi import FastAPI, Depends, HTTPException, Header
from sqlmodel import select, Session
from .db import get_session, init_db
from .models import Player, World
from secrets import token_urlsafe
from argon2 import PasswordHasher
from urllib.parse import urlencode
from typing import Annotated, Any
from dotenv import load_dotenv
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from fastapi.responses import RedirectResponse
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
from slack_bolt.async_app import AsyncAck, AsyncApp, AsyncRespond, AsyncSay
from slack_bolt.response import BoltResponse
from tortoise import Tortoise
from .models import User, UserCreate, UserResponse, UserUpdate
if not load_dotenv():
raise FileNotFoundError("Environment secrets not found!")
hasher = PasswordHasher()
@asynccontextmanager
async def lifespan(_: FastAPI):
init_db()
await Tortoise.init(
db_url="sqlite://random_access.db", modules={"models": ["random_access.models"]}
)
await Tortoise.generate_schemas()
yield
await Tortoise.close_connections()
app = FastAPI(lifespan=lifespan)
slack = AsyncApp(
signing_secret=os.getenv("SLACK_SIGNING_SECRET")
)
slack_handler = AsyncSlackRequestHandler(slack)
@app.post("/players/")
def create_player(player: Player, session: Session = Depends(get_session)) -> Player:
session.add(player)
session.commit()
session.refresh(player)
return player
@slack.event("app_mention") # pyright:ignore[reportUnknownMemberType]
async def handle_app_mentions(body: BoltResponse, say: AsyncSay, logger: Logger):
logger.info(body)
_ = await say("What's up?")
@app.get("/players/location")
def read_player_location(
api_token: Annotated[str | None, Header()], session: Session = Depends(get_session)
@slack.event("message") # pyright:ignore[reportUnknownMemberType]
async def handle_message():
pass
@slack.command("/random-access") # pyright:ignore[reportUnknownMemberType]
async def handle_command(ack: AsyncAck, body: BoltResponse, respond: AsyncRespond):
await ack()
subcommand = dict(body).get('text') # type: ignore
print(subcommand)
await respond("hewowo")
app = FastAPI(title="Random Access API", version="0.1.0", lifespan=lifespan)
@app.post("/slack/events")
async def endpoint(req: Request):
return await slack_handler.handle(req)
# Authentication dependency
async def get_current_user(x_api_key: Annotated[str, Header()]) -> User:
if not x_api_key:
raise HTTPException(status_code=401, detail="API key required")
user = await User.get_or_none(api_key=x_api_key)
if not user:
raise HTTPException(status_code=401, detail="Invalid API key")
return user
# Public endpoints (no auth required)
@app.get("/")
async def root():
return {"message": "Random Access API is running"}
@app.get("/auth/start")
async def auth_start(game_id: str):
url = "https://slack.com/openid/connect/authorize/?"
params = {"response_type": "code", "scope": "openid profile email", "client_id": os.environ.get("SLACK_CLIENT_ID"), "state": game_id, "redirect_uri": "https://random-access.prox.mra.sh/auth/callback"}
return RedirectResponse(url + urlencode(params))
@app.get("/auth/callback")
async def auth_callback(code: str, state: str):
print(code, state)
return "yay!"
@app.post("/register", response_model=UserResponse)
async def register_user(user_data: UserCreate):
# Check if username or email already exists
existing_user = await User.get_or_none(slack_id=user_data.slack_id)
if existing_user:
raise HTTPException(status_code=400, detail="Username already exists")
existing_email = await User.get_or_none(email=user_data.email)
if existing_email:
raise HTTPException(status_code=400, detail="Email already exists")
# Generate API key
api_key = secrets.token_urlsafe(32)
# Create user
user = await User.create(
slack_id=user_data.slack_id,
email=user_data.email,
display_name=user_data.display_name,
api_key=api_key,
)
return UserResponse(
id=user.id,
slack_id=user.slack_id,
email=user.email,
display_name=user.display_name,
api_key=user.api_key,
created_at=user.created_at.isoformat(),
)
# Protected endpoints (auth required)
@app.get("/profile", response_model=UserResponse)
async def get_profile(current_user: User = Depends(get_current_user)): # pyright:ignore[reportCallInDefaultInitializer]
return UserResponse(
id=current_user.id,
slack_id=current_user.slack_id,
email=current_user.email,
display_name=current_user.display_name,
api_key=current_user.api_key,
created_at=current_user.created_at.isoformat(),
)
@app.put("/profile", response_model=UserResponse)
async def update_profile(
user_update: UserUpdate, current_user: User = Depends(get_current_user) # pyright:ignore[reportCallInDefaultInitializer]
):
if not api_token:
raise HTTPException(status_code=401, detail="No API token provided")
hashed_token = hasher.hash(api_token)
results = session.exec(select(Player).where(Player.api_token == hashed_token))
if len(results.all()) != 1:
raise HTTPException(status_code=500, detail="Critical error, please report this!")
# Update fields if provided
if user_update.email is not None:
# Check if email is already taken by another user
existing_email = await User.get_or_none(email=user_update.email)
if existing_email and existing_email.id != current_user.id:
raise HTTPException(status_code=400, detail="Email already exists")
current_user.email = user_update.email
if user_update.display_name is not None:
current_user.display_name = user_update.display_name
await current_user.save()
return UserResponse(
id=current_user.id,
slack_id=current_user.slack_id,
email=current_user.email,
display_name=current_user.display_name,
api_key=current_user.api_key,
created_at=current_user.created_at.isoformat(),
)
@app.get("/profile/data")
async def get_profile_data(current_user: User = Depends(get_current_user)) -> dict[str, int | str | bool | float | dict[str, Any]]: # pyright:ignore[reportCallInDefaultInitializer,reportExplicitAny]
"""Get flexible profile data (for future passport features)"""
@app.get("/players/")
def read_players(session: Session = Depends(get_session)) -> list[dict[str, int | str]]:
players = [
{
"id": player.id,
"username": player.in_game_name,
"current_world_id": player.current_world_id,
"level": player.level,
}
for player in list(session.exec(select(Player)).all())
]
return players
return {"profile_data": current_user.profile_data} # pyright:ignore[reportUnknownMemberType]
@app.get("/worlds")
def read_worlds(session: Session = Depends(get_session)):
worlds = list(session.exec(select(World)).all())
return worlds
@app.put("/profile/data")
async def update_profile_data(
data: dict[str, Any], current_user: User = Depends(get_current_user) # pyright:ignore[reportCallInDefaultInitializer,reportExplicitAny]
):
"""Update flexible profile data (for future passport features)"""
current_user.profile_data = data
await current_user.save()
return {
"message": "Profile data updated",
"profile_data": current_user.profile_data,
}