AuthorBot commited on
Commit
ea890f6
Β·
1 Parent(s): 0f06ce6

Add: SuperAdmin create author endpoint (POST /api/super/authors) with optional plan + fix register ValueError 500

Browse files
app/api/schemas_router.py CHANGED
@@ -38,11 +38,15 @@ def _set_refresh_cookie(response: Response, refresh_token: str) -> None:
38
  @router.post("/auth/register", response_model=TokenResponse, status_code=201, tags=["Auth"])
39
  async def register(payload: RegisterRequest, response: Response, db: AsyncSession = Depends(get_db)):
40
  """Register a new author account."""
41
- result = await AuthService(db).register(
42
- email=payload.email,
43
- password=payload.password,
44
- full_name=payload.full_name,
45
- )
 
 
 
 
46
  _set_refresh_cookie(response, result["refresh_token"])
47
  return result
48
 
 
38
  @router.post("/auth/register", response_model=TokenResponse, status_code=201, tags=["Auth"])
39
  async def register(payload: RegisterRequest, response: Response, db: AsyncSession = Depends(get_db)):
40
  """Register a new author account."""
41
+ from fastapi import HTTPException
42
+ try:
43
+ result = await AuthService(db).register(
44
+ email=payload.email,
45
+ password=payload.password,
46
+ full_name=payload.full_name,
47
+ )
48
+ except ValueError as e:
49
+ raise HTTPException(status_code=400, detail=str(e))
50
  _set_refresh_cookie(response, result["refresh_token"])
51
  return result
52
 
app/schemas/superadmin.py CHANGED
@@ -3,7 +3,30 @@
3
  from datetime import datetime
4
  from typing import Literal
5
 
6
- from pydantic import BaseModel, Field
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
 
9
  class GrantAccessRequest(BaseModel):
 
3
  from datetime import datetime
4
  from typing import Literal
5
 
6
+ from pydantic import BaseModel, EmailStr, Field, field_validator
7
+
8
+
9
+ class CreateAuthorRequest(BaseModel):
10
+ """Request body for SuperAdmin to create a new author account."""
11
+
12
+ email: EmailStr
13
+ password: str = Field(..., min_length=8, max_length=128)
14
+ full_name: str = Field(..., min_length=1, max_length=255)
15
+ plan: Literal["monthly", "quarterly", "semi_annual", "annual"] | None = None
16
+ auto_renew: bool = False
17
+
18
+ @field_validator("password")
19
+ @classmethod
20
+ def password_complexity(cls, v: str) -> str:
21
+ if not any(c.isupper() for c in v):
22
+ raise ValueError("Password must contain at least one uppercase letter")
23
+ if not any(c.islower() for c in v):
24
+ raise ValueError("Password must contain at least one lowercase letter")
25
+ if not any(c.isdigit() for c in v):
26
+ raise ValueError("Password must contain at least one digit")
27
+ if not any(c in "!@#$%^&*()_+-=[]{}|;':\",./<>?" for c in v):
28
+ raise ValueError("Password must contain at least one special character")
29
+ return v
30
 
31
 
32
  class GrantAccessRequest(BaseModel):
app/superadmin/router.py CHANGED
@@ -28,6 +28,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
28
 
29
  from app.dependencies import get_db, get_current_superadmin, get_redis
30
  from app.schemas.superadmin import (
 
31
  GrantAccessRequest, RevokeAccessRequest,
32
  AddBonusTokensRequest, ExtendSubscriptionRequest,
33
  ClientListResponse, ClientDetailResponse,
@@ -81,6 +82,65 @@ async def diagnostic(superadmin=Depends(get_current_superadmin)):
81
 
82
  # ── Authors ───────────────────────────────────────────────────────────────────
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  @router.get("/authors")
85
  async def list_authors(
86
  limit: int = Query(50, ge=1, le=500),
 
28
 
29
  from app.dependencies import get_db, get_current_superadmin, get_redis
30
  from app.schemas.superadmin import (
31
+ CreateAuthorRequest,
32
  GrantAccessRequest, RevokeAccessRequest,
33
  AddBonusTokensRequest, ExtendSubscriptionRequest,
34
  ClientListResponse, ClientDetailResponse,
 
82
 
83
  # ── Authors ───────────────────────────────────────────────────────────────────
84
 
85
+ @router.post("/authors", status_code=201)
86
+ async def create_author(
87
+ payload: CreateAuthorRequest,
88
+ superadmin=Depends(get_current_superadmin),
89
+ db: AsyncSession = Depends(get_db),
90
+ redis=Depends(get_redis),
91
+ ):
92
+ """Create a new author account. Optionally assign a subscription plan."""
93
+ try:
94
+ from app.services.auth_service import AuthService
95
+
96
+ # Register the author
97
+ auth_svc = AuthService(db)
98
+ result = await auth_svc.register(
99
+ email=payload.email,
100
+ password=payload.password,
101
+ full_name=payload.full_name,
102
+ )
103
+ author_id = result["author_slug"]
104
+
105
+ # Log audit
106
+ from app.repositories.audit_repo import AuditRepository
107
+ audit = AuditRepository(db)
108
+ await audit.log(
109
+ actor_id=superadmin.id,
110
+ actor_email=superadmin.email,
111
+ action="create_author",
112
+ target_type="user",
113
+ target_id=author_id,
114
+ details=f"Created author {payload.email}",
115
+ )
116
+
117
+ response = {
118
+ "message": f"Author '{payload.full_name}' created successfully",
119
+ "author_id": author_id,
120
+ "email": payload.email,
121
+ }
122
+
123
+ # If plan specified, grant subscription immediately
124
+ if payload.plan:
125
+ service = SuperAdminService(db, redis)
126
+ grant_result = await service.grant_access(
127
+ actor=superadmin,
128
+ author_id=author_id,
129
+ plan=payload.plan,
130
+ auto_renew=payload.auto_renew,
131
+ )
132
+ response["subscription"] = {
133
+ "plan": payload.plan,
134
+ "token": grant_result.get("token"),
135
+ "expires_at": str(grant_result.get("expires_at", "")),
136
+ }
137
+
138
+ return response
139
+ except ValueError as e:
140
+ raise HTTPException(status_code=400, detail=str(e))
141
+ except Exception as e:
142
+ return _err(e, "create_author")
143
+
144
  @router.get("/authors")
145
  async def list_authors(
146
  limit: int = Query(50, ge=1, le=500),