magpie/app/api/endpoints/team.py
Alan Weinstock e9b4cdd88e feat: link_url variants, team accounts, session layout, menagerie route (#18 #19)
#19 — link_url on campaign variants (migration 019)
- ADD COLUMN link_url TEXT on campaign_variants
- create_variant, upsert_variant, update_variant all carry link_url
- RedditClient.post() supports kind=link when link_url set + body empty
- RedditPostStrategy passes link_url from extra dict
- poster.py merges link_url from variant into extra (same as slug/tags)
- API VariantCreate/VariantUpdate schemas include link_url
- CampaignDetail: link_url field in Add Variant form with copy button;
  link_url shown in variant list with clickable link + copy button
- Variant button disabled if neither body nor link_url is set

#18 — Multi-user team accounts (migrations 020-022)
- 020: team_accounts table (display_name, platform, username, session_file)
- 021: opportunities.assigned_to + post_as FK → team_accounts
- 022: posts.posted_by_account_id FK → team_accounts
- Store: list/get/get_by_username/create_team_account, assign_opportunity
- API: GET/POST /api/v1/team; POST /api/v1/team/{id}/assign
- config.py: sessions_dir added; reddit_session_file now points to
  sessions/alan_reddit.json (backward compat path kept)
- scripts/migrate_sessions.py: one-shot move session.json →
  sessions/alan_reddit.json + creates placeholder files for future accounts
- manage.sh: build (VITE_BASE_URL=/magpie/ npm build), serve (static),
  migrate-sessions subcommands added; login updated to new session path
- Caddy: @magpie_no_session gate + handle /magpie/api* and /magpie*
  blocks added to menagerie.circuitforge.tech site block
2026-05-27 15:31:58 -07:00

89 lines
2.5 KiB
Python

"""
Team accounts — list and manage posting identities across platforms.
Read operations are available to all; create/update reserved for admin use.
"""
from __future__ import annotations
import asyncio
import logging
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.core.config import get_settings
from app.db.store import Store
router = APIRouter(prefix="/team", tags=["team"])
logger = logging.getLogger(__name__)
def _get_store() -> Store:
return Store(get_settings().db_path)
def _in_thread(fn):
store = _get_store()
try:
return fn(store)
finally:
store.close()
class TeamAccountCreate(BaseModel):
display_name: str
platform: str
username: str
account_type: str = "personal"
session_file: str | None = None
notes: str | None = None
# ------------------------------------------------------------------ #
# Routes
# ------------------------------------------------------------------ #
@router.get("")
async def list_team_accounts(platform: str | None = None, active_only: bool = True):
"""List all registered team accounts. Filter by platform if provided."""
return await asyncio.to_thread(
_in_thread,
lambda s: s.list_team_accounts(platform=platform, active_only=active_only),
)
@router.get("/{account_id}")
async def get_team_account(account_id: int):
result = await asyncio.to_thread(_in_thread, lambda s: s.get_team_account(account_id))
if result is None:
raise HTTPException(404, "Team account not found")
return result
@router.post("", status_code=201)
async def create_team_account(body: TeamAccountCreate):
logger.info(
"Creating team account: %s / %s (%s)", body.display_name, body.platform, body.account_type
)
return await asyncio.to_thread(
_in_thread,
lambda s: s.create_team_account(**body.model_dump()),
)
@router.post("/{opportunity_id}/assign")
async def assign_opportunity(
opportunity_id: int,
assigned_to: int | None = None,
post_as: int | None = None,
):
"""Assign an opportunity to a team member and/or set the posting account."""
result = await asyncio.to_thread(
_in_thread,
lambda s: s.assign_opportunity(opportunity_id, assigned_to, post_as),
)
if result is None:
raise HTTPException(404, "Opportunity not found")
logger.info(
"Opportunity %s assigned_to=%s post_as=%s", opportunity_id, assigned_to, post_as
)
return result