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
This commit is contained in:
0
backend/app/auth/__init__.py
Normal file
0
backend/app/auth/__init__.py
Normal file
41
backend/app/auth/dependencies.py
Normal file
41
backend/app/auth/dependencies.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from jose import JWTError
|
||||
|
||||
from app.auth.jwt import decode_token
|
||||
from app.database import get_db
|
||||
from app.models.user import HubUser, UserRole
|
||||
|
||||
bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(bearer),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> HubUser:
|
||||
if not credentials:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||
try:
|
||||
payload = decode_token(credentials.credentials)
|
||||
user_id: str = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
except JWTError:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
result = await db.execute(select(HubUser).where(HubUser.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(status_code=401, detail="User not found or inactive")
|
||||
return user
|
||||
|
||||
async def require_super_admin(user: HubUser = Depends(get_current_user)) -> HubUser:
|
||||
if user.role != UserRole.super_admin:
|
||||
raise HTTPException(status_code=403, detail="Super admin access required")
|
||||
return user
|
||||
|
||||
async def require_school_admin(user: HubUser = Depends(get_current_user)) -> HubUser:
|
||||
if user.role not in (UserRole.super_admin, UserRole.school_admin):
|
||||
raise HTTPException(status_code=403, detail="School admin access required")
|
||||
return user
|
||||
12
backend/app/auth/jwt.py
Normal file
12
backend/app/auth/jwt.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from jose import jwt, JWTError
|
||||
from app.config import settings
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
|
||||
def decode_token(token: str) -> dict:
|
||||
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
9
backend/app/auth/password.py
Normal file
9
backend/app/auth/password.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def hash_password(plain: str) -> str:
|
||||
return pwd_context.hash(plain)
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
return pwd_context.verify(plain, hashed)
|
||||
Reference in New Issue
Block a user