# Nova Web Admin API — Shared Components # Standalone copy of all schemas and reusable responses from index.yaml. # Grouped by feature for easy navigation. # Kept in sync with novaOpenApi/web/index.yaml#/components. openapi: "3.0.3" info: title: Nova Web Admin API version: "v1.0" paths: /health: get: summary: Health Check description: Endpoint to check the health status of the Nova OpenAPI service. responses: '200': description: Service is healthy # Paths are defined in the main index.yaml. This file only contains the shared components. components: # ─── Reusable responses ─────────────────────────────────────────────────── responses: ClientError: description: | Client error (HTTP 4XX). Invalid request, missing params, validation failure, or auth error. content: application/json: schema: $ref: "#/components/schemas/ApiError" example: code: 180215 message: "Invalid request data (validation failed)" details: - source: "username" issue: "required" ServerError: description: Internal server error (HTTP 5XX). content: application/json: schema: $ref: "#/components/schemas/ApiError" example: code: 100203 message: "Unknown internal server error" details: [] schemas: # ── Base ────────────────────────────────────────────────────────────────── ApiError: type: object required: [code, message] description: | Error envelope returned by `awsBase/apiMiddleware.go`. `code` is composed by `serviceCode*10000 + pkgCode*100 + clientErrCode`. - `serviceCode`: `10` = system, `18` = Nova - `pkgCode`: `2` = staff package (`PkgErr_Staff`) - `clientErrCode`: specific error code (`00` for base, `01`-`99` for specific errors) Base codes for web admin: `sysErrCode = 100200`, `pkgErrCode = 180200` **System errors (`1002ZZ`)**: - `100201` — Database connection error - `100202` — Database query error - `100203` — Unknown internal server error - `100204` — Timeout calling external service - `100205` — Internal data parse / unmarshal error **Nova business errors (`1802ZZ`)**: - `180206` — Invalid username or password - `180207` — Account disabled (`is_active = false`) - `180208` — Account locked (too many failed attempts) - `180209` — Must change password before continuing - `180210` — Invalid or expired OTP code - `180211` — `secret_id` not found or does not belong to user - `180212` — Duplicate `request_id` (replay attack detected) - `180213` — User not found - `180214` — Insufficient permissions (forbidden) - `180215` — Invalid request data (validation failed) - `180216` — Phone number already exists - `180217` — Username already exists - `180218` — Email already linked to another staff account - `180219` — OTP session expired (5-minute intermediate window) - `180220` — Account blocked — too many failed OTP attempts (requires admin unblock) - `180221` — User account is permanently deactivated properties: code: type: integer format: int32 description: "Composed error code, e.g. 180206" example: 180206 message: type: string description: Human-readable error description details: type: array description: Optional field-level error details (may be omitted) items: type: object properties: source: type: string description: Field or system that caused the error (e.g. "username") issue: type: string description: Description of the issue (e.g. "invalid credentials") # ── Auth ────────────────────────────────────────────────────────────────── LoginRequest: type: object required: [username, password] properties: username: type: string description: Username (usually email) password: type: string description: Plain-text password LoginResponse: type: object description: | Returned directly on HTTP 200. `two_fa_status` values: - `NOT_CONFIGURED` — account has no 2FA set up; redirect to Setup 2FA screen. - `VERIFIED` — 2FA active; proceed to Step 2 OTP verification. User roles: `ADMIN`, `OWNER`, `STAFF` properties: is_valid: type: boolean description: true if credentials are valid user_id: type: string description: Unique user identifier role: type: string enum: [ADMIN, OWNER, STAFF] description: User role two_fa_status: type: string enum: [NOT_CONFIGURED, VERIFIED] description: 2FA status of the account failed_login_attempts: type: integer format: int32 description: Number of consecutive failed login attempts locked_login_expires_at: type: string format: date-time nullable: true description: "Timestamp when the account lock expires (ISO 8601). Null if not locked." VerifyOTPRequest: type: object required: [otp_code, user_id] description: Step 2 payload — 6-digit TOTP code plus the staff user ID. properties: otp_code: type: string description: 6-digit numeric TOTP code from the Authenticator app. pattern: "^[0-9]{6}$" minLength: 6 maxLength: 6 example: "123456" user_id: type: string description: Staff user ID. VerifyOTPResponse: type: object description: Step 2 success — 2FA validation result. Returned directly on HTTP 200. required: [valid] properties: valid: type: boolean description: true if the 2FA code is valid. example: true CancelOTPSessionRequest: type: object required: [session_token] description: Destroys the intermediate password session ("Go Back" from Step 2 OTP screen). properties: session_token: type: string description: Intermediate session token to destroy. # ── Google Auth / 2FA Setup ─────────────────────────────────────────────── GenerateQRCodeLoginRequest: type: object required: [user_id, account_name] properties: user_id: type: string description: Employee ID account_name: type: string description: Account name (usually email) GenerateQRCodeLoginResponse: type: object description: Returned directly on HTTP 200. properties: qr_png_b64: type: string format: byte description: Base64-encoded PNG bytes secret_id: type: string description: Secret ID from GA service EnrollKeyVaultRequest: type: object required: [user_id, secret_id, code] properties: user_id: type: string secret_id: type: string code: type: string description: OTP code to confirm enrollment EnrollKeyVaultResponse: type: object description: Returned directly on HTTP 200. properties: valid: type: boolean description: true if the OTP code is valid and enrollment succeeded # ── Client User ─────────────────────────────────────────────────────────── CreateClientUserRequest: type: object required: [first_name, last_name, phone, username] properties: first_name: type: string description: Required. Max 25 characters. maxLength: 25 last_name: type: string description: Required. Max 25 characters. maxLength: 25 second_last_name: type: string description: Optional. Max 25 characters. maxLength: 25 phone: type: string description: Required. Digits only, 8–15 digits, international format. Must be unique. pattern: "^[0-9]{8,15}$" username: type: string description: Required. Must be unique. maxLength: 75 email: type: string format: email description: Optional. Valid email format, max 75 characters. maxLength: 75 country: type: string description: "Default: MEX" default: MEX address: type: string description: Optional. Max 200 characters. maxLength: 200 CreateClientUserResponse: type: object description: Returned directly on HTTP 200. properties: user_id: type: string description: Generated user ID username: type: string description: Client username role: type: string description: Always "CLIENT" example: CLIENT ClientUserItem: type: object properties: user_id: type: string username: type: string full_name: type: string description: FirstName + LastName + SecondLastName phone: type: string email: type: string address: type: string created_date: type: string description: "Format: DD/MM/YYYY HH:mm" example: "21/05/2026 08:30" status: type: string enum: [active, inactive] ListClientRequest: type: object description: Client user list filters, sorting, and pagination. properties: search: type: string description: Search by username, full name, email, or phone. Empty string disables search. statuses: type: array description: Empty array gets all statuses. items: type: string enum: [active, inactive] sort_by: type: string description: Sort field. enum: [created_at] default: created_at sort_dir: type: string description: Sort direction. enum: [asc, desc] default: desc limit: type: integer format: int32 description: Batch size. default: 20 offset: type: integer format: int32 description: Pagination offset. default: 0 ListClientUsersResponse: type: object description: Returned directly on HTTP 200. properties: items: type: array items: $ref: "#/components/schemas/ClientUserItem" total: type: integer format: int64 description: Total matched records limit: type: integer format: int32 offset: type: integer format: int32 # ── Staff User ──────────────────────────────────────────────────────────── CreateStaffUserRequest: type: object required: [first_name, last_name, email, phone, role, username] properties: first_name: type: string description: "Required. Min 2 chars. Letters, spaces, accents only (á,é,í,ó,ú,ü,ñ)." minLength: 2 last_name: type: string description: Required. Min 2 chars. Letters, spaces, accents only. minLength: 2 second_last_name: type: string description: Optional. Min 2 chars if provided. minLength: 2 email: type: string format: email description: Required. Valid email. Must be unique across all staff accounts. phone: type: string description: "Required. E.164 international format (e.g. +84901234567)." pattern: "^\\+[1-9]\\d{6,14}$" role: type: string description: "Required. One of: ADMIN, STAFF (excludes OWNER)." enum: [ADMIN, STAFF] username: type: string description: "Required. Pre-generated via GenerateStaffUsername. Format: nova.." CreateStaffUserResponse: type: object description: Returned directly on HTTP 200. properties: user_id: type: string description: Generated staff user ID username: type: string description: Confirmed username role: type: string enum: [ADMIN, STAFF] description: Assigned role GenerateStaffUsernameRequest: type: object required: [first_name, last_name] properties: first_name: type: string description: Required. last_name: type: string description: Required. GenerateStaffUsernameResponse: type: object description: | Returned directly on HTTP 200. Preview only — not persisted to DB. Format: `nova.{initial}{lastname}.{XX}` (lowercase), `XX` = 2-digit random number. properties: username: type: string description: "Generated username preview, e.g. nova.jdoe.43" example: "nova.jdoe.43" StaffUserItem: type: object properties: user_id: type: string full_name: type: string description: FirstName + LastName + SecondLastName username: type: string description: "Auto-generated. Format: nova.." email: type: string phone: type: string description: "E.164 format displayed as: +84 901234567" role: type: string enum: [ADMIN, STAFF] status: type: string enum: [active, inactive, locked] last_login: type: string description: "Format: DD/MM/YYYY HH:mm. `-` if never logged in." example: "21/05/2026 08:00" created_date: type: string description: "Format: DD/MM/YYYY HH:mm" example: "01/01/2026 09:00" ListStaffRequest: type: object description: Staff user list filters, sorting, and pagination. properties: search: type: string description: Search by full name, username, email, or phone. Empty string disables search. role: type: string description: Role filter. Empty string gets all roles. enum: ["", ADMIN, STAFF] statuses: type: array description: Empty array gets all statuses. items: type: string enum: [active, inactive, deactivated] sort_by: type: string description: Sort field. enum: [full_name, created_at] default: full_name sort_dir: type: string description: Sort direction. enum: [asc, desc] default: asc limit: type: integer format: int32 description: Batch size. default: 20 offset: type: integer format: int32 description: Pagination offset. default: 0 ListStaffUsersResponse: type: object description: Returned directly on HTTP 200. properties: items: type: array items: $ref: "#/components/schemas/StaffUserItem" total: type: integer format: int64 description: Total matched records limit: type: integer format: int32 offset: type: integer format: int32 # ── Station ────────────────────────────────────────────────────────────── CreateStationAddressRequest: type: object required: [country_id, state_province_id, city_id, district, street, house_number] properties: country_id: type: string description: Country identifier. state_province_id: type: string description: State/province identifier. city_id: type: string description: City identifier. district: type: string minLength: 2 maxLength: 75 street: type: string minLength: 2 maxLength: 75 house_number: type: string minLength: 1 maxLength: 75 CreateStationRequest: type: object required: [client_id, provider_code, station_name, type, capacity_kwp, address, created_by] properties: client_id: type: string description: Mongo ObjectID of the client. pattern: "^[a-fA-F0-9]{24}$" provider_code: type: string description: Provider code used to create the provider-side station. example: GROWATT station_name: type: string minLength: 2 maxLength: 75 type: type: string enum: [Residential, Industrial, Agricultural, Parking] capacity_kwp: type: number format: double minimum: 0 exclusiveMinimum: true maximum: 99999 address: $ref: "#/components/schemas/CreateStationAddressRequest" notes: type: string maxLength: 500 created_by: type: string description: Staff user ID that initiated station creation. CreateStationResponse: type: object description: Returned directly on HTTP 200. properties: station_id: type: string description: Generated station UUID. client_id: type: string description: Client ID linked to the station. station_name: type: string provider_code: type: string provider_station_id: type: string description: Station ID returned by the provider. provisioning_status: type: string description: Provider provisioning status after creation. # ── GetStaffInfo ─────────────────────────────────────────────────────────── GetStaffInfoRequest: type: object required: [staff_id] description: Fetch a single staff account by its MongoDB ObjectID. properties: staff_id: type: string description: "MongoDB ObjectID of the staff account (24-char hex)." minLength: 24 maxLength: 24 example: "6643f1a2b4c5d60012e34abc" GetStaffInfoResponse: type: object description: | Full staff profile. Returned directly on HTTP 200. - `status` is one of: `active`, `inactive`, `deactivated`. - `lock_status` is omitted when the account is not locked. - `last_login` is `"-"` if the staff has never logged in. properties: user_id: type: string description: Staff MongoDB ObjectID (hex string). example: "6643f1a2b4c5d60012e34abc" first_name: type: string example: "Jane" last_name: type: string example: "Doe" second_last_name: type: string nullable: true example: "" username: type: string description: "Format: nova.." example: "nova.jdoe.43" email: type: string format: email example: "jane.doe@nova.com" phone: type: string description: "E.164 international format, e.g. +5219981234567" example: "+5219981234567" role: type: string enum: [ADMIN, STAFF, OWNER] example: "ADMIN" status: type: string enum: [active, inactive, deactivated] example: "active" two_fa_status: type: string enum: [NOT_CONFIGURED, VERIFIED] example: "VERIFIED" must_change_password: type: boolean description: true if the staff must change their password before proceeding. example: false failed_login_attempts: type: integer format: int32 description: Current count of consecutive failed login attempts. example: 0 last_login: type: string description: "Format: DD/MM/YYYY HH:mm. `-` if never logged in." example: "21/05/2026 08:00" created_at: type: string description: "Format: DD/MM/YYYY HH:mm" example: "01/01/2026 09:00" created_by: type: string description: "MongoDB ObjectID of the staff who created this account." example: "6643f1a2b4c5d60012e34001" # ── Config ──────────────────────────────────────────────────────────────── GetConfigResponse: type: object description: | Returned directly on HTTP 200. - `roles` excludes OWNER (not creatable via UI). - `session_expiry_seconds` — client must force logout when exceeded. properties: roles: type: array items: type: string description: "Valid staff roles, e.g. [\"ADMIN\", \"STAFF\"]" example: ["ADMIN", "STAFF"] statuses: type: array items: type: string description: "Valid status values, e.g. [\"active\", \"inactive\", \"locked\"]" example: ["active", "inactive", "locked"] session_expiry_seconds: type: integer format: int32 description: Session expiry in seconds — client forces logout after this duration example: 3600 # ── Session ─────────────────────────────────────────────────────────────── InitSessionResponse: type: object description: Returned directly on HTTP 200. properties: tracking_id: type: string description: "Unique tracking ID for the current staff session. Format: NOVA-XXXXXX" example: "NOVA-000001"