90ac0b81d1
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1304 lines
40 KiB
YAML
1304 lines
40 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: FlatRender Identity Service (internal)
|
|
version: 1.0.0
|
|
description: |
|
|
Internal API for the Identity Service. Owned by .NET service.
|
|
Handles users, tenants, plans, payments, MFA, sessions.
|
|
Called by the Gateway and by other services via service tokens.
|
|
|
|
servers:
|
|
- url: http://identity-svc.internal/v1
|
|
|
|
security:
|
|
- BearerAuth: []
|
|
- ServiceToken: []
|
|
|
|
# Common types are referenced from ../common/types.yaml
|
|
# In production this is bundled via openapi-merge or similar.
|
|
|
|
tags:
|
|
- name: Auth
|
|
- name: Users
|
|
- name: Tenants
|
|
- name: Plans
|
|
- name: Payments
|
|
- name: Discounts
|
|
- name: ApiKeys
|
|
- name: Webhooks
|
|
- name: Gamification
|
|
- name: Admin
|
|
|
|
paths:
|
|
|
|
# ===================== AUTH =====================
|
|
/auth/register:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Register a new user (email/password)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [tenant_slug, password]
|
|
properties:
|
|
tenant_slug: { type: string }
|
|
email: { type: string, format: email }
|
|
phone_number: { type: string }
|
|
password: { type: string, minLength: 8 }
|
|
full_name: { type: string }
|
|
affiliate_code: { type: string }
|
|
accept_terms: { type: boolean }
|
|
responses:
|
|
'201':
|
|
description: Created (verification email/SMS sent)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
user_id: { type: string, format: uuid }
|
|
verification_required: { type: boolean }
|
|
|
|
/auth/login:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Login with email/phone + password
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [tenant_slug, password]
|
|
properties:
|
|
tenant_slug: { type: string }
|
|
email: { type: string }
|
|
phone_number: { type: string }
|
|
password: { type: string }
|
|
device_id: { type: string }
|
|
device_name: { type: string }
|
|
responses:
|
|
'200':
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/AuthTokens'
|
|
'401': { description: Invalid credentials }
|
|
'403': { description: MFA required, returns mfa_token }
|
|
|
|
/auth/login/oauth/{provider}:
|
|
post:
|
|
tags: [Auth]
|
|
summary: OAuth login (google/telegram)
|
|
parameters:
|
|
- name: provider
|
|
in: path
|
|
required: true
|
|
schema: { type: string, enum: [google, telegram] }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [tenant_slug, code]
|
|
properties:
|
|
tenant_slug: { type: string }
|
|
code: { type: string }
|
|
redirect_uri: { type: string }
|
|
responses:
|
|
'200':
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/AuthTokens' }
|
|
|
|
/auth/refresh:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Exchange refresh token for new access token
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [refresh_token]
|
|
properties:
|
|
refresh_token: { type: string }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/AuthTokens' }
|
|
|
|
/auth/logout:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Revoke current session
|
|
responses:
|
|
'204': { description: Logged out }
|
|
|
|
/auth/sessions:
|
|
get:
|
|
tags: [Auth]
|
|
summary: List active sessions for current user
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
sessions:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Session' }
|
|
|
|
/auth/sessions/{session_id}:
|
|
delete:
|
|
tags: [Auth]
|
|
summary: Revoke a specific session
|
|
parameters:
|
|
- { name: session_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'204': { description: Revoked }
|
|
|
|
/auth/verify/email:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Verify email via OTP code
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [token, code]
|
|
properties:
|
|
token: { type: string }
|
|
code: { type: string }
|
|
responses:
|
|
'200': { description: Verified }
|
|
|
|
/auth/verify/phone:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Verify phone via OTP code
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [token, code]
|
|
properties:
|
|
token: { type: string }
|
|
code: { type: string }
|
|
responses:
|
|
'200': { description: Verified }
|
|
|
|
/auth/password/reset/request:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Request password reset (email/SMS)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [tenant_slug]
|
|
properties:
|
|
tenant_slug: { type: string }
|
|
email: { type: string }
|
|
phone_number: { type: string }
|
|
responses:
|
|
'202': { description: Sent if account exists }
|
|
|
|
/auth/password/reset/confirm:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Reset password with token
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [token, new_password]
|
|
properties:
|
|
token: { type: string }
|
|
new_password: { type: string, minLength: 8 }
|
|
responses:
|
|
'200': { description: OK }
|
|
|
|
/auth/password/change:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Change password (logged-in)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [current_password, new_password]
|
|
properties:
|
|
current_password: { type: string }
|
|
new_password: { type: string, minLength: 8 }
|
|
responses:
|
|
'204': { description: Changed }
|
|
|
|
# MFA
|
|
/auth/mfa/setup:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Initiate MFA setup (returns TOTP secret/QR)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [factor_type]
|
|
properties:
|
|
factor_type: { type: string, enum: [TOTP, SMS] }
|
|
label: { type: string }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
factor_id: { type: string, format: uuid }
|
|
secret: { type: string }
|
|
qr_code_url: { type: string }
|
|
recovery_codes: { type: array, items: { type: string } }
|
|
|
|
/auth/mfa/verify:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Verify MFA factor with first code
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [factor_id, code]
|
|
properties:
|
|
factor_id: { type: string, format: uuid }
|
|
code: { type: string }
|
|
responses:
|
|
'200': { description: Verified }
|
|
|
|
/auth/mfa/challenge:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Complete login after MFA prompt
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [mfa_token, code]
|
|
properties:
|
|
mfa_token: { type: string }
|
|
code: { type: string }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/AuthTokens' }
|
|
|
|
# Push subscriptions (PWA)
|
|
/auth/push/subscribe:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Register browser Web Push subscription
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [endpoint, keys]
|
|
properties:
|
|
endpoint: { type: string }
|
|
keys:
|
|
type: object
|
|
required: [p256dh, auth]
|
|
properties:
|
|
p256dh: { type: string }
|
|
auth: { type: string }
|
|
user_agent: { type: string }
|
|
responses:
|
|
'201': { description: Created }
|
|
|
|
/auth/push/unsubscribe:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Remove a push subscription
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
endpoint: { type: string }
|
|
responses:
|
|
'204': { description: Removed }
|
|
|
|
# ===================== USERS =====================
|
|
/users/me:
|
|
get:
|
|
tags: [Users]
|
|
summary: Current user profile
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/User' }
|
|
patch:
|
|
tags: [Users]
|
|
summary: Update profile
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/UserUpdate' }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/User' }
|
|
|
|
/users/me/balance:
|
|
get:
|
|
tags: [Users]
|
|
summary: Get current balance + affiliate balance
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
balance_minor: { type: integer, format: int64 }
|
|
affiliate_balance_minor: { type: integer, format: int64 }
|
|
currency: { type: string }
|
|
daily_remain_render_count: { type: integer }
|
|
parallel_rendering_ceiling: { type: integer }
|
|
|
|
/users/me/avatar:
|
|
post:
|
|
tags: [Users]
|
|
summary: Pick avatar (from library) or upload custom
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
avatar_id: { type: string, format: uuid }
|
|
avatar_url: { type: string }
|
|
responses:
|
|
'200': { description: Updated }
|
|
|
|
/users/{user_id}:
|
|
get:
|
|
tags: [Users]
|
|
summary: (Admin) Get any user
|
|
parameters:
|
|
- { name: user_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/User' }
|
|
|
|
/users:
|
|
get:
|
|
tags: [Users]
|
|
summary: (Admin) Search users
|
|
parameters:
|
|
- { name: q, in: query, schema: { type: string } }
|
|
- { name: tenant_id, in: query, schema: { type: string, format: uuid } }
|
|
- { name: page, in: query, schema: { type: integer, default: 1 } }
|
|
- { name: page_size, in: query, schema: { type: integer, default: 20 } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { type: array, items: { $ref: '#/components/schemas/User' } }
|
|
meta: { $ref: '#/components/schemas/PaginationMeta' }
|
|
|
|
/users/{user_id}/ban:
|
|
post:
|
|
tags: [Admin]
|
|
summary: Ban a user
|
|
parameters:
|
|
- { name: user_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [reason]
|
|
properties:
|
|
reason: { type: string }
|
|
unblock_date: { type: string, format: date-time }
|
|
responses:
|
|
'204': { description: Banned }
|
|
|
|
# ===================== TENANTS =====================
|
|
/tenants:
|
|
get:
|
|
tags: [Tenants]
|
|
summary: (Admin) List tenants
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { type: array, items: { $ref: '#/components/schemas/Tenant' } }
|
|
meta: { $ref: '#/components/schemas/PaginationMeta' }
|
|
post:
|
|
tags: [Tenants]
|
|
summary: Create a tenant (Admin or signup flow)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/TenantCreate' }
|
|
responses:
|
|
'201':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Tenant' }
|
|
|
|
/tenants/by-slug/{slug}:
|
|
get:
|
|
tags: [Tenants]
|
|
summary: Resolve tenant by slug/domain (used by Gateway)
|
|
parameters:
|
|
- { name: slug, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Tenant' }
|
|
|
|
/tenants/{tenant_id}:
|
|
get:
|
|
tags: [Tenants]
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Tenant' }
|
|
patch:
|
|
tags: [Tenants]
|
|
summary: Update tenant settings/contact
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/TenantUpdate' }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Tenant' }
|
|
|
|
/tenants/{tenant_id}/branding:
|
|
get:
|
|
tags: [Tenants]
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/TenantBranding' }
|
|
put:
|
|
tags: [Tenants]
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/TenantBranding' }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/TenantBranding' }
|
|
|
|
/tenants/{tenant_id}/domains/verify:
|
|
post:
|
|
tags: [Tenants]
|
|
summary: Start domain verification (returns DNS challenge)
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [domain]
|
|
properties:
|
|
domain: { type: string }
|
|
method: { type: string, enum: [DNS_TXT, HTTP_FILE], default: DNS_TXT }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
verification_id: { type: string, format: uuid }
|
|
challenge_record: { type: string }
|
|
expires_at: { type: string, format: date-time }
|
|
|
|
/tenants/{tenant_id}/usage:
|
|
get:
|
|
tags: [Tenants]
|
|
summary: Usage time-series for billing/dashboard
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
- { name: from, in: query, schema: { type: string, format: date } }
|
|
- { name: to, in: query, schema: { type: string, format: date } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/TenantUsageDay' }
|
|
|
|
# ===================== API KEYS =====================
|
|
/tenants/{tenant_id}/api-keys:
|
|
get:
|
|
tags: [ApiKeys]
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/ApiKey' }
|
|
post:
|
|
tags: [ApiKeys]
|
|
summary: Create new API key (full secret returned ONCE)
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ApiKeyCreate' }
|
|
responses:
|
|
'201':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
allOf:
|
|
- $ref: '#/components/schemas/ApiKey'
|
|
- type: object
|
|
properties:
|
|
secret_key:
|
|
type: string
|
|
description: Shown only on creation. Format fr_live_xxx... or fr_test_xxx...
|
|
|
|
/tenants/{tenant_id}/api-keys/{api_key_id}:
|
|
delete:
|
|
tags: [ApiKeys]
|
|
summary: Revoke API key
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
- { name: api_key_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
reason: { type: string }
|
|
responses:
|
|
'204': { description: Revoked }
|
|
|
|
/api-keys/validate:
|
|
post:
|
|
tags: [ApiKeys]
|
|
summary: (Internal) Validate API key + signature (called by Gateway)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [key_prefix, key_hash]
|
|
properties:
|
|
key_prefix: { type: string }
|
|
key_hash: { type: string }
|
|
ip_address: { type: string }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
valid: { type: boolean }
|
|
tenant_id: { type: string, format: uuid }
|
|
scopes: { type: array, items: { type: string } }
|
|
rate_limit_rpm: { type: integer }
|
|
|
|
# ===================== WEBHOOKS =====================
|
|
/tenants/{tenant_id}/webhooks:
|
|
get:
|
|
tags: [Webhooks]
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { type: array, items: { $ref: '#/components/schemas/Webhook' } }
|
|
post:
|
|
tags: [Webhooks]
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/WebhookCreate' }
|
|
responses:
|
|
'201':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Webhook' }
|
|
|
|
/tenants/{tenant_id}/webhooks/{webhook_id}:
|
|
delete:
|
|
tags: [Webhooks]
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
- { name: webhook_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'204': { description: Deleted }
|
|
|
|
/tenants/{tenant_id}/webhooks/{webhook_id}/deliveries:
|
|
get:
|
|
tags: [Webhooks]
|
|
summary: Recent delivery attempts
|
|
parameters:
|
|
- { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
- { name: webhook_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/WebhookDelivery' }
|
|
|
|
# ===================== PLANS =====================
|
|
/plans:
|
|
get:
|
|
tags: [Plans]
|
|
summary: List active plans (tenant-scoped via auth)
|
|
parameters:
|
|
- { name: scope, in: query, schema: { type: string, enum: [User, Tenant] } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { type: array, items: { $ref: '#/components/schemas/Plan' } }
|
|
|
|
/plans/{plan_id}:
|
|
get:
|
|
tags: [Plans]
|
|
parameters:
|
|
- { name: plan_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Plan' }
|
|
|
|
/users/me/plan:
|
|
get:
|
|
tags: [Plans]
|
|
summary: Current active plan
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/UserPlan' }
|
|
|
|
/users/me/plan/purchase:
|
|
post:
|
|
tags: [Plans]
|
|
summary: Start purchase flow (returns payment redirect URL)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [plan_id]
|
|
properties:
|
|
plan_id: { type: string, format: uuid }
|
|
gateway: { type: string, enum: [ZarinPal, IdPay, Bazaar, Stripe, Balance] }
|
|
discount_code: { type: string }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
payment_id: { type: string, format: uuid }
|
|
redirect_url: { type: string }
|
|
|
|
# ===================== PAYMENTS =====================
|
|
/payments:
|
|
get:
|
|
tags: [Payments]
|
|
summary: List user's payments
|
|
parameters:
|
|
- { name: page, in: query, schema: { type: integer } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { type: array, items: { $ref: '#/components/schemas/Payment' } }
|
|
meta: { $ref: '#/components/schemas/PaginationMeta' }
|
|
|
|
/payments/{payment_id}:
|
|
get:
|
|
tags: [Payments]
|
|
parameters:
|
|
- { name: payment_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Payment' }
|
|
|
|
/payments/callback/zarinpal:
|
|
get:
|
|
tags: [Payments]
|
|
summary: ZarinPal callback (verifies payment)
|
|
parameters:
|
|
- { name: Authority, in: query, schema: { type: string } }
|
|
- { name: Status, in: query, schema: { type: string } }
|
|
responses:
|
|
'302': { description: Redirect to UI }
|
|
|
|
/payments/callback/stripe:
|
|
post:
|
|
tags: [Payments]
|
|
summary: Stripe webhook
|
|
responses:
|
|
'200': { description: OK }
|
|
|
|
/payments/refund:
|
|
post:
|
|
tags: [Payments]
|
|
summary: (Internal) Issue refund (called by render service on failure)
|
|
security: [ServiceToken: []]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [payment_id, reason]
|
|
properties:
|
|
payment_id: { type: string, format: uuid }
|
|
amount_minor: { type: integer, format: int64 }
|
|
reason: { type: string }
|
|
refund_to: { type: string, enum: [Balance, OriginalMethod, Plan] }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
refund_id: { type: string, format: uuid }
|
|
status: { type: string }
|
|
|
|
# ===================== DISCOUNTS =====================
|
|
/discounts/validate:
|
|
post:
|
|
tags: [Discounts]
|
|
summary: Validate a discount code
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [code]
|
|
properties:
|
|
code: { type: string }
|
|
plan_id: { type: string, format: uuid }
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
valid: { type: boolean }
|
|
discount_minor: { type: integer, format: int64 }
|
|
kind: { type: string }
|
|
value: { type: number }
|
|
|
|
/discounts:
|
|
get:
|
|
tags: [Discounts]
|
|
summary: (Admin) List discounts
|
|
responses:
|
|
'200': { description: OK }
|
|
post:
|
|
tags: [Discounts]
|
|
summary: (Admin) Create discount
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/DiscountCreate' }
|
|
responses:
|
|
'201': { description: Created }
|
|
|
|
# ===================== GAMIFICATION =====================
|
|
/quests:
|
|
get:
|
|
tags: [Gamification]
|
|
summary: Active quests for current user
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { type: array, items: { $ref: '#/components/schemas/Quest' } }
|
|
|
|
/quests/{quest_id}/claim:
|
|
post:
|
|
tags: [Gamification]
|
|
summary: Claim a completed quest prize
|
|
parameters:
|
|
- { name: quest_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200': { description: Claimed }
|
|
|
|
/gifts/earned:
|
|
get:
|
|
tags: [Gamification]
|
|
summary: List earned gifts not yet used
|
|
responses:
|
|
'200':
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { type: array, items: { $ref: '#/components/schemas/EarnedGift' } }
|
|
|
|
/gifts/earned/{earned_gift_id}/use:
|
|
post:
|
|
tags: [Gamification]
|
|
summary: Claim/use an earned gift
|
|
parameters:
|
|
- { name: earned_gift_id, in: path, required: true, schema: { type: string, format: uuid } }
|
|
responses:
|
|
'200': { description: Used }
|
|
|
|
|
|
components:
|
|
securitySchemes:
|
|
BearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
ServiceToken:
|
|
type: http
|
|
scheme: bearer
|
|
|
|
schemas:
|
|
PaginationMeta:
|
|
type: object
|
|
properties:
|
|
page: { type: integer }
|
|
page_size: { type: integer }
|
|
total: { type: integer }
|
|
has_more: { type: boolean }
|
|
|
|
AuthTokens:
|
|
type: object
|
|
required: [access_token, refresh_token, expires_in]
|
|
properties:
|
|
access_token: { type: string }
|
|
refresh_token: { type: string }
|
|
token_type: { type: string, enum: [Bearer] }
|
|
expires_in: { type: integer, description: seconds }
|
|
user: { $ref: '#/components/schemas/User' }
|
|
tenant: { $ref: '#/components/schemas/Tenant' }
|
|
|
|
User:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
tenant_id: { type: string, format: uuid }
|
|
email: { type: string, nullable: true }
|
|
email_verified: { type: boolean }
|
|
phone_number: { type: string, nullable: true }
|
|
phone_verified: { type: boolean }
|
|
full_name: { type: string, nullable: true }
|
|
avatar_url: { type: string, nullable: true }
|
|
is_admin: { type: boolean }
|
|
is_tenant_admin: { type: boolean }
|
|
register_mode: { type: string }
|
|
last_active_date: { type: string, format: date-time }
|
|
balance_minor: { type: integer, format: int64 }
|
|
affiliate_balance_minor: { type: integer, format: int64 }
|
|
loyalty_score: { type: integer }
|
|
daily_remain_render_count: { type: integer }
|
|
max_daily_render_count: { type: integer }
|
|
parallel_rendering_ceiling: { type: integer }
|
|
used_storage_bytes: { type: integer, format: int64 }
|
|
register_date: { type: string, format: date-time }
|
|
|
|
UserUpdate:
|
|
type: object
|
|
properties:
|
|
full_name: { type: string }
|
|
slogan: { type: string }
|
|
about_me: { type: string }
|
|
company_name: { type: string }
|
|
website_name: { type: string }
|
|
birth_date: { type: string, format: date }
|
|
gender: { type: string, enum: [Male, Female, Other, PreferNotToSay] }
|
|
email_tell_me: { type: boolean }
|
|
sms_tell_me: { type: boolean }
|
|
push_tell_me: { type: boolean }
|
|
telegram_tell_me: { type: boolean }
|
|
|
|
Session:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
device_name: { type: string }
|
|
user_agent: { type: string }
|
|
ip_address: { type: string }
|
|
issued_at: { type: string, format: date-time }
|
|
last_used_at: { type: string, format: date-time }
|
|
is_current: { type: boolean }
|
|
|
|
Tenant:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
slug: { type: string }
|
|
name: { type: string }
|
|
kind: { type: string, enum: [Internal, Reseller, Enterprise] }
|
|
status: { type: string, enum: [Active, Trial, Suspended, Cancelled] }
|
|
custom_domain: { type: string, nullable: true }
|
|
domain_verified: { type: boolean }
|
|
contact_email: { type: string }
|
|
max_users: { type: integer, nullable: true }
|
|
max_storage_gb: { type: integer, nullable: true }
|
|
monthly_render_qty: { type: integer, nullable: true }
|
|
trial_ends_at: { type: string, format: date-time, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
TenantCreate:
|
|
type: object
|
|
required: [slug, name, contact_email]
|
|
properties:
|
|
slug: { type: string }
|
|
name: { type: string }
|
|
kind: { type: string, enum: [Reseller, Enterprise] }
|
|
contact_name: { type: string }
|
|
contact_email: { type: string, format: email }
|
|
contact_phone: { type: string }
|
|
|
|
TenantUpdate:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
contact_name: { type: string }
|
|
contact_email: { type: string }
|
|
contact_phone: { type: string }
|
|
billing_email: { type: string }
|
|
allowed_origins: { type: array, items: { type: string } }
|
|
|
|
TenantBranding:
|
|
type: object
|
|
properties:
|
|
display_name: { type: string }
|
|
logo_url: { type: string }
|
|
logo_dark_url: { type: string }
|
|
favicon_url: { type: string }
|
|
og_image_url: { type: string }
|
|
primary_color: { type: string }
|
|
secondary_color: { type: string }
|
|
accent_color: { type: string }
|
|
background_color: { type: string }
|
|
font_family: { type: string }
|
|
email_from_name: { type: string }
|
|
email_from_address: { type: string }
|
|
email_reply_to: { type: string }
|
|
email_footer_html: { type: string }
|
|
support_url: { type: string }
|
|
terms_url: { type: string }
|
|
privacy_url: { type: string }
|
|
embed_enabled: { type: boolean }
|
|
embed_allowed_hosts: { type: array, items: { type: string } }
|
|
watermark_text: { type: string }
|
|
watermark_image_url: { type: string }
|
|
watermark_enabled: { type: boolean }
|
|
|
|
TenantUsageDay:
|
|
type: object
|
|
properties:
|
|
usage_date: { type: string, format: date }
|
|
renders_completed: { type: integer }
|
|
render_seconds: { type: integer, format: int64 }
|
|
storage_bytes: { type: integer, format: int64 }
|
|
api_calls: { type: integer, format: int64 }
|
|
active_users: { type: integer }
|
|
amount_billed_minor: { type: integer, format: int64 }
|
|
billing_currency: { type: string }
|
|
billing_status: { type: string }
|
|
|
|
ApiKey:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
tenant_id: { type: string, format: uuid }
|
|
name: { type: string }
|
|
environment: { type: string, enum: [Live, Test] }
|
|
key_prefix: { type: string }
|
|
last4: { type: string }
|
|
scopes: { type: array, items: { type: string } }
|
|
allowed_ips: { type: array, items: { type: string } }
|
|
rate_limit_rpm: { type: integer }
|
|
is_active: { type: boolean }
|
|
expires_at: { type: string, format: date-time, nullable: true }
|
|
last_used_at: { type: string, format: date-time, nullable: true }
|
|
usage_count: { type: integer, format: int64 }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
ApiKeyCreate:
|
|
type: object
|
|
required: [name, scopes]
|
|
properties:
|
|
name: { type: string }
|
|
environment: { type: string, enum: [Live, Test] }
|
|
scopes:
|
|
type: array
|
|
items:
|
|
type: string
|
|
enum:
|
|
- renders:create
|
|
- renders:read
|
|
- renders:cancel
|
|
- projects:read
|
|
- projects:write
|
|
- templates:read
|
|
- exports:read
|
|
- exports:download
|
|
- users:read
|
|
- users:write
|
|
- webhooks:manage
|
|
allowed_ips: { type: array, items: { type: string } }
|
|
rate_limit_rpm: { type: integer }
|
|
expires_at: { type: string, format: date-time, nullable: true }
|
|
|
|
Webhook:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
name: { type: string }
|
|
url: { type: string }
|
|
events: { type: array, items: { type: string } }
|
|
is_active: { type: boolean }
|
|
last_triggered_at: { type: string, format: date-time, nullable: true }
|
|
last_status_code: { type: integer, nullable: true }
|
|
consecutive_failures: { type: integer }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
WebhookCreate:
|
|
type: object
|
|
required: [name, url, events]
|
|
properties:
|
|
name: { type: string }
|
|
url: { type: string, format: uri }
|
|
events:
|
|
type: array
|
|
items: { type: string }
|
|
|
|
WebhookDelivery:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
event_type: { type: string }
|
|
request_url: { type: string }
|
|
response_status: { type: integer, nullable: true }
|
|
response_body: { type: string, nullable: true }
|
|
duration_ms: { type: integer }
|
|
attempt: { type: integer }
|
|
succeeded: { type: boolean }
|
|
error_message: { type: string, nullable: true }
|
|
delivered_at: { type: string, format: date-time, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
Plan:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
code: { type: string }
|
|
name: { type: string }
|
|
description: { type: string }
|
|
price_minor: { type: integer, format: int64 }
|
|
before_price_minor: { type: integer, format: int64, nullable: true }
|
|
currency: { type: string }
|
|
billing_period: { type: string }
|
|
seconds_charge: { type: integer }
|
|
monthly_renders_quota: { type: integer, nullable: true }
|
|
storage_gb: { type: integer }
|
|
parallel_renders: { type: integer }
|
|
max_resolution: { type: string }
|
|
render_speed_factor: { type: number }
|
|
icon: { type: string }
|
|
is_featured: { type: boolean }
|
|
features: { type: object, additionalProperties: true }
|
|
|
|
UserPlan:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
plan_id: { type: string, format: uuid }
|
|
plan_code: { type: string }
|
|
plan_name: { type: string }
|
|
initial_seconds_charge: { type: integer }
|
|
remain_charge_sec: { type: integer }
|
|
monthly_renders_used: { type: integer }
|
|
starts_at: { type: string, format: date-time }
|
|
expires_at: { type: string, format: date-time }
|
|
cancelled_at: { type: string, format: date-time, nullable: true }
|
|
auto_renew: { type: boolean }
|
|
|
|
Payment:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
gateway: { type: string }
|
|
status: { type: string }
|
|
action: { type: string }
|
|
amount_minor: { type: integer, format: int64 }
|
|
currency: { type: string }
|
|
title: { type: string }
|
|
description: { type: string }
|
|
card_last4: { type: string }
|
|
confirmed_at: { type: string, format: date-time, nullable: true }
|
|
failed_at: { type: string, format: date-time, nullable: true }
|
|
failure_reason: { type: string, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
DiscountCreate:
|
|
type: object
|
|
required: [name, code, kind, value]
|
|
properties:
|
|
name: { type: string }
|
|
code: { type: string }
|
|
kind: { type: string, enum: [Percentage, FixedAmount, FreeMonths, RenderCredits] }
|
|
value: { type: number }
|
|
owner_user_id: { type: string, format: uuid }
|
|
owner_profit_percentage: { type: number }
|
|
max_use_count: { type: integer }
|
|
applies_to_plan_ids: { type: array, items: { type: string, format: uuid } }
|
|
starts_at: { type: string, format: date-time }
|
|
expires_at: { type: string, format: date-time }
|
|
|
|
Quest:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
title: { type: string }
|
|
challenge: { type: string }
|
|
why: { type: string }
|
|
hint: { type: string }
|
|
icon: { type: string }
|
|
quest_type: { type: string, enum: [OneTime, Daily, Weekly, Onboarding, Milestone] }
|
|
target_count: { type: integer }
|
|
current_count: { type: integer }
|
|
is_completed: { type: boolean }
|
|
prize_claimed: { type: boolean }
|
|
prize_type: { type: string }
|
|
prize_amount: { type: integer, format: int64 }
|
|
expires_at: { type: string, format: date-time, nullable: true }
|
|
|
|
EarnedGift:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
gift_id: { type: string, format: uuid }
|
|
name: { type: string }
|
|
description: { type: string }
|
|
prize_type: { type: string }
|
|
value: { type: integer, format: int64 }
|
|
unit: { type: string }
|
|
earned_at: { type: string, format: date-time }
|
|
expires_at: { type: string, format: date-time, nullable: true }
|
|
is_used: { type: boolean }
|