diff --git a/api/.env.sample b/api/.env.sample index a02f793..a491044 100644 --- a/api/.env.sample +++ b/api/.env.sample @@ -1,2 +1,2 @@ -FIREBASE_CONFIG=firebase_json_config_remove_all_whitespace_and_make_it_one_liner +FIREBASE_CONFIG='firebase json config - uglify it to make it one liner' DB_CONN_STRING=host=localhost user=postgres password=password dbname=postgres port=5432 diff --git a/api/README.md b/api/README.md index 71728f7..207884f 100644 --- a/api/README.md +++ b/api/README.md @@ -1,10 +1,64 @@ # Ring Notify Backend -## Running Docker Image +A Go-based API server for sending Firebase Cloud Messaging (FCM) notifications to trigger calls. + +## Features + +- **User Management**: Create users with FCM tokens and get API keys +- **FCM Notifications**: Send push notifications to trigger calls + +### Accessing Swagger UI + +When the server is running, you can access the interactive Swagger UI at: + +``` +http://localhost:1323/swagger/index.html +``` + +### API Endpoints + +- `GET /` - Health check endpoint +- `POST /user/create` - Create a new user with FCM token +- `POST /notify/call` - Send FCM notification (requires Bearer token) + +### Regenerating Documentation + +To regenerate the Swagger documentation after making changes to the API: + +```bash +./generate-docs.sh +``` + +## Running the Application + +### Local Development + +1. Install dependencies: + +```bash +go mod download +``` + +2. Set up environment variables (copy `.env.sample` to `.env` and configure) + +3. Run the server: + +```bash +go run main.go +``` + +The server will start on `http://localhost:1323` + +### Docker + +#### Running Docker Image `docker container run --rm --env-file .env -p 1323:1323 ghcr.io/wtarit/ring-notify-backend:0.0.1` -## Build Docker Image +#### Build Docker Image + +Inside `api/` directory: -Inside `api/` directory -`docker build --tag ring-notify-backend .` +```bash +docker build --tag ring-notify-backend . +``` diff --git a/api/docs/docs.go b/api/docs/docs.go new file mode 100644 index 0000000..4f3ca02 --- /dev/null +++ b/api/docs/docs.go @@ -0,0 +1,233 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/": { + "get": { + "description": "Get the status of the API", + "produces": [ + "application/json" + ], + "tags": [ + "healthcheck" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.HealthCheckResponse" + } + } + } + } + }, + "/notify/call": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Send a Firebase Cloud Messaging notification to trigger a call", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notify" + ], + "summary": "Send FCM notification call", + "parameters": [ + { + "type": "string", + "default": "Bearer your-api-key-here", + "description": "Bearer token (API Key)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Call request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/notify.CallRequest" + } + } + ], + "responses": { + "200": { + "description": "Called", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/notify.ErrorResponse" + } + } + } + } + }, + "/user/create": { + "post": { + "description": "Create a new user with FCM token and get API key", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Create a new user", + "parameters": [ + { + "description": "User creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.CreateUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/user.CreateUserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "models.HealthCheckResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "active" + } + } + }, + "notify.CallRequest": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "example": "Notification from ESP32" + } + } + }, + "notify.ErrorResponse": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "example": "Token no longer valid" + } + } + }, + "user.CreateUserRequest": { + "type": "object", + "required": [ + "fcmToken" + ], + "properties": { + "fcmToken": { + "type": "string", + "example": "fcm-token" + } + } + }, + "user.CreateUserResponse": { + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "example": "00000000-0000-0000-0000-000000000000" + }, + "fcmKey": { + "type": "string", + "example": "fcm-token-example" + }, + "fcmKeyUpdated": { + "type": "string", + "example": "2025-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "00000000-0000-0000-0000-000000000000" + }, + "userCreated": { + "type": "string", + "example": "2025-01-01T00:00:00Z" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "0.0.1", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Ring Notify API", + Description: "API Specification for Ring Notify app.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/api/docs/swagger.json b/api/docs/swagger.json new file mode 100644 index 0000000..62e238b --- /dev/null +++ b/api/docs/swagger.json @@ -0,0 +1,207 @@ +{ + "swagger": "2.0", + "info": { + "description": "API Specification for Ring Notify app.", + "title": "Ring Notify API", + "contact": {}, + "version": "0.0.1" + }, + "paths": { + "/": { + "get": { + "description": "Get the status of the API", + "produces": [ + "application/json" + ], + "tags": [ + "healthcheck" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.HealthCheckResponse" + } + } + } + } + }, + "/notify/call": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Send a Firebase Cloud Messaging notification to trigger a call", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notify" + ], + "summary": "Send FCM notification call", + "parameters": [ + { + "type": "string", + "default": "Bearer your-api-key-here", + "description": "Bearer token (API Key)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Call request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/notify.CallRequest" + } + } + ], + "responses": { + "200": { + "description": "Called", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/notify.ErrorResponse" + } + } + } + } + }, + "/user/create": { + "post": { + "description": "Create a new user with FCM token and get API key", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Create a new user", + "parameters": [ + { + "description": "User creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.CreateUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/user.CreateUserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "models.HealthCheckResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "active" + } + } + }, + "notify.CallRequest": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "example": "Notification from ESP32" + } + } + }, + "notify.ErrorResponse": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "example": "Token no longer valid" + } + } + }, + "user.CreateUserRequest": { + "type": "object", + "required": [ + "fcmToken" + ], + "properties": { + "fcmToken": { + "type": "string", + "example": "fcm-token" + } + } + }, + "user.CreateUserResponse": { + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "example": "00000000-0000-0000-0000-000000000000" + }, + "fcmKey": { + "type": "string", + "example": "fcm-token-example" + }, + "fcmKeyUpdated": { + "type": "string", + "example": "2025-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "00000000-0000-0000-0000-000000000000" + }, + "userCreated": { + "type": "string", + "example": "2025-01-01T00:00:00Z" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml new file mode 100644 index 0000000..9aff951 --- /dev/null +++ b/api/docs/swagger.yaml @@ -0,0 +1,137 @@ +definitions: + models.HealthCheckResponse: + properties: + status: + example: active + type: string + type: object + notify.CallRequest: + properties: + text: + example: Notification from ESP32 + type: string + required: + - text + type: object + notify.ErrorResponse: + properties: + reason: + example: Token no longer valid + type: string + type: object + user.CreateUserRequest: + properties: + fcmToken: + example: fcm-token + type: string + required: + - fcmToken + type: object + user.CreateUserResponse: + properties: + apiKey: + example: 00000000-0000-0000-0000-000000000000 + type: string + fcmKey: + example: fcm-token-example + type: string + fcmKeyUpdated: + example: "2025-01-01T00:00:00Z" + type: string + id: + example: 00000000-0000-0000-0000-000000000000 + type: string + userCreated: + example: "2025-01-01T00:00:00Z" + type: string + type: object +info: + contact: {} + description: API Specification for Ring Notify app. + title: Ring Notify API + version: 0.0.1 +paths: + /: + get: + description: Get the status of the API + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.HealthCheckResponse' + summary: Health check + tags: + - healthcheck + /notify/call: + post: + consumes: + - application/json + description: Send a Firebase Cloud Messaging notification to trigger a call + parameters: + - default: Bearer your-api-key-here + description: Bearer token (API Key) + in: header + name: Authorization + required: true + type: string + - description: Call request payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/notify.CallRequest' + produces: + - application/json + responses: + "200": + description: Called + schema: + type: string + "400": + description: Bad Request + schema: + type: string + "403": + description: Forbidden + schema: + $ref: '#/definitions/notify.ErrorResponse' + security: + - BearerAuth: [] + summary: Send FCM notification call + tags: + - notify + /user/create: + post: + consumes: + - application/json + description: Create a new user with FCM token and get API key + parameters: + - description: User creation request + in: body + name: request + required: true + schema: + $ref: '#/definitions/user.CreateUserRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/user.CreateUserResponse' + "400": + description: Bad Request + schema: + type: string + summary: Create a new user + tags: + - user +securityDefinitions: + BearerAuth: + description: Type "Bearer" followed by a space and JWT token. + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/api/generate-docs.sh b/api/generate-docs.sh new file mode 100755 index 0000000..d028d1b --- /dev/null +++ b/api/generate-docs.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Generate Swagger documentation +echo "Generating Swagger documentation..." + +# Check if swag is installed +if ! command -v ~/go/bin/swag &> /dev/null; then + echo "Installing swag..." + go install github.com/swaggo/swag/cmd/swag@latest +fi + +# Generate docs +~/go/bin/swag init + +echo "Swagger documentation generated successfully!" +echo "You can view it at: http://localhost:1323/swagger/index.html" diff --git a/api/go.mod b/api/go.mod index 9329a9a..d0231b1 100644 --- a/api/go.mod +++ b/api/go.mod @@ -27,7 +27,10 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/envoyproxy/go-control-plane v0.13.4 // indirect @@ -35,9 +38,14 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-jose/go-jose/v4 v4.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -52,14 +60,19 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lyft/protoc-gen-star/v2 v2.0.4 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/spf13/afero v1.14.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/swaggo/echo-swagger v1.4.1 // indirect + github.com/swaggo/files/v2 v2.0.2 // indirect + github.com/swaggo/swag v1.16.6 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/zeebo/errs v1.4.0 // indirect @@ -73,19 +86,21 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect golang.org/dl v0.0.0-20250611185650-584cab5eeb14 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.41.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/tools v0.35.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/api/go.sum b/api/go.sum index 6a97d38..7588f49 100644 --- a/api/go.sum +++ b/api/go.sum @@ -113,8 +113,14 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 h1:wbMd4eG/fOhsCa6+IP8uEDvWF5vl7rNoUWmP5f72Tbs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0/go.mod h1:gdIm9TxRk5soClCwuB0FtdXsbqtw0aqPwBEurK9tPkw= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -155,6 +161,8 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -167,6 +175,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -276,6 +292,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -283,6 +301,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= @@ -295,6 +314,8 @@ github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJO github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/lyft/protoc-gen-star/v2 v2.0.4 h1:JDlNKttNIRd68AAIychs0AqEpO8/I/WYi01OQ7Raw6Q= github.com/lyft/protoc-gen-star/v2 v2.0.4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -321,6 +342,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= +github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= +github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= +github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -393,6 +420,8 @@ golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -431,6 +460,8 @@ golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -468,6 +499,8 @@ golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -496,6 +529,8 @@ golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -538,6 +573,8 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -553,6 +590,8 @@ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -614,6 +653,8 @@ golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -739,6 +780,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/main.go b/api/main.go index 226b3b8..b05923f 100644 --- a/api/main.go +++ b/api/main.go @@ -2,6 +2,7 @@ package main import ( "api/configs" + "api/models" "api/notify" "api/user" "log" @@ -11,8 +12,20 @@ import ( "github.com/go-playground/validator/v10" "github.com/joho/godotenv" "github.com/labstack/echo/v4" + echoSwagger "github.com/swaggo/echo-swagger" + + _ "api/docs" // This line is necessary for go-swagger to find your docs! ) +// @title Ring Notify API +// @version 0.0.1 +// @description API Specification for Ring Notify app. + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description Type "Bearer" followed by a space and JWT token. + func main() { if err := godotenv.Load(); err != nil && !os.IsNotExist(err) { log.Fatalln("Error loading .env") @@ -23,10 +36,26 @@ func main() { e := echo.New() e.Validator = &user.CustomValidator{Validator: validator.New()} - e.GET("/", func(c echo.Context) error { - return c.String(http.StatusOK, "Hello, World") - }) + + e.GET("/", healthCheck) e.POST("/notify/call", notify.Call) e.POST("/user/create", user.CreateUser) + + // Swagger endpoint + e.GET("/swagger/*", echoSwagger.WrapHandler) + e.Logger.Fatal(e.Start(":1323")) } + +// healthCheck godoc +// +// @Summary Health check +// @Description Get the status of the API +// @Tags healthcheck +// @Produce json +// @Success 200 {object} models.HealthCheckResponse +// @Router / [get] +func healthCheck(c echo.Context) error { + response := models.HealthCheckResponse{Status: "active"} + return c.JSON(http.StatusOK, response) +} diff --git a/api/models/healthcheck.go b/api/models/healthcheck.go new file mode 100644 index 0000000..cc04472 --- /dev/null +++ b/api/models/healthcheck.go @@ -0,0 +1,5 @@ +package models + +type HealthCheckResponse struct { + Status string `json:"status" example:"active"` +} diff --git a/api/models/user.go b/api/models/user.go index c2f5a0f..63507ce 100644 --- a/api/models/user.go +++ b/api/models/user.go @@ -7,11 +7,12 @@ import ( "gorm.io/gorm" ) +// User represents a user in the system type User struct { gorm.Model - ID uuid.UUID - APIKey string - FCMKey string - UserCreated time.Time - FCMKeyUpdated time.Time + ID uuid.UUID `json:"id" example:"uuid"` + APIKey string `json:"apiKey" example:"uuid"` + FCMKey string `json:"fcmKey" example:"fcm-token-example"` + UserCreated time.Time `json:"userCreated" example:"2023-01-01T00:00:00Z"` + FCMKeyUpdated time.Time `json:"fcmKeyUpdated" example:"2023-01-01T00:00:00Z"` } diff --git a/api/notify/notify.go b/api/notify/notify.go index 08794ed..529d595 100644 --- a/api/notify/notify.go +++ b/api/notify/notify.go @@ -13,13 +13,27 @@ import ( ) type CallRequest struct { - Text string `json:"text"` + Text string `json:"text" validate:"required" example:"Notification from ESP32"` } type ErrorResponse struct { - Reason string `json:"reason"` + Reason string `json:"reason" example:"Token no longer valid"` } +// Call godoc +// +// @Summary Send FCM notification call +// @Description Send a Firebase Cloud Messaging notification to trigger a call +// @Tags notify +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer token (API Key)" default(Bearer your-api-key-here) +// @Param request body CallRequest true "Call request payload" +// @Success 200 {string} string "Called" +// @Failure 400 {string} string "Bad Request" +// @Failure 403 {object} ErrorResponse +// @Security BearerAuth +// @Router /notify/call [post] func Call(c echo.Context) error { var callRequest CallRequest err := c.Bind(&callRequest) diff --git a/api/user/user.go b/api/user/user.go index 8e90b8b..fade59b 100644 --- a/api/user/user.go +++ b/api/user/user.go @@ -14,7 +14,14 @@ import ( type ( CreateUserRequest struct { - FcmToken string `json:"fcmToken" validate:"required"` + FcmToken string `json:"fcmToken" validate:"required" example:"fcm-token"` + } + CreateUserResponse struct { + ID string `json:"id" example:"00000000-0000-0000-0000-000000000000"` + APIKey string `json:"apiKey" example:"00000000-0000-0000-0000-000000000000"` + FCMKey string `json:"fcmKey" example:"fcm-token-example"` + UserCreated string `json:"userCreated" example:"2025-01-01T00:00:00Z"` + FCMKeyUpdated string `json:"fcmKeyUpdated" example:"2025-01-01T00:00:00Z"` } CustomValidator struct { Validator *validator.Validate @@ -29,6 +36,17 @@ func (cv *CustomValidator) Validate(i interface{}) error { return nil } +// CreateUser godoc +// +// @Summary Create a new user +// @Description Create a new user with FCM token and get API key +// @Tags user +// @Accept json +// @Produce json +// @Param request body CreateUserRequest true "User creation request" +// @Success 201 {object} CreateUserResponse +// @Failure 400 {string} string "Bad Request" +// @Router /user/create [post] func CreateUser(c echo.Context) error { var reqBody CreateUserRequest err := c.Bind(&reqBody)