Files
TapTrack-Hub/backend/app/routers/auth.py
kevin-asprec 73a17aaf9a feat(phase-1): TapTrack Hub initial scaffold
Full project scaffold for TapTrack Hub — cloud SaaS control plane
for managing on-prem TapTrack school deployments.

## Infrastructure
- Docker Compose: backend (gunicorn+uvicorn), Celery worker + beat,
  frontend (Vite build + nginx), PostgreSQL 15, Redis 7, nginx proxy
- Dockerfile for backend and frontend, nginx reverse proxy config

## Backend (FastAPI + SQLAlchemy async + Celery)
Database schema (10 tables):
  hub_users, schools, licenses, sms_jobs, sms_credit_ledger,
  invoices, invoice_line_items, school_subscriptions,
  support_tickets, ticket_replies, audit_logs, announcements

Auth: JWT (python-jose) + bcrypt + role-based FastAPI dependencies
  (get_current_user, require_super_admin, require_school_admin)

Routers (11): auth, schools, licenses, sms, billing, tickets,
  users, dashboard, school_portal, announcements, sync

Celery tasks (6):
  sms.process_queue, billing.generate_monthly_invoices,
  billing.send_invoice_email, billing.check_overdue,
  license.check_expiry, reports.send_monthly_reports

Services: SMTP email helper (smtplib + Jinja2)
Seed script: creates super admin admin@taptrack.io

## Frontend (Vue 3 + Vite + Pinia + Tailwind CSS)
Router: 14 routes across super admin + school portal layouts
Stores: Pinia auth store with localStorage persistence
API client: full axios client for all backend endpoints
Layouts: AppLayout (super admin), PortalLayout (school), AuthLayout
Components: AppSidebar, PortalSidebar, SidebarItem, KpiCard,
  StatusBadge, ToastStack
Pages: Login, Dashboard, Schools, SchoolDetail, Licenses, SMS,
  Billing, Tickets, TicketDetail, Users, Announcements, 404
Portal pages: Overview, Billing, SMS Reports, Tickets, Profile

## PAUL Planning Files
- .paul/ROADMAP.md: full 15-phase roadmap with detailed scope
- .paul/STATE.md: current position, tech stack, architecture notes
- .paul/phases/01-setup/01-PLAN.md: complete Phase 1 plan (done)
- .paul/phases/02 through 15: README stubs for all future phases
2026-03-16 07:26:06 +08:00

74 lines
2.6 KiB
Python

"""Authentication endpoints for TapTrack Hub."""
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status, Request
from pydantic import BaseModel, EmailStr
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.auth.password import verify_password, hash_password
from app.auth.jwt import create_access_token
from app.auth.dependencies import get_current_user
from app.database import get_db
from app.models.user import HubUser, UserRole
router = APIRouter(prefix="/api/auth", tags=["auth"])
class LoginRequest(BaseModel):
email: EmailStr
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
role: str
user_id: str
full_name: str
school_id: str | None
class ChangePasswordRequest(BaseModel):
current_password: str
new_password: str
@router.post("/login", response_model=TokenResponse)
async def login(body: LoginRequest, request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(HubUser).where(HubUser.email == body.email))
user = result.scalar_one_or_none()
if not user or not verify_password(body.password, user.hashed_password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
if not user.is_active:
raise HTTPException(status_code=401, detail="Account is inactive")
user.last_login_at = datetime.now(timezone.utc)
await db.commit()
token = create_access_token({"sub": user.id, "role": user.role.value})
return TokenResponse(
access_token=token,
role=user.role.value,
user_id=user.id,
full_name=user.full_name,
school_id=user.school_id,
)
@router.get("/me")
async def get_me(current_user: HubUser = Depends(get_current_user)):
return {
"id": current_user.id,
"email": current_user.email,
"full_name": current_user.full_name,
"role": current_user.role.value,
"school_id": current_user.school_id,
"is_active": current_user.is_active,
}
@router.put("/me/password", status_code=204)
async def change_password(
body: ChangePasswordRequest,
current_user: HubUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
if len(body.new_password) < 8:
raise HTTPException(status_code=422, detail="Password must be at least 8 characters")
if not verify_password(body.current_password, current_user.hashed_password):
raise HTTPException(status_code=400, detail="Current password is incorrect")
current_user.hashed_password = hash_password(body.new_password)
await db.commit()