first commit

This commit is contained in:
debu-2 2026-10-02 11:22:40 +05:30
commit efa5080897
127 changed files with 18610 additions and 0 deletions

View file

@ -0,0 +1,176 @@
import re
from datetime import datetime, timezone
from urllib.parse import urlparse
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from .auth import current_user
from .database import get_db
from .models import UrmiOtpProject, UserAccount
from .otp_service import GenerateOtpPayload, generate_otp_for_user
from .urmiotp_catalog import OTP_API_DOCUMENTATION, render_templates_for_project
router = APIRouter(prefix="/api/v1/urmiotp", tags=["urmiotp"])
PROJECT_ID_PATTERN = re.compile(r"^[a-z][a-z0-9-]{4,28}[a-z0-9]$")
MESSAGE_LABEL_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 -]{0,18}[A-Za-z0-9]$|^[A-Za-z0-9]$")
RESERVED_TERMS = ("google", "goog", "gcp", "ssl")
class CreateUrmiOtpProjectRequest(BaseModel):
project_name: str = Field(min_length=6, max_length=30)
message_label: str = Field(min_length=1, max_length=20)
app_type: str = Field(pattern="^(web|mobile)$")
app_url: str = Field(min_length=8, max_length=1000)
purpose: str = Field(min_length=20, max_length=1000)
terms_accepted: bool
def validate_project_name(value: str) -> str:
project_name = value.strip().lower()
if not PROJECT_ID_PATTERN.fullmatch(project_name):
raise ValueError(
"Project name must be 6–30 lowercase letters, numbers, or hyphens; "
"start with a letter and end with a letter or number"
)
if "--" in project_name:
raise ValueError("Project name cannot contain consecutive hyphens")
if any(term in project_name for term in RESERVED_TERMS):
raise ValueError("Project name contains a reserved term")
return project_name
def validate_message_label(value: str) -> str:
message_label = value.strip()
if not message_label or len(message_label) > 20:
raise ValueError("Message label must be 1–20 characters")
if not MESSAGE_LABEL_PATTERN.fullmatch(message_label):
raise ValueError(
"Message label may only contain letters, numbers, spaces, and hyphens; "
"start and end with a letter or number"
)
return message_label
def validate_app_url(value: str, app_type: str) -> str:
app_url = value.strip()
parsed = urlparse(app_url)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("Enter a valid HTTPS website, domain, or app-store URL")
if app_type == "mobile" and not any(
host in parsed.netloc.lower()
for host in ("play.google.com", "apps.apple.com", "appgallery.huawei.com")
):
raise ValueError("Mobile apps require a Google Play, Apple App Store, or AppGallery URL")
return app_url
def serialize_project(project: UrmiOtpProject) -> dict:
return {
"id": str(project.id),
"project_name": project.project_name,
"message_label": project.message_label,
"app_type": project.app_type,
"app_url": project.app_url,
"purpose": project.purpose,
"status": project.status,
"review_note": project.review_note,
"terms_accepted_at": project.terms_accepted_at,
"created_at": project.created_at,
"reviewed_at": project.reviewed_at,
}
@router.get("/projects")
def list_projects(
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> list[dict]:
projects = db.scalars(
select(UrmiOtpProject)
.where(UrmiOtpProject.user_id == user.id)
.order_by(UrmiOtpProject.created_at.desc())
).all()
return [serialize_project(project) for project in projects]
@router.post("/projects", status_code=status.HTTP_201_CREATED)
def create_project(
payload: CreateUrmiOtpProjectRequest,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
if not payload.terms_accepted:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="You must agree to the UrmiOTP terms and conditions",
)
try:
project_name = validate_project_name(payload.project_name)
message_label = validate_message_label(payload.message_label)
app_url = validate_app_url(payload.app_url, payload.app_type)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
project = UrmiOtpProject(
user_id=user.id,
project_name=project_name,
message_label=message_label,
app_type=payload.app_type,
app_url=app_url,
purpose=payload.purpose.strip(),
terms_accepted_at=datetime.now(timezone.utc),
status="pending",
)
db.add(project)
try:
db.commit()
except IntegrityError as exc:
db.rollback()
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="This project name is already in use",
) from exc
db.refresh(project)
return serialize_project(project)
@router.get("/projects/{project_id}/documentation")
def project_documentation(
project_id: UUID,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
project = db.scalar(
select(UrmiOtpProject).where(
UrmiOtpProject.id == project_id,
UrmiOtpProject.user_id == user.id,
)
)
if not project:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="UrmiOTP project not found")
if project.status != "approved":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="API documentation is available after project approval",
)
return {
"project": serialize_project(project),
"templates": render_templates_for_project(project.message_label),
"api": OTP_API_DOCUMENTATION,
}
@router.post("/generate")
def generate_otp_console(
payload: GenerateOtpPayload,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
"""Console/session OTP generate — writes to otp_message_logs for Logs & Analytics."""
return generate_otp_for_user(db, user, payload)