diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 0000000..3a63997 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,8 @@ +# Develop + + +## Swaggo + +```bash +swag init -g cmd/quantum-auth/main.go +``` \ No newline at end of file diff --git a/README.md b/README.md index 15986ee..bd4ea4b 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,5 @@ If you rely on QuantumAuth or believe in our mission, please consider becoming a This section will list the names or logos of organizations and individuals who sponsor QuantumAuth at the *Project Sponsor* tier and above. If you'd like to be featured here, please visit our Sponsor page! + + diff --git a/cmd/quantum-auth/main.go b/cmd/quantum-auth/main.go index 3f9f024..f7d7b2f 100644 --- a/cmd/quantum-auth/main.go +++ b/cmd/quantum-auth/main.go @@ -11,10 +11,9 @@ import ( "github.com/quantumauth-io/quantum-go-utils/config" ) -// @title Quantum Auth API +// @title QuantumAuth API // @version 1.0 // @description Experimental quantum-resistant, hardware-aware auth service. -// @host localhost:1042 // @BasePath / func main() { diff --git a/docs/docs.go b/docs/docs.go index 6de6c2d..4dd85cd 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -29,7 +29,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/quantumhttp.SecurePingResponse" + "$ref": "#/definitions/http.SecurePingResponse" } }, "401": { @@ -61,7 +61,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/quantumhttp.authChallengeRequest" + "$ref": "#/definitions/http.authChallengeRequest" } } ], @@ -69,7 +69,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/quantumhttp.authChallengeResponse" + "$ref": "#/definitions/http.authChallengeResponse" } }, "400": { @@ -87,9 +87,61 @@ const docTemplate = `{ } } }, + "/auth/full-login": { + "post": { + "description": "Authenticates a device by verifying the user's password and both TPM \u0026 PQ signatures over a login message.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Full device login (password + TPM + PQ)", + "parameters": [ + { + "description": "Full login request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.fullLoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.fullLoginResponse" + } + }, + "400": { + "description": "invalid input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "user or device not found", + "schema": { + "type": "string" + } + } + } + } + }, "/auth/verify": { "post": { - "description": "Verifies a signed challenge response using TPM and PQ signatures plus password", + "description": "Verifies TPM + PQ signatures in the Authorization header for an API request", "consumes": [ "application/json" ], @@ -99,7 +151,7 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Verify auth response (hybrid TPM + PQ)", + "summary": "Verify signed QuantumAuth request", "parameters": [ { "description": "Verify request", @@ -107,7 +159,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/quantumhttp.authVerifyRequest" + "$ref": "#/definitions/http.authVerifyRequest" } } ], @@ -115,7 +167,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/quantumhttp.authVerifyResponse" + "$ref": "#/definitions/http.authVerifyResponse" } }, "400": { @@ -125,13 +177,13 @@ const docTemplate = `{ } }, "401": { - "description": "invalid signature or password", + "description": "unauthorized", "schema": { - "$ref": "#/definitions/quantumhttp.authVerifyResponse" + "$ref": "#/definitions/http.authVerifyResponse" } }, "404": { - "description": "challenge or device not found", + "description": "user or device not found", "schema": { "type": "string" } @@ -159,7 +211,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/quantumhttp.registerDeviceRequest" + "$ref": "#/definitions/http.registerDeviceRequest" } } ], @@ -167,7 +219,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/quantumhttp.registerDeviceResponse" + "$ref": "#/definitions/http.registerDeviceResponse" } }, "400": { @@ -185,6 +237,110 @@ const docTemplate = `{ } } }, + "/newsletter/subscribe": { + "post": { + "description": "Creates or re-subscribes an email to the newsletter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "newsletter" + ], + "summary": "Subscribe to newsletter", + "parameters": [ + { + "description": "Newsletter subscribe payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.newsletterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/http.newsletterResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/newsletter/unsubscribe": { + "post": { + "description": "Marks an email as unsubscribed (soft unsubscribe)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "newsletter" + ], + "summary": "Unsubscribe from newsletter", + "parameters": [ + { + "description": "Newsletter unsubscribe payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.newsletterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.newsletterResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/users/register": { "post": { "description": "Create a user account", @@ -205,7 +361,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/quantumhttp.SignupRequest" + "$ref": "#/definitions/http.SignupRequest" } } ], @@ -233,7 +389,7 @@ const docTemplate = `{ } }, "definitions": { - "quantumhttp.SecurePingResponse": { + "http.SecurePingResponse": { "type": "object", "properties": { "device_id": { @@ -250,7 +406,7 @@ const docTemplate = `{ } } }, - "quantumhttp.SignupRequest": { + "http.SignupRequest": { "type": "object", "required": [ "email" @@ -265,7 +421,7 @@ const docTemplate = `{ "lastName": { "type": "string" }, - "password": { + "password_b64": { "type": "string" }, "username": { @@ -273,7 +429,7 @@ const docTemplate = `{ } } }, - "quantumhttp.authChallengeRequest": { + "http.authChallengeRequest": { "type": "object", "properties": { "device_id": { @@ -281,7 +437,7 @@ const docTemplate = `{ } } }, - "quantumhttp.authChallengeResponse": { + "http.authChallengeResponse": { "type": "object", "properties": { "challenge_id": { @@ -291,20 +447,34 @@ const docTemplate = `{ "type": "string" }, "nonce": { + "type": "integer" + } + } + }, + "http.authVerifyRequest": { + "type": "object" + }, + "http.authVerifyResponse": { + "type": "object", + "properties": { + "authenticated": { + "type": "boolean" + }, + "user_id": { "type": "string" } } }, - "quantumhttp.authVerifyRequest": { + "http.fullLoginRequest": { "type": "object", "properties": { - "challenge_id": { + "device_id": { "type": "string" }, - "device_id": { + "message_b64": { "type": "string" }, - "password": { + "password_b64": { "type": "string" }, "pq_signature": { @@ -312,21 +482,52 @@ const docTemplate = `{ }, "tpm_signature": { "type": "string" + }, + "user_id": { + "type": "string" } } }, - "quantumhttp.authVerifyResponse": { + "http.fullLoginResponse": { "type": "object", "properties": { "authenticated": { "type": "boolean" }, + "device_id": { + "type": "string" + }, "user_id": { "type": "string" } } }, - "quantumhttp.registerDeviceRequest": { + "http.newsletterRequest": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + } + } + }, + "http.newsletterResponse": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "newsletter_id": { + "type": "string" + }, + "subscribed": { + "type": "boolean" + } + } + }, + "http.registerDeviceRequest": { "type": "object", "properties": { "device_label": { @@ -338,12 +539,12 @@ const docTemplate = `{ "tpm_public_key": { "type": "string" }, - "user_email": { + "user_Id": { "type": "string" } } }, - "quantumhttp.registerDeviceResponse": { + "http.registerDeviceResponse": { "type": "object", "properties": { "device_id": { @@ -357,10 +558,10 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", - Host: "localhost:1042", + Host: "", BasePath: "/", Schemes: []string{}, - Title: "Quantum Auth API", + Title: "QuantumAuth API", Description: "Experimental quantum-resistant, hardware-aware auth service.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, diff --git a/docs/swagger.json b/docs/swagger.json index 65f3671..539e71e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2,11 +2,10 @@ "swagger": "2.0", "info": { "description": "Experimental quantum-resistant, hardware-aware auth service.", - "title": "Quantum Auth API", + "title": "QuantumAuth API", "contact": {}, "version": "1.0" }, - "host": "localhost:1042", "basePath": "/", "paths": { "/api/secure-ping": { @@ -23,7 +22,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/quantumhttp.SecurePingResponse" + "$ref": "#/definitions/http.SecurePingResponse" } }, "401": { @@ -55,7 +54,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/quantumhttp.authChallengeRequest" + "$ref": "#/definitions/http.authChallengeRequest" } } ], @@ -63,7 +62,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/quantumhttp.authChallengeResponse" + "$ref": "#/definitions/http.authChallengeResponse" } }, "400": { @@ -81,9 +80,61 @@ } } }, + "/auth/full-login": { + "post": { + "description": "Authenticates a device by verifying the user's password and both TPM \u0026 PQ signatures over a login message.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Full device login (password + TPM + PQ)", + "parameters": [ + { + "description": "Full login request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.fullLoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.fullLoginResponse" + } + }, + "400": { + "description": "invalid input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "user or device not found", + "schema": { + "type": "string" + } + } + } + } + }, "/auth/verify": { "post": { - "description": "Verifies a signed challenge response using TPM and PQ signatures plus password", + "description": "Verifies TPM + PQ signatures in the Authorization header for an API request", "consumes": [ "application/json" ], @@ -93,7 +144,7 @@ "tags": [ "auth" ], - "summary": "Verify auth response (hybrid TPM + PQ)", + "summary": "Verify signed QuantumAuth request", "parameters": [ { "description": "Verify request", @@ -101,7 +152,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/quantumhttp.authVerifyRequest" + "$ref": "#/definitions/http.authVerifyRequest" } } ], @@ -109,7 +160,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/quantumhttp.authVerifyResponse" + "$ref": "#/definitions/http.authVerifyResponse" } }, "400": { @@ -119,13 +170,13 @@ } }, "401": { - "description": "invalid signature or password", + "description": "unauthorized", "schema": { - "$ref": "#/definitions/quantumhttp.authVerifyResponse" + "$ref": "#/definitions/http.authVerifyResponse" } }, "404": { - "description": "challenge or device not found", + "description": "user or device not found", "schema": { "type": "string" } @@ -153,7 +204,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/quantumhttp.registerDeviceRequest" + "$ref": "#/definitions/http.registerDeviceRequest" } } ], @@ -161,7 +212,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/quantumhttp.registerDeviceResponse" + "$ref": "#/definitions/http.registerDeviceResponse" } }, "400": { @@ -179,6 +230,110 @@ } } }, + "/newsletter/subscribe": { + "post": { + "description": "Creates or re-subscribes an email to the newsletter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "newsletter" + ], + "summary": "Subscribe to newsletter", + "parameters": [ + { + "description": "Newsletter subscribe payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.newsletterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/http.newsletterResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/newsletter/unsubscribe": { + "post": { + "description": "Marks an email as unsubscribed (soft unsubscribe)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "newsletter" + ], + "summary": "Unsubscribe from newsletter", + "parameters": [ + { + "description": "Newsletter unsubscribe payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.newsletterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.newsletterResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/users/register": { "post": { "description": "Create a user account", @@ -199,7 +354,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/quantumhttp.SignupRequest" + "$ref": "#/definitions/http.SignupRequest" } } ], @@ -227,7 +382,7 @@ } }, "definitions": { - "quantumhttp.SecurePingResponse": { + "http.SecurePingResponse": { "type": "object", "properties": { "device_id": { @@ -244,7 +399,7 @@ } } }, - "quantumhttp.SignupRequest": { + "http.SignupRequest": { "type": "object", "required": [ "email" @@ -259,7 +414,7 @@ "lastName": { "type": "string" }, - "password": { + "password_b64": { "type": "string" }, "username": { @@ -267,7 +422,7 @@ } } }, - "quantumhttp.authChallengeRequest": { + "http.authChallengeRequest": { "type": "object", "properties": { "device_id": { @@ -275,7 +430,7 @@ } } }, - "quantumhttp.authChallengeResponse": { + "http.authChallengeResponse": { "type": "object", "properties": { "challenge_id": { @@ -285,20 +440,34 @@ "type": "string" }, "nonce": { + "type": "integer" + } + } + }, + "http.authVerifyRequest": { + "type": "object" + }, + "http.authVerifyResponse": { + "type": "object", + "properties": { + "authenticated": { + "type": "boolean" + }, + "user_id": { "type": "string" } } }, - "quantumhttp.authVerifyRequest": { + "http.fullLoginRequest": { "type": "object", "properties": { - "challenge_id": { + "device_id": { "type": "string" }, - "device_id": { + "message_b64": { "type": "string" }, - "password": { + "password_b64": { "type": "string" }, "pq_signature": { @@ -306,21 +475,52 @@ }, "tpm_signature": { "type": "string" + }, + "user_id": { + "type": "string" } } }, - "quantumhttp.authVerifyResponse": { + "http.fullLoginResponse": { "type": "object", "properties": { "authenticated": { "type": "boolean" }, + "device_id": { + "type": "string" + }, "user_id": { "type": "string" } } }, - "quantumhttp.registerDeviceRequest": { + "http.newsletterRequest": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + } + } + }, + "http.newsletterResponse": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "newsletter_id": { + "type": "string" + }, + "subscribed": { + "type": "boolean" + } + } + }, + "http.registerDeviceRequest": { "type": "object", "properties": { "device_label": { @@ -332,12 +532,12 @@ "tpm_public_key": { "type": "string" }, - "user_email": { + "user_Id": { "type": "string" } } }, - "quantumhttp.registerDeviceResponse": { + "http.registerDeviceResponse": { "type": "object", "properties": { "device_id": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9314d06..5067f33 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,6 +1,6 @@ basePath: / definitions: - quantumhttp.SecurePingResponse: + http.SecurePingResponse: properties: device_id: type: string @@ -11,7 +11,7 @@ definitions: user_id: type: string type: object - quantumhttp.SignupRequest: + http.SignupRequest: properties: email: type: string @@ -19,48 +19,77 @@ definitions: type: string lastName: type: string - password: + password_b64: type: string username: type: string required: - email type: object - quantumhttp.authChallengeRequest: + http.authChallengeRequest: properties: device_id: type: string type: object - quantumhttp.authChallengeResponse: + http.authChallengeResponse: properties: challenge_id: type: string expires_at: type: string nonce: - type: string + type: integer + type: object + http.authVerifyRequest: type: object - quantumhttp.authVerifyRequest: + http.authVerifyResponse: properties: - challenge_id: + authenticated: + type: boolean + user_id: type: string + type: object + http.fullLoginRequest: + properties: device_id: type: string - password: + message_b64: + type: string + password_b64: type: string pq_signature: type: string tpm_signature: type: string + user_id: + type: string type: object - quantumhttp.authVerifyResponse: + http.fullLoginResponse: properties: authenticated: type: boolean + device_id: + type: string user_id: type: string type: object - quantumhttp.registerDeviceRequest: + http.newsletterRequest: + properties: + email: + type: string + required: + - email + type: object + http.newsletterResponse: + properties: + email: + type: string + newsletter_id: + type: string + subscribed: + type: boolean + type: object + http.registerDeviceRequest: properties: device_label: type: string @@ -68,19 +97,18 @@ definitions: type: string tpm_public_key: type: string - user_email: + user_Id: type: string type: object - quantumhttp.registerDeviceResponse: + http.registerDeviceResponse: properties: device_id: type: string type: object -host: localhost:1042 info: contact: {} description: Experimental quantum-resistant, hardware-aware auth service. - title: Quantum Auth API + title: QuantumAuth API version: "1.0" paths: /api/secure-ping: @@ -92,7 +120,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/quantumhttp.SecurePingResponse' + $ref: '#/definitions/http.SecurePingResponse' "401": description: unauthorized schema: @@ -111,14 +139,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/quantumhttp.authChallengeRequest' + $ref: '#/definitions/http.authChallengeRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/quantumhttp.authChallengeResponse' + $ref: '#/definitions/http.authChallengeResponse' "400": description: invalid input schema: @@ -130,39 +158,74 @@ paths: summary: Issue auth challenge tags: - auth + /auth/full-login: + post: + consumes: + - application/json + description: Authenticates a device by verifying the user's password and both + TPM & PQ signatures over a login message. + parameters: + - description: Full login request + in: body + name: payload + required: true + schema: + $ref: '#/definitions/http.fullLoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.fullLoginResponse' + "400": + description: invalid input + schema: + type: string + "401": + description: unauthorized + schema: + type: string + "404": + description: user or device not found + schema: + type: string + summary: Full device login (password + TPM + PQ) + tags: + - auth /auth/verify: post: consumes: - application/json - description: Verifies a signed challenge response using TPM and PQ signatures - plus password + description: Verifies TPM + PQ signatures in the Authorization header for an + API request parameters: - description: Verify request in: body name: payload required: true schema: - $ref: '#/definitions/quantumhttp.authVerifyRequest' + $ref: '#/definitions/http.authVerifyRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/quantumhttp.authVerifyResponse' + $ref: '#/definitions/http.authVerifyResponse' "400": description: invalid input schema: type: string "401": - description: invalid signature or password + description: unauthorized schema: - $ref: '#/definitions/quantumhttp.authVerifyResponse' + $ref: '#/definitions/http.authVerifyResponse' "404": - description: challenge or device not found + description: user or device not found schema: type: string - summary: Verify auth response (hybrid TPM + PQ) + summary: Verify signed QuantumAuth request tags: - auth /devices/register: @@ -177,14 +240,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/quantumhttp.registerDeviceRequest' + $ref: '#/definitions/http.registerDeviceRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/quantumhttp.registerDeviceResponse' + $ref: '#/definitions/http.registerDeviceResponse' "400": description: invalid input schema: @@ -196,6 +259,74 @@ paths: summary: Register device tags: - devices + /newsletter/subscribe: + post: + consumes: + - application/json + description: Creates or re-subscribes an email to the newsletter + parameters: + - description: Newsletter subscribe payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/http.newsletterRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/http.newsletterResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Subscribe to newsletter + tags: + - newsletter + /newsletter/unsubscribe: + post: + consumes: + - application/json + description: Marks an email as unsubscribed (soft unsubscribe) + parameters: + - description: Newsletter unsubscribe payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/http.newsletterRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.newsletterResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Unsubscribe from newsletter + tags: + - newsletter /users/register: post: consumes: @@ -207,7 +338,7 @@ paths: name: payload required: true schema: - $ref: '#/definitions/quantumhttp.SignupRequest' + $ref: '#/definitions/http.SignupRequest' produces: - application/json responses: diff --git a/go.mod b/go.mod index 1f18995..20f5efb 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/gin-contrib/cors v1.7.6 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect diff --git a/go.sum b/go.sum index 135063f..b28edab 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= diff --git a/internal/quantum/database/migrations/00004_create_news_letter.up.sql b/internal/quantum/database/migrations/00004_create_news_letter.up.sql new file mode 100644 index 0000000..f873caa --- /dev/null +++ b/internal/quantum/database/migrations/00004_create_news_letter.up.sql @@ -0,0 +1,35 @@ +-- Create newsletter table +CREATE TABLE IF NOT EXISTS newsletter ( + newsletter_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + email VARCHAR(255) NOT NULL UNIQUE, + subscribed BOOLEAN NOT NULL DEFAULT true, + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Create reusable updated_at trigger function (idempotent) +CREATE OR REPLACE FUNCTION set_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Attach trigger to newsletter table (idempotent) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_trigger + WHERE tgname = 'newsletter_set_updated_at' + ) THEN + CREATE TRIGGER newsletter_set_updated_at + BEFORE UPDATE ON newsletter + FOR EACH ROW + EXECUTE FUNCTION set_updated_at(); + END IF; +END; +$$; diff --git a/internal/quantum/database/migrations/00004_drop_news_letter.down.sql b/internal/quantum/database/migrations/00004_drop_news_letter.down.sql new file mode 100644 index 0000000..b99e349 --- /dev/null +++ b/internal/quantum/database/migrations/00004_drop_news_letter.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS newsletter; diff --git a/internal/quantum/database/newsletter.go b/internal/quantum/database/newsletter.go new file mode 100644 index 0000000..e29651c --- /dev/null +++ b/internal/quantum/database/newsletter.go @@ -0,0 +1,131 @@ +package database + +import ( + "context" + "time" + + "github.com/quantumauth-io/quantum-go-utils/log" +) + +// NewsletterSubscription represents a row in the newsletter table. +type NewsletterSubscription struct { + ID string + Email string + Subscribed bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type SubscribeNewsletterInput struct { + Email string +} + +// SubscribeNewsletter creates or re-subscribes an email. +// - If email is new: inserts a row subscribed=true +// - If email exists: sets subscribed=true (re-subscribe) and bumps updated_at via trigger +func (r *QuantumAuthRepository) SubscribeNewsletter(ctx context.Context, in SubscribeNewsletterInput) (string, error) { + const query = ` + INSERT INTO newsletter (email, subscribed) + VALUES ($1, true) + ON CONFLICT (email) + DO UPDATE SET subscribed = EXCLUDED.subscribed + RETURNING newsletter_id; + ` + + var id string + row, err := r.db.QueryRow(ctx, query, in.Email) + if err != nil { + log.Error("Error subscribing newsletter", "error", err, "email", in.Email) + return "", err + } + + if err := row.Scan(&id); err != nil { + log.Error("Error subscribing newsletter (scan)", "error", err, "email", in.Email) + return "", err + } + + return id, nil +} + +type UnsubscribeNewsletterInput struct { + Email string +} + +// UnsubscribeNewsletter sets subscribed=false for the given email. +// If the email doesn't exist, it returns nil (no row found) as an error from Scan. +func (r *QuantumAuthRepository) UnsubscribeNewsletter(ctx context.Context, in UnsubscribeNewsletterInput) (string, error) { + const query = ` + UPDATE newsletter + SET subscribed = false + WHERE email = $1 + RETURNING newsletter_id; + ` + + var id string + row, err := r.db.QueryRow(ctx, query, in.Email) + if err != nil { + log.Error("Error unsubscribing newsletter", "error", err, "email", in.Email) + return "", err + } + + if err := row.Scan(&id); err != nil { + log.Error("Error unsubscribing newsletter (scan)", "error", err, "email", in.Email) + return "", err + } + + return id, nil +} + +// GetNewsletterByEmail returns a newsletter subscription row by email. +func (r *QuantumAuthRepository) GetNewsletterByEmail(ctx context.Context, email string) (*NewsletterSubscription, error) { + const query = ` + SELECT newsletter_id, email, subscribed, created_at, updated_at + FROM newsletter + WHERE email = $1; + ` + + var n NewsletterSubscription + row, err := r.db.QueryRow(ctx, query, email) + if err != nil { + log.Error("Error getting newsletter subscription", "error", err, "email", email) + return nil, err + } + + if err := row.Scan( + &n.ID, + &n.Email, + &n.Subscribed, + &n.CreatedAt, + &n.UpdatedAt, + ); err != nil { + log.Error("Error getting newsletter subscription (scan)", "error", err, "email", email) + return nil, err + } + + return &n, nil +} + +// IsNewsletterSubscribed is a small helper to check if an email is subscribed. +// Returns (false, nil) if no row exists. +func (r *QuantumAuthRepository) IsNewsletterSubscribed(ctx context.Context, email string) (bool, error) { + const query = ` + SELECT subscribed + FROM newsletter + WHERE email = $1; + ` + + var subscribed bool + row, err := r.db.QueryRow(ctx, query, email) + if err != nil { + // You likely want to treat "no rows" as not subscribed at call site. + log.Error("Error checking newsletter subscription", "error", err, "email", email) + return false, err + } + + if err := row.Scan(&subscribed); err != nil { + log.Error("Error checking newsletter subscription (scan)", "error", err, "email", email) + return false, err + } + + return subscribed, nil +} diff --git a/internal/quantum/email/template_welcome.go b/internal/quantum/email/template_welcome.go index 488c622..1f80889 100644 --- a/internal/quantum/email/template_welcome.go +++ b/internal/quantum/email/template_welcome.go @@ -7,87 +7,168 @@ import ( "time" ) -// WelcomeEmailHTML returns a simple, nice-looking HTML email. -// Keep it table-based for best compatibility across email clients. -func WelcomeEmailHTML(username, docsURL string) string { +type emailTheme struct { + Primary500 string // sky-500 + Primary600 string // sky-600-ish + Accent500 string // blue-500 + Bg950 string // deep background + Bg900 string + Surface string // rgba white overlay + Border string // rgba border + Text string // near-white + Muted string + Subtle string +} + +func qaEmailTheme() emailTheme { + return emailTheme{ + Primary600: "#0284C7", + Primary500: "#0EA5E9", + Accent500: "#3B82F6", + + Bg950: "#030712", + Bg900: "#081022", + + // Use solid colors for email (no rgba) + Surface: "#0B1630", // slightly lighter navy surface + Border: "#1C2A4A", // subtle border + + Text: "#F1F5F9", // slate-100 + Muted: "#CBD5E1", // slate-300 + Subtle: "#94A3B8", // slate-400 + } +} + +// WelcomeEmailHTML returns a dark, theme-aligned HTML email. +// Table-based for best compatibility across email clients. +func WelcomeEmailHTML(username, docsURL, logoURL string) string { if strings.TrimSpace(docsURL) == "" { docsURL = "https://docs.quantumauth.io" } - u := strings.TrimSpace(username) if u == "" { u = "there" } - // Escape any user-provided data to avoid HTML injection. uEsc := html.EscapeString(u) docsEsc := html.EscapeString(docsURL) + logoEsc := html.EscapeString(strings.TrimSpace(logoURL)) + + th := qaEmailTheme() return fmt.Sprintf(` -
-| - |