diff --git a/account/api/api.go b/account/api/api.go index f8fc7d1..3f4b520 100644 --- a/account/api/api.go +++ b/account/api/api.go @@ -187,7 +187,7 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) { auth.POST("/register", h.register) auth.POST("/register/verify", h.verifyEmail) - auth.POST("/register/resend", h.resendEmailVerification) + auth.POST("/register/send", h.sendEmailVerification) auth.POST("/login", h.login) @@ -229,7 +229,7 @@ type verificationCodeRequest struct { Code string `json:"code"` } -type verificationResendRequest struct { +type verificationSendRequest struct { Email string `json:"email"` } @@ -429,13 +429,13 @@ func (h *handler) verifyEmail(c *gin.Context) { }) } -func (h *handler) resendEmailVerification(c *gin.Context) { +func (h *handler) sendEmailVerification(c *gin.Context) { if hasQueryParameter(c, "email") { respondError(c, http.StatusBadRequest, "email_in_query", "email must be sent in the request body") return } - var req verificationResendRequest + var req verificationSendRequest if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, "invalid_request", "invalid request payload") return @@ -468,12 +468,12 @@ func (h *handler) resendEmailVerification(c *gin.Context) { } if err := h.enqueueEmailVerification(c.Request.Context(), user); err != nil { - slog.Error("failed to resend verification email", "err", err, "email", user.Email) + slog.Error("failed to send verification email", "err", err, "email", user.Email) respondError(c, http.StatusInternalServerError, "verification_failed", "verification email could not be sent") return } - c.JSON(http.StatusOK, gin.H{"message": "verification email resent"}) + c.JSON(http.StatusOK, gin.H{"message": "verification email sent"}) } func (h *handler) requestPasswordReset(c *gin.Context) { diff --git a/account/api/api_test.go b/account/api/api_test.go index 081e8b5..2eb94d6 100644 --- a/account/api/api_test.go +++ b/account/api/api_test.go @@ -290,7 +290,7 @@ func TestResendVerificationEndpoint(t *testing.T) { t.Fatalf("failed to marshal resend payload: %v", err) } - req = httptest.NewRequest(http.MethodPost, "/api/auth/register/resend", bytes.NewReader(resendBody)) + req = httptest.NewRequest(http.MethodPost, "/api/auth/register/send", bytes.NewReader(resendBody)) req.Header.Set("Content-Type", "application/json") rr = httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -382,7 +382,7 @@ func TestResendVerificationEndpointErrors(t *testing.T) { t.Fatalf("failed to marshal resend payload: %v", err) } - req = httptest.NewRequest(http.MethodPost, "/api/auth/register/resend", bytes.NewReader(resendBody)) + req = httptest.NewRequest(http.MethodPost, "/api/auth/register/send", bytes.NewReader(resendBody)) req.Header.Set("Content-Type", "application/json") rr = httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -396,7 +396,7 @@ func TestResendVerificationEndpointErrors(t *testing.T) { t.Fatalf("failed to marshal unknown payload: %v", err) } - req = httptest.NewRequest(http.MethodPost, "/api/auth/register/resend", bytes.NewReader(unknownBody)) + req = httptest.NewRequest(http.MethodPost, "/api/auth/register/send", bytes.NewReader(unknownBody)) req.Header.Set("Content-Type", "application/json") rr = httptest.NewRecorder() router.ServeHTTP(rr, req) diff --git a/dashboard/app/(auth)/register/RegisterContent.tsx b/dashboard/app/(auth)/register/RegisterContent.tsx index e114d82..a3d9040 100644 --- a/dashboard/app/(auth)/register/RegisterContent.tsx +++ b/dashboard/app/(auth)/register/RegisterContent.tsx @@ -483,7 +483,7 @@ export default function RegisterContent() { } try { - const resendResponse = await fetch('/api/auth/register/resend', { + const resendResponse = await fetch('/api/auth/register/send', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -705,7 +705,7 @@ export default function RegisterContent() { setIsResending(true) try { - const response = await fetch('/api/auth/register/resend', { + const response = await fetch('/api/auth/register/send', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/dashboard/app/api/auth/register/resend/route.ts b/dashboard/app/api/auth/register/resend/route.ts deleted file mode 100644 index a6f2588..0000000 --- a/dashboard/app/api/auth/register/resend/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { POST, GET } from '../../verify-email/resend/route' diff --git a/dashboard/app/api/auth/register/send/route.ts b/dashboard/app/api/auth/register/send/route.ts new file mode 100644 index 0000000..e970732 --- /dev/null +++ b/dashboard/app/api/auth/register/send/route.ts @@ -0,0 +1 @@ +export { POST, GET } from '../../verify-email/send/route' diff --git a/dashboard/app/api/auth/verify-email/resend/route.ts b/dashboard/app/api/auth/verify-email/send/route.ts similarity index 83% rename from dashboard/app/api/auth/verify-email/resend/route.ts rename to dashboard/app/api/auth/verify-email/send/route.ts index 89ab3bd..df2927f 100644 --- a/dashboard/app/api/auth/verify-email/resend/route.ts +++ b/dashboard/app/api/auth/verify-email/send/route.ts @@ -5,7 +5,7 @@ import { getAccountServiceBaseUrl } from '@lib/serviceConfig' const ACCOUNT_SERVICE_URL = getAccountServiceBaseUrl() const ACCOUNT_API_BASE = `${ACCOUNT_SERVICE_URL}/api/auth` -type ResendPayload = { +type SendPayload = { email?: string } @@ -14,11 +14,11 @@ function normalizeEmail(value: unknown) { } export async function POST(request: NextRequest) { - let payload: ResendPayload + let payload: SendPayload try { - payload = (await request.json()) as ResendPayload + payload = (await request.json()) as SendPayload } catch (error) { - console.error('Failed to decode verification resend payload', error) + console.error('Failed to decode verification send payload', error) return NextResponse.json({ success: false, error: 'invalid_request', needMfa: false }, { status: 400 }) } @@ -28,7 +28,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetch(`${ACCOUNT_API_BASE}/register/resend`, { + const response = await fetch(`${ACCOUNT_API_BASE}/register/send`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -45,7 +45,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, error: null, needMfa: false }) } catch (error) { - console.error('Account service verification resend proxy failed', error) + console.error('Account service verification send proxy failed', error) return NextResponse.json( { success: false, error: 'account_service_unreachable', needMfa: false }, { status: 502 }, diff --git a/docs/api-endpoints.md b/docs/api-endpoints.md index 32f183e..1c9f43c 100644 --- a/docs/api-endpoints.md +++ b/docs/api-endpoints.md @@ -4,7 +4,7 @@ This document describes the HTTP endpoints provided by the XControl platform. Ea ## Authentication Gateway (Next.js) -The XControl web frontend exposes authentication APIs under `dashboard/app/api/auth`. These endpoints act as a secure gateway that proxies requests to the shared Account Service (`/api/auth/register`, `/api/auth/register/verify`, `/api/auth/login`, `/api/auth/mfa/setup`, `/api/auth/mfa/verify`). Responses always include `{ "success": boolean, "error": string | null, "needMfa": boolean }` so that multiple frontends can share the same Account Service behaviour. +The XControl web frontend exposes authentication APIs under `dashboard/app/api/auth`. These endpoints act as a secure gateway that proxies requests to the shared Account Service (`/api/auth/register`, `/api/auth/register/send`, `/api/auth/register/verify`, `/api/auth/login`, `/api/auth/mfa/setup`, `/api/auth/mfa/verify`). Responses always include `{ "success": boolean, "error": string | null, "needMfa": boolean }` so that multiple frontends can share the same Account Service behaviour. Gateway-managed session cookies (`xc_session`) and MFA challenge cookies (`xc_mfa_challenge`) are issued with `HttpOnly`, `Secure`, and `SameSite=Strict` attributes. Cookies are HTTPS-only and never expose raw secrets to JavaScript. @@ -22,6 +22,18 @@ Gateway-managed session cookies (`xc_session`) and MFA challenge cookies (`xc_mf -d '{"name":"demo","email":"demo@example.com","password":"Secret123","confirmPassword":"Secret123"}' ``` +### POST /api/auth/register/send +- **Description:** Trigger a verification email for an existing pending registration. This endpoint may be used to send the initial code when the frontend wants to separate registration from verification, or to resend a code if the user did not receive the previous email. +- **Body Parameters (JSON):** + - `email` – The pending account email address. +- **Response:** `{ "success": true, "error": null, "needMfa": false }` on success. On failure `error` contains the Account Service error code. +- **Test:** + ```bash + curl -X POST http://localhost:3000/api/auth/register/send \ + -H "Content-Type: application/json" \ + -d '{"email":"demo@example.com"}' + ``` + ### POST /api/auth/verify-email - **Description:** Confirm the 6-digit email verification code issued during registration. Activates the account when the code matches and has not expired. - **Body Parameters (JSON):**