diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1d11a0b0e50a8b5f87b4b40d6584fac89f2b93f8..48341c4406cbc06c416e1d6da36cc3b0dbcb295f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,56 +1,99 @@ +# GitLab CI configuration file for CI/CD pipeline + +# Define stages for the pipeline stages: - Build - Test - Deploy +# Default configuration default: + # Use Docker as the default image image: docker + # Enable Docker-in-Docker service services: - name: 'docker:dind' + # Before script actions before_script: + # Display Docker info - docker info +# Define variables variables: + # Set Docker TLS certificate directory DOCKER_TLS_CERTDIR: '/certs' + # Set path for downloading secure files SECURE_FILES_DOWNLOAD_PATH: './.secrets/' +# Build stage Build: stage: Build + # Before script actions before_script: + # Change directory to Go directory - cd ./Go/ + # Build script script: + # Build the Docker images using Docker Compose - docker compose build +# Test stage Test: stage: Test + # Before script actions before_script: + # Change directory to Go directory - cd ./Go/ + # Install required packages for testing - apk add --no-cache curl bash + # Download secure files - curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash > /dev/null 2>&1 + # Test script script: + # Start globeboard-test service and exit with the test result - docker compose up globeboard-test --exit-code-from globeboard-test + # Copy the test report from the container to the host - docker compose cp globeboard-test:/root/report.xml ./report.xml + # After script actions after_script: + # Change directory to Go directory - cd ./Go/ + # Stop the globeboard-test service - docker compose down globeboard-test + # Coverage regex coverage: '/coverage:\s+(\d+\.\d+)%\s+of statements/' + # Define artifacts to be stored artifacts: + # Define when to store the artifacts when: always + # Define paths of the artifacts paths: - ./Go/report.xml + # Define test reports reports: junit: ./Go/report.xml +# Deploy stage Deploy: stage: Deploy + # Before script actions before_script: + # Change directory to Go directory - cd ./Go/ + # Shut down all services - docker compose down + # Install required packages for deployment - apk add --no-cache curl bash + # Download secure files - curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash > /dev/null 2>&1 + # Deploy script script: + # Start the globeboard service in detached mode - docker compose up globeboard -d + # After script actions after_script: + # Display running Docker containers - docker ps + # Define when to trigger the deployment stage only: - main \ No newline at end of file diff --git a/Go/.env.example b/Go/.env.example index 9172e5b0cd77855d40171e079fb4bacafb7da8bc..abdfc6f580a14e70f43a457ba491410702faa7c4 100644 --- a/Go/.env.example +++ b/Go/.env.example @@ -1,3 +1,10 @@ +# Configuration file (.env) for setting environment variables + +# Port number for the HTTP server PORT=8080 + +# Path to the Firebase credentials file FIREBASE_CREDENTIALS_FILE=./super_secret_folder/database_file.json -FIRESTORE_PROJECT_ID=firestore_project_1 \ No newline at end of file + +# Firebase project ID for Firestore +FIRESTORE_PROJECT_ID=firestore_project_1 diff --git a/Go/Dockerfile b/Go/Dockerfile index 951b631dba50cd03b912eb971e6ed422b8b9b72b..15bfd86da988ff3573e284d1b2a7ed771ee152e7 100644 --- a/Go/Dockerfile +++ b/Go/Dockerfile @@ -1,39 +1,38 @@ # syntax=docker/dockerfile:1.2 + +# Define the base image and setup build environment FROM golang:1.22 AS builder # Define Authors LABEL authors="Torgrim Thorsen" -# Set destination for COPY +# Set the working directory WORKDIR /app -# Download Go modules +# Copy go modules for dependencies COPY go.mod go.sum ./ # Copy the source code. COPY ../ ./ -# Build +# Build the application RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/globeboard -# Use a minimal alpine image for the final build stage. +# Use a minimal alpine image for the final build stage FROM alpine:3.19 -# Install CA certificates. +# Install CA certificates RUN apk --no-cache add ca-certificates -# Set new Working Directory. +# Set the working directory WORKDIR /root/ -# Copy the pre-built binary file from the previous stage. +# Copy the pre-built binary file from the previous stage COPY --from=builder /app/main . -# Optional: -# To bind to a TCP port, runtime parameters must be supplied to the docker command. -# But we can document in the Dockerfile what ports -# the application is going to listen on by default. -# https://docs.docker.com/reference/dockerfile/#expose +# Expose the port on which the application will listen +# Note: This does not publish the port, but documents that the container will listen on this port EXPOSE 8080 -# Run +# Define the command to run the application CMD ["./main"] \ No newline at end of file diff --git a/Go/Dockerfile-test b/Go/Dockerfile-test index 63bff9429814cd6bdcb166d7434a68447fc9d9e4..9b357aa01b9393fe0b619385679473513224f399 100644 --- a/Go/Dockerfile-test +++ b/Go/Dockerfile-test @@ -1,35 +1,37 @@ # syntax=docker/dockerfile:1.2 + +# Define the base image and setup build environment FROM golang:1.22 AS builder # Define Authors LABEL authors="Torgrim Thorsen" -# Set destination for COPY +# Set the working directory WORKDIR /app -# Download Go modules +# Copy go modules for dependencies COPY go.mod go.sum ./ -# Copy the source code. +# Copy the source code COPY ../ ./ -# Build +# Build the test binary RUN CGO_ENABLED=0 GOOS=linux go test -cover -coverpkg=./... -c -installsuffix cgo -o test ./cmd/globeboard -# Use a minimal alpine image for the final build stage. +# Use a minimal alpine image for the final build stage FROM golang:1.22-alpine -# Install CA certificates and Go. +# Install CA certificates and Go RUN apk --no-cache add ca-certificates -# Install Report Tool. +# Install Go JUnit report tool RUN go install github.com/jstemmer/go-junit-report/v2@latest -# Set new Working Directory. +# Set the working directory WORKDIR /root/ -# Copy the pre-built binary file from the previous stage. +# Copy the pre-built test binary from the previous stage COPY --from=builder /app/test . -# Run -CMD ./test -test.v | go-junit-report > report.xml && ./test -test.v +# Define the command to run the tests and generate JUnit report +CMD ./test -test.v | go-junit-report > report.xml && ./test -test.v \ No newline at end of file diff --git a/Go/auth/auth.go b/Go/auth/auth.go index 1d505e1f83eefc8cf167d52c41370461c9a5be86..0288afa3410f05fed2c9cfd2cabc53ae0ffd9d36 100644 --- a/Go/auth/auth.go +++ b/Go/auth/auth.go @@ -1,3 +1,4 @@ +// Package authenticate provides functionality for initializing and accessing Firebase Authentication. package authenticate import ( @@ -10,17 +11,21 @@ import ( ) var ( - ctx = context.Background() - Client *auth.Client + ctx = context.Background() // Background context for Firebase operations + Client *auth.Client // Singleton Firebase Authentication client ) func init() { + // Load Firebase service account credentials from environment variable sa := option.WithCredentialsFile(os.Getenv("FIREBASE_CREDENTIALS_FILE")) + + // Initialize Firebase app with the loaded credentials app, err := firebase.NewApp(ctx, nil, sa) if err != nil { log.Panic("Firebase Failed to initialize: ", err) } + // Initialize the Firebase Authentication client Client, err = app.Auth(ctx) if err != nil { log.Panic("Firebase Failed to initialize Authentication client: ", err) diff --git a/Go/cmd/globeboard/app.go b/Go/cmd/globeboard/app.go index 5140bbf6de6fa312c067b5e882ee2ad6733f9427..498d2d4f85ecab3ccb00ac6e575e65f7c127b016 100644 --- a/Go/cmd/globeboard/app.go +++ b/Go/cmd/globeboard/app.go @@ -1,3 +1,4 @@ +// Package main is the entry point for the application. package main import ( @@ -12,8 +13,7 @@ import ( "os" ) -// fileExists checks if a file exists and is not a directory before we -// try using it to prevent further errors. +// fileExists checks if a file exists, and is not a directory. func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { @@ -23,11 +23,12 @@ func fileExists(filename string) bool { } func main() { - // confirm that the Firebase Credentials file is accessible, if not panic. + // Confirm that the Firebase Credentials file is accessible, if not, panic. if !fileExists(os.Getenv("FIREBASE_CREDENTIALS_FILE")) { log.Panic("Firebase Credentials file is not mounted") } defer func() { + // Close the Firestore client connection on application exit if err := db.Client.Close(); err != nil { log.Printf("Error closing Firestore client: %v", err) } @@ -42,16 +43,16 @@ func main() { // Define HTTP endpoints mux := http.NewServeMux() - mux.HandleFunc(Paths.Root, handlers.EmptyHandler) - mux.HandleFunc(Endpoints.UserRegistration, util.UserRegistrationHandler) - mux.HandleFunc(Endpoints.UserDeletionId, util.UserDeletionHandler) - mux.HandleFunc(Endpoints.ApiKey, util.APIKeyHandler) - mux.HandleFunc(Endpoints.RegistrationsID, dashboard.RegistrationsIdHandler) - mux.HandleFunc(Endpoints.Registrations, dashboard.RegistrationsHandler) - mux.HandleFunc(Endpoints.DashboardsID, dashboard.DashboardsIdHandler) - mux.HandleFunc(Endpoints.NotificationsID, dashboard.NotificationsIdHandler) - mux.HandleFunc(Endpoints.Notifications, dashboard.NotificationsHandler) - mux.HandleFunc(Endpoints.Status, dashboard.StatusHandler) + mux.HandleFunc(Paths.Root, handlers.EmptyHandler) // Root endpoint + mux.HandleFunc(Endpoints.UserRegistration, util.UserRegistrationHandler) // User registration endpoint + mux.HandleFunc(Endpoints.UserDeletionID, util.UserDeletionHandler) // User deletion endpoint + mux.HandleFunc(Endpoints.ApiKey, util.APIKeyHandler) // API key endpoint + mux.HandleFunc(Endpoints.RegistrationsID, dashboard.RegistrationsIdHandler) // Registrations by ID endpoint + mux.HandleFunc(Endpoints.Registrations, dashboard.RegistrationsHandler) // Registrations endpoint + mux.HandleFunc(Endpoints.DashboardsID, dashboard.DashboardsIdHandler) // Dashboards by ID endpoint + mux.HandleFunc(Endpoints.NotificationsID, dashboard.NotificationsIdHandler) // Notifications by ID endpoint + mux.HandleFunc(Endpoints.Notifications, dashboard.NotificationsHandler) // Notifications endpoint + mux.HandleFunc(Endpoints.Status, dashboard.StatusHandler) // Status endpoint // Start the HTTP server log.Println("Starting server on port " + port + " ...") diff --git a/Go/cmd/globeboard/app_test.go b/Go/cmd/globeboard/app_test.go index 527daf7fc1fb89e815e6caf8b8df9c9898cff669..aee5b2f29134cc3e8c0cce05c37e5e14763f59de 100644 --- a/Go/cmd/globeboard/app_test.go +++ b/Go/cmd/globeboard/app_test.go @@ -50,7 +50,7 @@ func init() { mux.HandleFunc(Paths.Root, handlers.EmptyHandler) mux.HandleFunc(Endpoints.UserRegistration, util.UserRegistrationHandler) - mux.HandleFunc(Endpoints.UserDeletionId, util.UserDeletionHandler) + mux.HandleFunc(Endpoints.UserDeletionID, util.UserDeletionHandler) mux.HandleFunc(Endpoints.ApiKey, util.APIKeyHandler) mux.HandleFunc(Endpoints.RegistrationsID, dashboard.RegistrationsIdHandler) mux.HandleFunc(Endpoints.Registrations, dashboard.RegistrationsHandler) @@ -219,7 +219,7 @@ func TestStatusGet(t *testing.T) { func TestNotificationsHandlerPostDiscord(t *testing.T) { notificationData := []byte(`{ - "url": "https://localhost/discord", + "url": "https://discord.com", "country": "", "event": ["INVOKE","REGISTER","CHANGE","DELETE"] }`) @@ -249,7 +249,7 @@ func TestNotificationsHandlerPostDiscord(t *testing.T) { func TestNotificationsHandlerPost(t *testing.T) { notificationData := []byte(`{ - "url": "https://localhost/", + "url": "https://google.com/", "country": "", "event": ["INVOKE","DELETE"] }`) @@ -441,7 +441,6 @@ func TestDashboardIdHandlerGet(t *testing.T) { if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } - t.Log(rr.Body.String()) } func TestDashboardIdHandlerGetMinimal(t *testing.T) { @@ -457,7 +456,6 @@ func TestDashboardIdHandlerGetMinimal(t *testing.T) { if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } - t.Log(rr.Body.String()) } func TestRegistrationsIdHandlerDeleteMinimal(t *testing.T) { @@ -1342,7 +1340,111 @@ func TestRegistrationsIdHandlerPostNoFeatures(t *testing.T) { if status := rr.Code; status != http.StatusBadRequest { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) - t.Log(rr.Body.String()) + } +} + +func TestRegistrationsIdHandlerPostEmptyFeatures(t *testing.T) { + patchData := []byte(`{ + "country": "Sweden", + "features": {} + }`) + + req, err := http.NewRequest(http.MethodPost, Endpoints.Registrations+"?token="+token, bytes.NewBuffer(patchData)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) + } +} + +func TestRegistrationsIdHandlerPostWrongIso(t *testing.T) { + patchData := []byte(`{ + "isocode": "sweden", + "features": { + "temperature": true, + "precipitation": true, + "capital": true, + "coordinates": true, + "population": true, + "area": true, + "targetCurrencies": ["jpy", "nok", "eur","gbp"] + } + }`) + + req, err := http.NewRequest(http.MethodPost, Endpoints.Registrations+"?token="+token, bytes.NewBuffer(patchData)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) + } +} + +func TestRegistrationsIdHandlerPostWrongCountry(t *testing.T) { + patchData := []byte(`{ + "country": "no", + "features": { + "temperature": true, + "precipitation": true, + "capital": true, + "coordinates": true, + "population": true, + "area": true, + "targetCurrencies": ["jpy", "nok", "eur","gbp"] + } + }`) + + req, err := http.NewRequest(http.MethodPost, Endpoints.Registrations+"?token="+token, bytes.NewBuffer(patchData)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) + } +} + +func TestRegistrationsIdHandlerPostCountryIsoMismatch(t *testing.T) { + patchData := []byte(`{ + "country": "sweden", + "isocode": "gb", + "features": { + "temperature": true, + "precipitation": true, + "capital": true, + "coordinates": true, + "population": true, + "area": true, + "targetCurrencies": ["jpy", "nok", "eur","gbp"] + } + }`) + + req, err := http.NewRequest(http.MethodPost, Endpoints.Registrations+"?token="+token, bytes.NewBuffer(patchData)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) } } @@ -1371,7 +1473,6 @@ func TestRegistrationsIdHandlerPatchCountry(t *testing.T) { if status := rr.Code; status != http.StatusBadRequest { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) - t.Log(rr.Body.String()) } } diff --git a/Go/db/db.go b/Go/db/db.go index b0b574133e20ac4e9aea8dcddd5480407e2a9934..bc35df0ad6207cb8cbcb94e1495636979c9c210e 100644 --- a/Go/db/db.go +++ b/Go/db/db.go @@ -1,3 +1,4 @@ +// Package db provides data access functions for interacting with Firestore. package db import ( @@ -18,35 +19,37 @@ import ( ) const ( - FirebaseClosingErr = "Error closing access to firestore: %v\n" - IterationFailed = "failed to iterate over query results: %v\n" - ParsingError = "Error parsing document: %v\n" + // IterationFailed Error for when iteration over Firestore results fails. + IterationFailed = "failed to iterate over query results: %v\n" ) var ( - // Use a context for Firestore operations - ctx = context.Background() - Client *firestore.Client - err error + ctx = context.Background() // Global context for Firestore operations, used across all Firestore calls. + Client *firestore.Client // Singleton Firestore client. + err error // Variable to handle errors globally within this package. ) +// init initializes the Firestore client using environment variables for credentials and project ID. func init() { - sa := option.WithCredentialsFile(os.Getenv("FIREBASE_CREDENTIALS_FILE")) - Client, err = firestore.NewClient(ctx, os.Getenv("FIRESTORE_PROJECT_ID"), sa) + sa := option.WithCredentialsFile(os.Getenv("FIREBASE_CREDENTIALS_FILE")) // Set up the credential file from environment. + Client, err = firestore.NewClient(ctx, os.Getenv("FIRESTORE_PROJECT_ID"), sa) // Create a new Firestore client. if err != nil { - log.Panic("Firestore was unable to initialize: ", err) + log.Panic("Firestore was unable to initialize: ", err) // Panic if Firestore client initialization fails. } } +// TestDBConnection tests the Firestore database connection by attempting to write and immediately read a document. func TestDBConnection() string { - collectionID := "Connectivity" - documentID := "DB_Connection_Test" + collectionID := "Connectivity" // Define the collection ID for connection tests. + documentID := "DB_Connection_Test" // Define the document ID for connection tests. + // Attempt to set a document in the Firestore collection, tagging it with server timestamp. _, err = Client.Collection(collectionID).Doc(documentID).Set(ctx, map[string]interface{}{ "PSA": "DO NOT DELETE THIS DOCUMENT!", "lastChecked": firestore.ServerTimestamp, }, firestore.MergeAll) + // Handle potential errors and map them to HTTP status codes. grpcStatusCode := status.Code(err) switch grpcStatusCode { case codes.OK: @@ -58,9 +61,8 @@ func TestDBConnection() string { case codes.PermissionDenied: return fmt.Sprintf("%d %s", http.StatusForbidden, http.StatusText(http.StatusForbidden)) case codes.NotFound: - // This might indicate the collection or document does not exist, - //which for this purpose is treated as a connection success - // since the error was Firestore-specific and not network or permission related. + // Treat not found as OK for this operation; it indicates collection/document was simply not found. + // Another error in-of-itself. return fmt.Sprintf("%d %s", http.StatusOK, http.StatusText(http.StatusOK)) case codes.ResourceExhausted: return fmt.Sprintf("%d %s", http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)) @@ -71,346 +73,370 @@ func TestDBConnection() string { case codes.Unknown, codes.Internal: return fmt.Sprintf("%d %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) default: - // For any other codes, return a generic HTTP 500 error return fmt.Sprintf("%d %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } - } +// AddApiKey adds a new API key to Firestore, ensuring it does not already exist for the provided user (UUID). func AddApiKey(docID, UUID string, key string) error { - ref := Client.Collection(Firestore.ApiKeyCollection) + ref := Client.Collection(Firestore.ApiKeyCollection) // Reference to the APIKey collection in Firestore. + // Query for existing API keys with the same UUID. iter := ref.Where("UUID", "==", UUID).Limit(1).Documents(ctx) - defer iter.Stop() + defer iter.Stop() // Ensure the iterator is cleaned up properly. for { _, err := iter.Next() if errors.Is(err, iterator.Done) { - break + break // Exit the loop if all documents have been iterated over. } if err != nil { - return fmt.Errorf(IterationFailed, err) + return fmt.Errorf(IterationFailed, err) // Return formatted error if iteration fails. } err = errors.New("API key is already registered to user") - return err + return err // Return error if an existing key is found. } - apiKeys := structs.APIKey{ - UUID: UUID, - APIKey: key, - } + apiKeys := structs.APIKey{UUID: UUID, APIKey: key} // Create an APIKey struct to be saved. - _, err = ref.Doc(docID).Set(ctx, apiKeys) + _, err = ref.Doc(docID).Set(ctx, apiKeys) // Set the APIKey document in Firestore. if err != nil { err := fmt.Errorf("error saving API key to Database: %v", err) - return err + return err // Return formatted error if setting the document fails. } - return nil + log.Printf("API key %s created successfully.", apiKeys.APIKey) // Log success. + return nil // Return nil error on success. } +// DeleteApiKey deletes an API key from Firestore based on UUID and key value. func DeleteApiKey(UUID, apiKey string) error { - ref := Client.Collection(Firestore.ApiKeyCollection) + ref := Client.Collection(Firestore.ApiKeyCollection) // Reference to the APIKey collection in Firestore. + // Query for the API key document based on UUID and key. iter := ref.Where("UUID", "==", UUID).Where("APIKey", "==", apiKey).Limit(1).Documents(ctx) - defer iter.Stop() + defer iter.Stop() // Ensure the iterator is cleaned up properly. - var docID string + var docID string // Variable to store the document ID of the found API key. for { doc, err := iter.Next() if errors.Is(err, iterator.Done) { - break + break // Exit the loop if all documents have been iterated over. } if err != nil { - return fmt.Errorf(IterationFailed, err) + return fmt.Errorf(IterationFailed, err) // Return formatted error if iteration fails. } - docID = doc.Ref.ID + docID = doc.Ref.ID // Store the document ID. } if docID == "" { - return errors.New("API key not found") - } + return errors.New("API key not found") // Return error if no document ID was found. + } // Handle if the API key was not found. - _, err = ref.Doc(docID).Delete(ctx) + _, err = ref.Doc(docID).Delete(ctx) // Delete the document from Firestore. if err != nil { - return fmt.Errorf("failed to delete API Key: %v", err) + return fmt.Errorf("failed to delete API Key: %v", err) // Return formatted error if delete fails. } - return nil + log.Printf("API key %s deleted successfully.", apiKey) // Log success. + return nil // Return nil error on success. } +// GetAPIKeyUUID retrieves the UUID associated with a specific API key from Firestore. func GetAPIKeyUUID(apiKey string) string { - ref := Client.Collection(Firestore.ApiKeyCollection) + ref := Client.Collection(Firestore.ApiKeyCollection) // Reference to the APIKey collection in Firestore. + // Query for the API key document based on the key value. iter := ref.Where("APIKey", "==", apiKey).Limit(1).Documents(ctx) - defer iter.Stop() - - var key structs.APIKey + defer iter.Stop() // Ensure the iterator is cleaned up properly. + var key structs.APIKey // Variable to store the API key data. for { doc, err := iter.Next() if errors.Is(err, iterator.Done) { - break + break // Exit the loop if all documents have been iterated over. } if err != nil { log.Printf(IterationFailed, err) - return "" + return "" // Return an empty string on error. } if err := doc.DataTo(&key); err != nil { log.Println("Error parsing document:", err) - return "" + return "" // Return an empty string on parsing error. } } - _, err = authenticate.Client.GetUser(ctx, key.UUID) + _, err = authenticate.Client.GetUser(ctx, key.UUID) // Authenticate the user based on UUID. if err != nil { log.Println("Error getting user:", err) - return "" + return "" // Return an empty string if user authentication fails. } else { - return key.UUID + log.Printf("UUID: %s successfully retrieved from API key: %s.", key.UUID, key.APIKey) + return key.UUID // Return the UUID on success. } } +// AddRegistration adds a new registration document to Firestore. func AddRegistration(docID string, data *structs.CountryInfoInternal) error { - ref := Client.Collection(Firestore.RegistrationCollection) + ref := Client.Collection(Firestore.RegistrationCollection) // Reference to the Registration collection. + // Set the registration document in Firestore with the given ID and data. _, err = ref.Doc(docID).Set(ctx, map[string]interface{}{ "ID": data.ID, "UUID": data.UUID, "Country": data.Country, "IsoCode": data.IsoCode, "Features": data.Features, - "Lastchange": firestore.ServerTimestamp, + "Lastchange": firestore.ServerTimestamp, // Use server timestamp to record last change. }) if err != nil { - return err + return err // Return error if the document set operation fails. } - return nil + log.Printf("Registration documents %s created successfully.", data.ID) + return nil // Return nil if the addition is successful. } +// GetRegistrations retrieves all registration documents for a given user (UUID) from Firestore. func GetRegistrations(UUID string) ([]*structs.CountryInfoInternal, error) { - ref := Client.Collection(Firestore.RegistrationCollection) + ref := Client.Collection(Firestore.RegistrationCollection) // Reference to the Registration collection. - docs, _ := ref.Where("UUID", "==", UUID).OrderBy("Lastchange", firestore.Desc).Documents(ctx).GetAll() + // Query and retrieve all documents where 'UUID' matches, ordered by 'Lastchange' descending. + docs, err := ref.Where("UUID", "==", UUID).OrderBy("Lastchange", firestore.Desc).Documents(ctx).GetAll() if err != nil { - log.Printf("Error fetching Registration: %v\n", err) - return nil, err + return nil, err // Return error if the fetch operation fails. } - var cis []*structs.CountryInfoInternal + var cis []*structs.CountryInfoInternal // Slice to store the fetched documents. for _, doc := range docs { var ci *structs.CountryInfoInternal if err := doc.DataTo(&ci); err != nil { - log.Printf(ParsingError, err) - return nil, err + return nil, err // Return error if parsing any document fails. } - cis = append(cis, ci) + cis = append(cis, ci) // Append the parsed document to the slice. } - - return cis, nil + log.Printf("Registration documents for user: %s retrieved successfully.", UUID) + return cis, nil // Return the slice of documents. } +// GetSpecificRegistration retrieves a specific registration document by ID and UUID from Firestore. func GetSpecificRegistration(ID, UUID string) (*structs.CountryInfoInternal, error) { - ref := Client.Collection(Firestore.RegistrationCollection) + ref := Client.Collection(Firestore.RegistrationCollection) // Reference to the Registration collection. + // Query for the specific document with the given 'ID' and 'UUID'. iter := ref.Where("ID", "==", ID).Where("UUID", "==", UUID).Limit(1).Documents(ctx) - defer iter.Stop() + defer iter.Stop() // Ensure the iterator is cleaned up properly. - var ci *structs.CountryInfoInternal + var ci *structs.CountryInfoInternal // Variable to store the fetched document. for { doc, err := iter.Next() if errors.Is(err, iterator.Done) { - break + break // Exit the loop if all documents have been iterated over. } if err != nil { - return nil, fmt.Errorf(IterationFailed, err) + return nil, fmt.Errorf(IterationFailed, err) // Return formatted error if iteration fails. } if err := doc.DataTo(&ci); err != nil { - log.Println("Error retrieving document:", err) - return nil, err + return nil, err // Return error if parsing the document fails. } - return ci, nil + log.Printf("Registration document %s retrieved successfully.", ci.ID) + return ci, nil // Return the parsed document. } - return nil, errors.New("no registration with that ID was found") + return nil, errors.New("no registration with that ID was found") // Return error if no document is found. } +// UpdateRegistration updates a specific registration document by ID and UUID in Firestore. func UpdateRegistration(ID, UUID string, data *structs.CountryInfoInternal) error { - ref := Client.Collection(Firestore.RegistrationCollection) + ref := Client.Collection(Firestore.RegistrationCollection) // Reference to the Registration collection. + // Query for the specific document to update. iter := ref.Where("ID", "==", ID).Where("UUID", "==", UUID).Limit(1).Documents(ctx) - defer iter.Stop() + defer iter.Stop() // Ensure the iterator is cleaned up properly. for { doc, err := iter.Next() if errors.Is(err, iterator.Done) { - break + break // Exit the loop if all documents have been iterated over. } if err != nil { - return fmt.Errorf(IterationFailed, err) + return fmt.Errorf(IterationFailed, err) // Return formatted error if iteration fails. } + // Update the document with the provided data. _, err = ref.Doc(doc.Ref.ID).Set(ctx, map[string]interface{}{ "ID": data.ID, "UUID": data.UUID, "Country": data.Country, "IsoCode": data.IsoCode, "Features": data.Features, - "Lastchange": firestore.ServerTimestamp, + "Lastchange": firestore.ServerTimestamp, // Use server timestamp to update 'Lastchange'. }) if err != nil { - log.Printf("Error saving data to database: %v\n", err) - return err + return err // Return error if the document update operation fails. } - return nil + log.Printf("Registration document %s patched successfully.", doc.Ref.ID) + return nil // Return nil error if the update is successful. } - return errors.New("no registration with that ID was found") + return errors.New("no registration with that ID was found") // Return error if no document is found. } +// DeleteRegistration deletes a specific registration document by ID and UUID from Firestore. func DeleteRegistration(ID, UUID string) error { - ref := Client.Collection(Firestore.RegistrationCollection) + ref := Client.Collection(Firestore.RegistrationCollection) // Reference to the Registration collection. + // Query for the specific document to delete. iter := ref.Where("ID", "==", ID).Where("UUID", "==", UUID).Limit(1).Documents(ctx) - defer iter.Stop() + defer iter.Stop() // Ensure the iterator is cleaned up properly. - var docID string + var docID string // Variable to store the document ID of the found registration. for { doc, err := iter.Next() if errors.Is(err, iterator.Done) { - break + break // Exit the loop if all documents have been iterated over. } if err != nil { - return fmt.Errorf(IterationFailed, err) + return fmt.Errorf(IterationFailed, err) // Return formatted error if iteration fails. } - docID = doc.Ref.ID + docID = doc.Ref.ID // Store the document ID. } if docID == "" { - return errors.New("ID match was not found") + return fmt.Errorf("ID match was not found") // Return error if no document ID was found. } - _, err = ref.Doc(docID).Delete(ctx) + _, err = ref.Doc(docID).Delete(ctx) // Delete the document from Firestore. if err != nil { - return fmt.Errorf("failed to delete document: %v", err) + return fmt.Errorf("failed to delete document: %v", err) // Return formatted error if delete fails. } log.Printf("Registration document %s deleted successfully\n", docID) - return nil + return nil // Return nil if the deletion is successful. } -func AddWebhook(docID string, webhook *structs.WebhookGet) error { - ref := Client.Collection(Firestore.WebhookCollection) +// AddWebhook creates a new webhook entry in Firestore. +func AddWebhook(docID string, webhook *structs.WebhookInternal) error { + ref := Client.Collection(Firestore.WebhookCollection) // Reference to the Webhook collection in Firestore. + // Set the webhook document with the provided ID and data. _, err = ref.Doc(docID).Set(ctx, webhook) if err != nil { - log.Printf(FirebaseClosingErr, err) - return err + return err // Return error if addition fails. } - return nil + log.Printf("Webhook %s created successfully.", webhook.ID) // Log success. + return nil // Return nil error on successful addition. } -func GetAllWebhooks() ([]structs.WebhookGet, error) { - ref := Client.Collection(Firestore.WebhookCollection) +// GetAllWebhooks retrieves all webhook entries from Firestore. +func GetAllWebhooks() ([]structs.WebhookInternal, error) { + ref := Client.Collection(Firestore.WebhookCollection) // Reference to the Webhook collection. + // Retrieve all documents from the webhook collection. docs, err := ref.Documents(ctx).GetAll() if err != nil { - log.Printf("Error fetching all stored Webhooks: %v\n", err) - return nil, err + return nil, err // Return the error if the fetch operation fails. } - var webhooks []structs.WebhookGet + var webhooks []structs.WebhookInternal // Slice to store the fetched webhook documents. for _, doc := range docs { - var webhook structs.WebhookGet + var webhook structs.WebhookInternal if err := doc.DataTo(&webhook); err != nil { - log.Printf(ParsingError, err) - return nil, err + return nil, err // Return error if parsing any document fails. } - webhooks = append(webhooks, webhook) + webhooks = append(webhooks, webhook) // Append the parsed document to the slice. } - return webhooks, nil + log.Printf("All Webhooks retrieved successfully.") // Log success. + return webhooks, nil // Return the slice of webhook documents. } +// GetWebhooksUser retrieves all webhook entries for a specific user (UUID) from Firestore. func GetWebhooksUser(UUID string) ([]structs.WebhookResponse, error) { - ref := Client.Collection(Firestore.WebhookCollection) + ref := Client.Collection(Firestore.WebhookCollection) // Reference to the Webhook collection. + // Query and retrieve all documents from the webhook collection where 'UUID' matches the provided UUID. docs, err := ref.Where("UUID", "==", UUID).Documents(ctx).GetAll() if err != nil { - log.Printf("Error fetching users webhooks: %v\n", err) - return nil, err + return nil, err // Return the error if the fetch operation fails. } - var webhooks []structs.WebhookResponse + var webhooks []structs.WebhookResponse // Slice to store the fetched webhook documents for the user. for _, doc := range docs { var webhook structs.WebhookResponse if err := doc.DataTo(&webhook); err != nil { - log.Printf(ParsingError, err) - return nil, err + return nil, err // Return error if parsing any document fails. } - webhooks = append(webhooks, webhook) + webhooks = append(webhooks, webhook) // Append the parsed document to the slice. } - return webhooks, nil + log.Printf("Webhooks retrieved successfully for user: %s.", UUID) // Log success. + return webhooks, nil // Return the slice of webhook documents for the user. } +// GetSpecificWebhook retrieves a specific webhook entry by ID and UUID from Firestore. func GetSpecificWebhook(ID, UUID string) (*structs.WebhookResponse, error) { - ref := Client.Collection(Firestore.WebhookCollection) + ref := Client.Collection(Firestore.WebhookCollection) // Reference to the Webhook collection. + // Query for the specific document with the given 'ID' and 'UUID'. iter := ref.Where("ID", "==", ID).Where("UUID", "==", UUID).Limit(1).Documents(ctx) - defer iter.Stop() + defer iter.Stop() // Ensure the iterator is cleaned up properly. - var webhook *structs.WebhookResponse + var webhook *structs.WebhookResponse // Variable to store the fetched document. for { doc, err := iter.Next() if errors.Is(err, iterator.Done) { - break + break // Exit the loop if all documents have been iterated over. } if err != nil { - return nil, fmt.Errorf(IterationFailed, err) + return nil, fmt.Errorf(IterationFailed, err) // Return formatted error if iteration fails. } if err := doc.DataTo(&webhook); err != nil { - return nil, err + return nil, err // Return error if parsing the document fails. } - return webhook, nil + + log.Printf("Webhook %s retrieved successfully.", webhook.ID) // Log success. + return webhook, nil // Return the parsed document. } - return nil, errors.New("no document with that ID was found") + return nil, errors.New("no document with that ID was found") // Return error if no document is found. } +// DeleteWebhook deletes a specific webhook entry by ID and UUID from Firestore. func DeleteWebhook(ID, UUID string) error { - ref := Client.Collection(Firestore.WebhookCollection) + ref := Client.Collection(Firestore.WebhookCollection) // Reference to the Webhook collection. + // Query for the specific document to delete. iter := ref.Where("ID", "==", ID).Where("UUID", "==", UUID).Limit(1).Documents(ctx) - defer iter.Stop() + defer iter.Stop() // Ensure the iterator is cleaned up properly. - var docID string + var docID string // Variable to store the document ID of the found webhook. for { doc, err := iter.Next() if errors.Is(err, iterator.Done) { - break + break // Exit the loop if all documents have been iterated over. } if err != nil { - return fmt.Errorf(IterationFailed, err) + return fmt.Errorf(IterationFailed, err) // Return formatted error if iteration fails. } - docID = doc.Ref.ID + docID = doc.Ref.ID // Store the document ID. } if docID == "" { - return fmt.Errorf("ID match was not found") + return fmt.Errorf("ID match was not found") // Return error if no document ID was found. } - _, err = ref.Doc(docID).Delete(ctx) + _, err = ref.Doc(docID).Delete(ctx) // Delete the document from Firestore. if err != nil { - return fmt.Errorf("failed to delete document: %v", err) + log.Print(err) // Log errors during the delete operation. + return fmt.Errorf("failed to delete document: %v", err) // Return formatted error if delete fails. } - log.Printf("Webhook %s deleted successfully\n\n", docID) - return nil + log.Printf("Webhook %s deleted successfully.", docID) // Log success. + return nil // Return nil error on successful operation. } diff --git a/Go/docker-compose.yml b/Go/docker-compose.yml index f69c344148a291061b20779c9f96f7d48d3b1a5c..4eb0059d28ac14d79ec1df4dd6ad02700c458e47 100644 --- a/Go/docker-compose.yml +++ b/Go/docker-compose.yml @@ -1,35 +1,63 @@ +# Docker Compose configuration file for defining services + services: + # Main service for globeboard application globeboard: + # Docker image for globeboard image: globeboard + # Build configuration build: + # Build context context: . + # Dockerfile location dockerfile: Dockerfile + # Ports mapping ports: - '${PORT}:${PORT}' + # Restart policy restart: unless-stopped + # Environment variables environment: + # Port number PORT: ${PORT} + # Path to Firebase credentials file FIREBASE_CREDENTIALS_FILE: /run/secrets/Firebase + # Firestore project ID FIRESTORE_PROJECT_ID: ${FIRESTORE_PROJECT_ID} + # Volumes volumes: - ./web:/root/web:ro + # Secrets configuration secrets: - Firebase + # Test service for globeboard globeboard-test: + # Docker image for globeboard test image: globeboard-test + # Build configuration build: + # Build context context: . + # Dockerfile location dockerfile: Dockerfile-test + # Restart policy restart: no + # Environment variables environment: + # Path to Firebase credentials file FIREBASE_CREDENTIALS_FILE: /run/secrets/Firebase + # Firestore project ID FIRESTORE_PROJECT_ID: ${FIRESTORE_PROJECT_ID} + # Volumes volumes: - ./web:/root/web:ro + # Secrets configuration secrets: - Firebase +# Secret definition secrets: Firebase: + # Path to Firebase credentials file file: ${FIREBASE_CREDENTIALS_FILE} \ No newline at end of file diff --git a/Go/internal/func/dashboardFunctions.go b/Go/internal/func/dashboardFunctions.go index 6fc55ee01a994adf2975de64196b668bdec17b65..f6f0232549daf2e1de08f1648068c2a65bf5f129 100644 --- a/Go/internal/func/dashboardFunctions.go +++ b/Go/internal/func/dashboardFunctions.go @@ -1,3 +1,4 @@ +// Package _func provides developer-made utility functions for use within the application. package _func import ( @@ -13,261 +14,321 @@ import ( "strings" ) +// OpenMeteoTemp structure defines the JSON structure for temperature response from the OpenMeteo API. type OpenMeteoTemp struct { Current struct { - Temperature float64 `json:"temperature_2m"` + Temperature float64 `json:"temperature_2m"` // Current temperature (2 meters above the ground). } `json:"current"` } const ( - alphaCodes = "alpha?codes=" + alphaCodes = "alpha?codes=" // URL parameter for filtering requests by country ISO codes. + ResponseBodyCloseError = "Error closing response body: %v" // Log format for errors closing the response body. ) +// GetTemp fetches the current temperature for the specified coordinates using the OpenMeteo API. func GetTemp(coordinates structs.CoordinatesDashboard) (float64, error) { - response, err := http.Get(External.OpenMeteoAPI + "?latitude=" + (coordinates.Latitude) + "&longitude=" + (coordinates.Longitude) + "¤t=temperature_2m") + // Constructing the URL to call the OpenMeteo API with query parameters for latitude and longitude. + response, err := http.Get(External.OpenMeteoAPI + "?latitude=" + coordinates.Latitude + "&longitude=" + coordinates.Longitude + "¤t=temperature_2m") if err != nil { - return 0, err + log.Print(err) + return 0, err // Return zero temperature and the error if the GET request fails. } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { - log.Print("Error closing Open-Meteo api response body @ dashboardFunctions:GetTemp: ", err) + log.Printf(ResponseBodyCloseError, err) } - }(response.Body) + }(response.Body) // Ensure the response body is closed after the function returns. - body, err := io.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) // Read the response body. if err != nil { - return 0, err + log.Print(err) + return 0, err // Return zero temperature and the error if an issue with reading the response occurs. } - var openMeteo OpenMeteoTemp - if err := json.Unmarshal(body, &openMeteo); err != nil { + var openMeteo OpenMeteoTemp // Declaring a variable to store unmarshalled JSON data. + if err := json.Unmarshal(body, &openMeteo); err != nil { // Unmarshal JSON data into OpenMeteoTemp struct. return 0, err } - temp := openMeteo.Current.Temperature + temp := openMeteo.Current.Temperature // Extracting the temperature from the struct. - return temp, nil + return temp, nil // Return the fetched temperature. } +// OpenMeteoPrecipitation structure defines the JSON structure for precipitation response from OpenMeteo API. type OpenMeteoPrecipitation struct { Current struct { - Precipitation float64 `json:"precipitation"` + Precipitation float64 `json:"precipitation"` // Current precipitation amount. } `json:"current"` } -func GetPrecipitation(coordinates *structs.CoordinatesDashboard) (float64, error) { - response, err := http.Get(External.OpenMeteoAPI + "?latitude=" + (coordinates.Latitude) + "&longitude=" + (coordinates.Longitude) + "¤t=precipitation") +// GetPrecipitation fetches the current precipitation for the specified coordinates. +func GetPrecipitation(coordinates structs.CoordinatesDashboard) (float64, error) { + // Construct the API request URL with coordinates. + response, err := http.Get(External.OpenMeteoAPI + "?latitude=" + coordinates.Latitude + "&longitude=" + coordinates.Longitude + "¤t=precipitation") if err != nil { - return 0, err + log.Print(err) + return 0, err // Return zero precipitations and the error if the GET request fails. } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { - log.Print("Error closing Open-Meteo api response body @ dashboardFunctions:GetTemp: ", err) + log.Printf(ResponseBodyCloseError, err) } - }(response.Body) + }(response.Body) // Ensuring the response body is properly closed. - body, err := io.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) // Read the response body. if err != nil { - return 0, err + log.Print(err) + return 0, err // Return zero precipitations and the error if an issue with reading the response occurs. } - var openMeteo OpenMeteoPrecipitation - if err := json.Unmarshal(body, &openMeteo); err != nil { + var openMeteo OpenMeteoPrecipitation // Struct to hold the precipitation data. + if err := json.Unmarshal(body, &openMeteo); err != nil { // Parse the JSON response into the struct. return 0, err } - precipitation := openMeteo.Current.Precipitation + precipitation := openMeteo.Current.Precipitation // Extract the precipitation value. - return precipitation, nil + return precipitation, nil // Return the precipitation data. } +// Country structure for parsing country capital information from a JSON response. type Country struct { - Capital []string `json:"capital"` + Capital []string `json:"capital"` // Capital is expected as an array of strings, + // though typically containing only one element. } +// GetCapital fetches the capital city of a country identified by its ISO code. func GetCapital(isocode string) (string, error) { + // Construct the request URL with ISO code and fields parameter. response, err := http.Get(External.CountriesAPI + alphaCodes + isocode + "&fields=capital") if err != nil { - return "nil", err + log.Print(err) + return "Earth", err // Return "Earth" and the error if GET requests fails. } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { - log.Print("Error closing countries api response body @ dashboardFunctions:GetCapital: ", err) + log.Printf(ResponseBodyCloseError, err) } - }(response.Body) + }(response.Body) // Ensure to close the response body on function exit. - body, err := io.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) // Read the entire response body. if err != nil { - return "", err + log.Print(err) + return "Earth", err //Return "Earth" and the error if an issue with reading the response occurs. } - var countries []Country + var countries []Country // Slice to hold the parsed JSON data. if err := json.Unmarshal(body, &countries); err != nil { - return "", err + return "", err // Handle JSON parsing errors. } - capital := countries[0].Capital[0] + if len(countries) == 0 || len(countries[0].Capital) == 0 { + return "Earth", errors.New("no capital found for the specified ISO code") // Handle cases where no capital is available. + } + capital := countries[0].Capital[0] // Assume the first element is the desired capital. - return capital, nil + return capital, nil // Return the capital city. } +// CountryCoordinates structure for parsing geographical coordinates from a JSON response. type CountryCoordinates struct { - LatLng []float64 `json:"latlng"` + LatLng []float64 `json:"latlng"` // Array of latitude and longitude. } +// GetCoordinates fetches the geographical coordinates (latitude and longitude) of a country specified by its ISO code. func GetCoordinates(isocode string) (structs.CoordinatesDashboard, error) { - var empty = structs.CoordinatesDashboard{} + var empty = structs.CoordinatesDashboard{} // A default struct in case of errors. + // Construct the request URL. response, err := http.Get(External.CountriesAPI + alphaCodes + isocode + "&fields=latlng") if err != nil { + log.Print(err) return empty, err } - body, err := io.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) // Read the response body. if err != nil { + log.Print(err) return empty, err } - var countriesCoords []CountryCoordinates + var countriesCoords []CountryCoordinates // Slice to store parsed data. if err := json.Unmarshal(body, &countriesCoords); err != nil { - return empty, err + return empty, err // Handle JSON parsing errors. } + // Assume the first entry contains the correct coordinates. var coords = structs.CoordinatesDashboard{ Latitude: strconv.FormatFloat(countriesCoords[0].LatLng[0], 'f', 5, 64), - Longitude: strconv.FormatFloat(countriesCoords[0].LatLng[0], 'f', 5, 64), + Longitude: strconv.FormatFloat(countriesCoords[0].LatLng[1], 'f', 5, 64), } - return coords, nil + return coords, nil // Return the coordinates in a struct. } +// CountryPopulation structure for parsing population data from a JSON response. type CountryPopulation struct { - Population int `json:"population"` + Population int `json:"population"` // Population as an integer. } +// GetPopulation fetches the population of a country specified by its ISO code. func GetPopulation(isocode string) (int, error) { + // Construct the API request URL. response, err := http.Get(External.CountriesAPI + alphaCodes + isocode + "&fields=population") if err != nil { + log.Print(err) return 0, err } - body, err := io.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) // Read the entire response body. if err != nil { + log.Print(err) return 0, err } - var population []CountryPopulation - if err := json.Unmarshal(body, &population); err != nil { - return 0, err + var populationData []CountryPopulation // Slice to hold parsed data. + if err := json.Unmarshal(body, &populationData); err != nil { + return 0, err // Handle JSON parsing errors. } - populace := population[0].Population - return populace, nil + if len(populationData) == 0 { + return 0, errors.New("no population data found for the specified ISO code") // Handle cases where no data is found. + } + population := populationData[0].Population // Assume the first entry is the correct one. + + return population, nil // Return the population. } +// CountryArea structure for parsing area data from a JSON response. type CountryArea struct { - Area float64 `json:"area"` + Area float64 `json:"area"` // Area in square kilometers as a float64. } +// GetArea fetches the total land area of a country specified by its ISO code. func GetArea(isocode string) (float64, error) { + // Construct the API request URL. response, err := http.Get(External.CountriesAPI + alphaCodes + isocode + "&fields=area") if err != nil { + log.Print(err) return 0, err } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { - log.Print("Error closing countries api response body @ dashboardFunctions:GetArea: ", err) + log.Printf(ResponseBodyCloseError, err) } - }(response.Body) + }(response.Body) // Ensure the response body is closed. - body, err := io.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) // Read the response body. if err != nil { + log.Print(err) return 0, err } - var countryArea []CountryArea + var countryArea []CountryArea // Slice to hold parsed data. if err := json.Unmarshal(body, &countryArea); err != nil { - return 0, err + return 0, err // Handle JSON parsing errors. + } + + if len(countryArea) == 0 { + return 0, errors.New("no area data found for the specified ISO code") // Handle cases where no data is available. } - area := countryArea[0].Area + area := countryArea[0].Area // Assume the first entry contains the correct area. - return area, nil + return area, nil // Return the area. } +// CurrencyResponse defines the structure for parsing currency information from a JSON response. type CurrencyResponse []struct { Currencies map[string]struct { - Name string `json:"name"` - Symbol string `json:"symbol"` + Name string `json:"name"` // Name of the currency. + Symbol string `json:"symbol"` // Symbol of the currency. } `json:"currencies"` } +// RatesResponse defines the structure for parsing exchange rate information from a JSON response. type RatesResponse struct { - Rates map[string]float64 `json:"rates"` + Rates map[string]float64 `json:"rates"` // Map of currency codes to their respective exchange rates. } +// GetExchangeRate computes the exchange rates for specified currencies against a base currency +// specified by the ISO code. func GetExchangeRate(isocode string, currencies []string) (map[string]float64, error) { - exchangeRateList, err := getExchangeRateList(isocode) + exchangeRateList, err := getExchangeRateList(isocode) // Fetch the list of all exchange rates for the base currency. if err != nil { + log.Print(err) return nil, err } - exchangeRate := make(map[string]float64) + + exchangeRate := make(map[string]float64) // Map to hold the filtered exchange rates. for _, currency := range currencies { - exchangeRate[strings.ToUpper(currency)] = exchangeRateList[strings.ToUpper(currency)] + exchangeRate[strings.ToUpper(currency)] = exchangeRateList[strings.ToUpper(currency)] // Filter and add the relevant rates. } - return exchangeRate, nil + return exchangeRate, nil // Return the map of exchange rates. } +// fetchCurrencyRates retrieves the exchange rates for all currencies against a specified base currency. func fetchCurrencyRates(currency string) (map[string]float64, error) { + // Construct the API request URL. response, err := http.Get(External.CurrencyAPI + currency) if err != nil { + log.Print(err) return nil, err } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { - log.Print("Error closing currency api response body: ", err) + log.Printf(ResponseBodyCloseError, err) } - }(response.Body) + }(response.Body) // Ensure the response body is closed. - body, err := io.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) // Read the response body. if err != nil { + log.Print(err) return nil, err } - var ratesData RatesResponse + var ratesData RatesResponse // Struct to hold the parsed data. if err := json.Unmarshal(body, &ratesData); err != nil { - return nil, err + return nil, err // Handle JSON parsing errors. } - return ratesData.Rates, nil + return ratesData.Rates, nil // Return the map of exchange rates. } +// getExchangeRateList fetches the exchange rates for all currencies against the base currency +// specified by the ISO code. func getExchangeRateList(isocode string) (map[string]float64, error) { + // Construct the API request URL for fetching currency information. response, err := http.Get(External.CountriesAPI + alphaCodes + isocode + "&fields=currencies") if err != nil { + log.Print(err) return nil, err } - body, err := io.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) // Read the response body. if err != nil { + log.Print(err) return nil, err } - var currencyData CurrencyResponse + var currencyData CurrencyResponse // Struct to hold the parsed currency information. if err := json.Unmarshal(body, ¤cyData); err != nil { - return nil, err + return nil, err // Handle JSON parsing errors. } for currency := range currencyData[0].Currencies { - rates, err := fetchCurrencyRates(currency) + rates, err := fetchCurrencyRates(currency) // Fetch the exchange rates for the base currency. if err != nil { - return nil, fmt.Errorf("error fetching currency rates: %v", err) + log.Printf("Error fetching currency rates: %v", err) + return nil, fmt.Errorf("error fetching currency rates: %v", err) // Handle errors in fetching exchange rates. } - return rates, nil + return rates, nil // Return the map of exchange rates. } - return nil, errors.New("no currency data found") + return nil, errors.New("no currency data found") // Return an error if no currency information is found. } diff --git a/Go/internal/func/keyGen.go b/Go/internal/func/keyGen.go index ad06b25cdf5207758c482df5088b1f24b979a1e5..ff1a2688e88a280409bc2187bfe59631c4747f2b 100644 --- a/Go/internal/func/keyGen.go +++ b/Go/internal/func/keyGen.go @@ -1,3 +1,4 @@ +// Package _func provides developer-made utility functions for use within the application. package _func import ( @@ -5,33 +6,36 @@ import ( "time" ) -// initialize the package +// init initializes the package by seeding the random number generator with the current time. func init() { - // Seed rand with time, making it nondeterministic - rand.New(rand.NewSource(time.Now().UnixNano())) + rand.New(rand.NewSource(time.Now().UnixNano())) // Seed rand with time, making it nondeterministic. } -// Runes list of characters to use for ID +// Runes is the list of characters to use for generating IDs. var Runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") +// GenerateAPIKey generates an API key of length 'n'. func GenerateAPIKey(n int) string { + // Make two slices that are 'n' long, a and b. a := make([]rune, n) b := make([]rune, n) + // loop through the slices and insert a random character at each index for each slice. for i := range a { a[i] = Runes[rand.Intn(len(Runes))] b[i] = Runes[rand.Intn(len(Runes))] } + // concatenate the slices to strings, following standard format. return "sk-" + string(a) + "tTRjPv" + string(b) } -// GenerateUID returns a Unique Identifier that is 'n' long +// GenerateUID returns a Unique Identifier of length 'n'. func GenerateUID(n int) string { - // Make a slice that is 'n' long + // Make a slice that is 'n' long. b := make([]rune, n) - //loop through the slice and insert a random character at each index + // loop through the slice and insert a random character at each index. for i := range b { b[i] = Runes[rand.Intn(len(Runes))] } - // concatenate the slice to string + // concatenate the slice to string. return string(b) } diff --git a/Go/internal/func/supported_countries.go b/Go/internal/func/supported_countries.go index 2c72d70e0b06170f3f380e7fdb587e387e4adc0e..520e5a47ddbaa9e394981eb5abe67b3c5224f7ca 100644 --- a/Go/internal/func/supported_countries.go +++ b/Go/internal/func/supported_countries.go @@ -1,3 +1,4 @@ +// Package _func provides developer-made utility functions for use within the application. package _func import ( @@ -8,51 +9,65 @@ import ( "globeboard/internal/utils/structs" "golang.org/x/text/cases" "golang.org/x/text/language" + "io" + "log" "net/http" "strings" "time" ) -// getSupportedCountries fetches supported countries with their common names and cca2 codes. +// getSupportedCountries fetches supported countries with their common names and ISO 3166-1 alpha-2 codes. func getSupportedCountries() (map[string]string, error) { - url := fmt.Sprintf("%sall?fields=name,cca2", External.CountriesAPI) - var responseData []struct { + url := fmt.Sprintf("%sall?fields=name,cca2", External.CountriesAPI) // Constructing the API request URL. + var responseData []struct { // Struct to parse the JSON response. Name struct { - Common string `json:"common"` + Common string `json:"common"` // Common name of the country. } `json:"name"` - CCA2 string `json:"cca2"` + CCA2 string `json:"cca2"` // ISO 3166-1 alpha-2 code of the country. } - client := &http.Client{Timeout: 10 * time.Second} - req, err := http.NewRequest(http.MethodGet, url, nil) + client := &http.Client{Timeout: 10 * time.Second} // HTTP client with a timeout. + req, err := http.NewRequest(http.MethodGet, url, nil) // Creating a new HTTP GET request. if err != nil { + log.Printf("Error creating request: %v", err) return nil, fmt.Errorf("error creating request: %v", err) } - req.Header.Add("content-type", "application/json") + req.Header.Add("content-type", "application/json") // Setting content-type of the request. - res, err := client.Do(req) + res, err := client.Do(req) // Send the request. if err != nil { + log.Printf("Error issuing request: %v", err) return nil, fmt.Errorf("error issuing request: %v", err) } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Printf("Error closing response body: %v", err) + } + }(res.Body) // Ensure the response body is closed after processing. - err = json.NewDecoder(res.Body).Decode(&responseData) + err = json.NewDecoder(res.Body).Decode(&responseData) // Decoding the JSON response into the struct. if err != nil { + log.Printf("Error decoding JSON: %v", err) return nil, fmt.Errorf("error decoding JSON: %v", err) } - countriesMap := make(map[string]string) + countriesMap := make(map[string]string) // Map to hold country codes and their common names. + // Loop over the response data and map the supported countries by ISO code. for _, item := range responseData { countriesMap[item.CCA2] = item.Name.Common } - return countriesMap, nil + return countriesMap, nil // Returning the map. } +// ValidateCountryInfo validates the country information. func ValidateCountryInfo(ci *structs.CountryInfoInternal) error { - err := validateCountryNameIsoCode(ci) + err := validateCountryNameIsoCode(ci) // Validate the name and ISO code. if err != nil { return err } + // Ensure that at least one feature is populated. if !ci.Features.Temperature && !ci.Features.Precipitation && !ci.Features.Capital && !ci.Features.Coordinates && !ci.Features.Population && !ci.Features.Area && (ci.Features.TargetCurrencies == nil || len(ci.Features.TargetCurrencies) == 0) { @@ -61,27 +76,30 @@ func ValidateCountryInfo(ci *structs.CountryInfoInternal) error { return nil } +// validateCountryNameIsoCode validates the provided country name and/or ISO code against supported countries. func validateCountryNameIsoCode(ci *structs.CountryInfoInternal) error { - validCountries, err := getSupportedCountries() + validCountries, err := getSupportedCountries() // Fetch the list of supported countries. if err != nil { - return fmt.Errorf("error validating country: %v", err) + log.Printf("Error retriving supported countries: %v", err) + return fmt.Errorf("error retriving supported countries: %v", err) } - + // Validate that a country has been specified. if err := validateCountryOrIsoCodeProvided(ci); err != nil { return err } - - if err := validateIsoCode(ci, validCountries); err != nil { + // Validate the ISO, and update country name based on ISO. (If country name wasn't provided) + if err := validateIsoCodeUpdateEmptyCountry(ci, validCountries); err != nil { return err } - - if err := updateAndValidateIsoCodeForCountry(ci, validCountries); err != nil { + // Validate the country name, and update ISO based on country. (If ISO name wasn't provided) + if err := updateIsoCodeAndValidateCountry(ci, validCountries); err != nil { return err } - + // Validate that the provided country and ISO correspond. return validateCorrespondence(ci, validCountries) } +// validateCountryOrIsoCodeProvided checks that either country name or ISO code is provided. func validateCountryOrIsoCodeProvided(ci *structs.CountryInfoInternal) error { if ci.Country == "" && ci.IsoCode == "" { return errors.New("either country name or ISO code must be provided") @@ -89,24 +107,30 @@ func validateCountryOrIsoCodeProvided(ci *structs.CountryInfoInternal) error { return nil } -func validateIsoCode(ci *structs.CountryInfoInternal, validCountries map[string]string) error { - if ci.IsoCode != "" { - ci.IsoCode = strings.ToTitle(ci.IsoCode) - if country, exists := validCountries[ci.IsoCode]; !exists { +// validateIsoCodeUpdateEmptyCountry checks if the provided ISO code is valid and updates the country name accordingly, +// if empty. +func validateIsoCodeUpdateEmptyCountry(ci *structs.CountryInfoInternal, validCountries map[string]string) error { + if ci.IsoCode != "" { // Validate non-empty ISO. + ci.IsoCode = strings.ToTitle(ci.IsoCode) // Convert ISO code to title case to match keys in validCountries. + if country, exists := validCountries[ci.IsoCode]; !exists { // Validate ISO against validCountries. return errors.New("invalid ISO code") } else { - ci.Country = country + if ci.Country == "" { + ci.Country = country // Update the country name based on the ISO code, if empty. + } } } return nil } -func updateAndValidateIsoCodeForCountry(ci *structs.CountryInfoInternal, validCountries map[string]string) error { +// updateIsoCodeAndValidateCountry +// checks if only the country name was provided it validates it and updates the ISO code. +func updateIsoCodeAndValidateCountry(ci *structs.CountryInfoInternal, validCountries map[string]string) error { if ci.IsoCode == "" && ci.Country != "" { - ci.Country = cases.Title(language.English, cases.Compact).String(ci.Country) + ci.Country = cases.Title(language.English, cases.Compact).String(ci.Country) // Normalize country name to title case. for code, name := range validCountries { if name == ci.Country { - ci.IsoCode = code + ci.IsoCode = code // Update ISO code if the country name is valid. return nil } } @@ -115,9 +139,10 @@ func updateAndValidateIsoCodeForCountry(ci *structs.CountryInfoInternal, validCo return nil } +// validateCorrespondence checks that the provided country name and ISO code match. func validateCorrespondence(ci *structs.CountryInfoInternal, validCountries map[string]string) error { if ci.Country != "" && ci.IsoCode != "" { - ci.Country = cases.Title(language.English, cases.Compact).String(ci.Country) + ci.Country = cases.Title(language.English, cases.Compact).String(ci.Country) // Normalize the country name. if validCountries[ci.IsoCode] != ci.Country { return errors.New("ISO code and country name do not match") } diff --git a/Go/internal/func/webhook_payload.go b/Go/internal/func/webhook_payload.go index fb99317ba5e3c950785aa31d3f04610a9c9e8ff0..2472410243d94f1390a6fb2eca60612e2bafa2c3 100644 --- a/Go/internal/func/webhook_payload.go +++ b/Go/internal/func/webhook_payload.go @@ -1,3 +1,4 @@ +// Package _func provides developer-made utility functions for use within the application. package _func import ( @@ -10,7 +11,6 @@ import ( "globeboard/internal/utils/constants/Endpoints" "globeboard/internal/utils/constants/Webhooks" "globeboard/internal/utils/structs" - "io" "log" "net/http" "strings" @@ -18,27 +18,28 @@ import ( ) var ( - isocode = "" - title = "" - color = 0 - method = "" + isocode = "" // Variable to store ISO code. + title = "" // Variable to store the title for webhook messages. + color = 0 // Variable to store the color code for webhook messages. + method = "" // Variable to store the HTTP method by which the webhook triggered. ) +// LoopSendWebhooksRegistrations sends notifications to registered webhooks about registration events. func LoopSendWebhooksRegistrations(caller string, ci *structs.CountryInfoExternal, endpoint, eventAction string) { ctx := context.Background() - // Ignoring error as we've already confirmed the caller at the endpoint. + // Retrieve user information; ignore error as the user is pre-validated. user, _ := authenticate.Client.GetUser(ctx, caller) - email := user.DisplayName + " (" + strings.ToLower(user.Email) + ")" + // Select appropriate message components based on the event type. switch eventAction { case Webhooks.EventRegister: title = Webhooks.POSTTitle color = Webhooks.POSTColor method = http.MethodPost case Webhooks.EventChange: - title = Webhooks.PUTTitle + title = Webhooks.PATCHTitle color = Webhooks.PUTColor method = http.MethodPatch case Webhooks.EventDelete: @@ -51,106 +52,86 @@ func LoopSendWebhooksRegistrations(caller string, ci *structs.CountryInfoExterna method = http.MethodGet } + // Get the isocode from the payload data. isocode = ci.IsoCode + // Fetch all webhooks from the database. webhooks, err := db.GetAllWebhooks() if err != nil { - log.Printf("Error retriving webhooks from database: %v", err) + log.Printf("Error retrieving webhooks from database: %v", err) return } + // Iterate through each webhook and send notifications if conditions are met. for _, webhook := range webhooks { if isRegistrationWebhookValid(caller, ci, eventAction, webhook) { - if strings.Contains(webhook.URL, "discord") { - sendDiscordWebhookPayload( - email, - title, - color, - method, - endpoint, - ci, - webhook.URL) + if strings.Contains(webhook.URL, "https://discord.com") { + sendDiscordWebhookPayload(email, title, color, method, endpoint, ci, webhook.URL) } else { - sendWebhookPayload( - email, - title, - method, - endpoint, - isocode, - webhook.URL) + sendWebhookPayload(email, title, method, endpoint, isocode, webhook.URL) } } } } +// LoopSendWebhooksDashboard sends notifications to registered webhooks about dashboard events. func LoopSendWebhooksDashboard(caller string, dr *structs.DashboardResponse) { ctx := context.Background() - // Ignoring error as we've already confirmed the caller at the endpoint. + // Retrieve user information; ignore error as the user is pre-validated. user, _ := authenticate.Client.GetUser(ctx, caller) - email := user.DisplayName + " (" + strings.ToLower(user.Email) + ")" + + // Default to INVOKE title as Dashboard endpoint GET populated dashboards at this time. title = Webhooks.GETTitle color = Webhooks.GETColor method = Webhooks.EventInvoke + + // Get the isocode from the payload data. isocode = dr.IsoCode + // Fetch all webhooks from the database. webhooks, err := db.GetAllWebhooks() if err != nil { - log.Printf("Error retriving webhooks from database: %v", err) + log.Printf("Error retrieving webhooks from database: %v", err) return } + // Iterate through each webhook and send notifications if conditions are met. for _, webhook := range webhooks { if isDashboardWebhookValid(caller, dr, Webhooks.EventInvoke, webhook) { if strings.Contains(webhook.URL, "discord") { - sendDiscordWebhookPayload( - email, - title, - color, - method, - Endpoints.DashboardsID, - dr, - webhook.URL) + sendDiscordWebhookPayload(email, title, color, method, Endpoints.DashboardsID, dr, webhook.URL) } else { - sendWebhookPayload( - email, - title, - method, - Endpoints.DashboardsID, - isocode, - webhook.URL) + sendWebhookPayload(email, title, method, Endpoints.DashboardsID, isocode, webhook.URL) } } } } -func isRegistrationWebhookValid(caller string, ci *structs.CountryInfoExternal, eventAction string, webhook structs.WebhookGet) bool { - if webhook.UUID == "" || webhook.UUID == caller { - if webhook.Country == "" || webhook.Country == ci.IsoCode { - if stringListContains(webhook.Event, eventAction) { - return true - } - return false +// isRegistrationWebhookValid checks if the webhook should trigger for the registration event. +func isRegistrationWebhookValid(caller string, ci *structs.CountryInfoExternal, eventAction string, webhook structs.WebhookInternal) bool { + if webhook.UUID == "" || webhook.UUID == caller { // Validate that the webhook is associated with the user. + // (Empty UUID is for developer webhooks) + if webhook.Country == "" || webhook.Country == ci.IsoCode { // Validate webhook for country. + // The event is about. + return stringListContains(webhook.Event, eventAction) // Validate and return if webhook contains trigger event. } - return false } return false } -func isDashboardWebhookValid(caller string, dr *structs.DashboardResponse, eventAction string, webhook structs.WebhookGet) bool { +// isDashboardWebhookValid checks if the webhook should trigger for the dashboard event. +func isDashboardWebhookValid(caller string, dr *structs.DashboardResponse, eventAction string, webhook structs.WebhookInternal) bool { if webhook.UUID == "" || webhook.UUID == caller { if webhook.Country == "" || webhook.Country == dr.IsoCode { - if stringListContains(webhook.Event, eventAction) { - return true - } - return false + return stringListContains(webhook.Event, eventAction) } - return false } return false } +// stringListContains checks if a string is present in a slice of strings. func stringListContains(s []string, str string) bool { for _, v := range s { if v == str { @@ -160,40 +141,23 @@ func stringListContains(s []string, str string) bool { return false } +// sendDiscordWebhookPayload sends a structured message as a Discord webhook. func sendDiscordWebhookPayload(email, title string, color int, event, endpoint string, requestBody interface{}, payloadUrl string) { - // Serialize the requestBody to a JSON string with pretty printing - requestBodyJSON, err := json.MarshalIndent(requestBody, "", " ") + requestBodyJSON, err := json.MarshalIndent(requestBody, "", " ") // Pretty-print JSON for readability. if err != nil { log.Println("Error marshaling request body:", err) return } - // Remove the UUID field from the payload. requestBodyString := string(requestBodyJSON) - requestBodyString = strings.Replace(requestBodyString, " \"uuid\": \"\",\n", "", -1) - requestBodyString = fmt.Sprintf("```json\n%s\n```", requestBodyString) + requestBodyString = fmt.Sprintf("```json\n%s\n```", requestBodyString) // Format JSON for Discord code block. + // Create the payload for the Discord webhook. fields := []structs.Field{ - { - Name: "Event", - Value: event, - Inline: true, - }, - { - Name: "Endpoint", - Value: endpoint, - Inline: true, - }, - { - Name: "Country", - Value: isocode, - Inline: true, - }, - { - Name: "Payload", - Value: requestBodyString, - Inline: false, - }, + {Name: "Event", Value: event, Inline: true}, + {Name: "Endpoint", Value: endpoint, Inline: true}, + {Name: "Country", Value: isocode, Inline: true}, + {Name: "Payload", Value: requestBodyString, Inline: false}, } payload := structs.WebhookPayload{ @@ -201,84 +165,69 @@ func sendDiscordWebhookPayload(email, title string, color int, event, endpoint s AvatarURL: "https://i.imgur.com/vjsvcxU.png", Embeds: []structs.Embed{ { - Title: title, - Author: structs.Author{ - Name: "User: " + email, - }, + Title: title, + Author: structs.Author{Name: "User: " + email}, Description: "-------------------------------------------------------------------------------------", - Timestamp: time.Now().Format(time.RFC3339), // Formatting the current time to RFC 3339 + Timestamp: time.Now().Format(time.RFC3339), Color: color, Fields: fields, - Footer: structs.Footer{ - Text: "Webhook Triggered:", - }, + Footer: structs.Footer{Text: "Webhook Triggered:"}, }, }, } - payloadBytes, err := json.Marshal(payload) + payloadBytes, err := json.Marshal(payload) // Serialize the payload into JSON. if err != nil { log.Println("Error marshaling payload:", err) return } - req, err := http.NewRequest("POST", payloadUrl, bytes.NewBuffer(payloadBytes)) + req, err := http.NewRequest("POST", payloadUrl, bytes.NewBuffer(payloadBytes)) // Create a POST request with the payload. if err != nil { log.Println("Error creating request:", err) return } - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json") // Set content type to JSON. client := &http.Client{} - resp, err := client.Do(req) + _, err = client.Do(req) // Send the request. if err != nil { log.Println("Error sending request:", err) return } - - _, err = io.ReadAll(resp.Body) - if err != nil { - log.Println("Error reading response body:", err) - return - } } +// sendWebhookPayload sends a JSON formatted message to a generic webhook. func sendWebhookPayload(email, title string, event, endpoint, country string, payloadUrl string) { - + // Create the generic webhook payload. payload := map[string]interface{}{ "User": email, "title": title, "event": event, "endpoint": endpoint, "country": country, - "timestamp": time.Now().UTC().Format("2006-01-02T15:04:05.999Z"), // Formatting the current time to ISO8601 + "timestamp": time.Now().UTC().Format("2006-01-02T15:04:05.999Z"), // Format current time in ISO8601 format. } - payloadBytes, err := json.Marshal(payload) + payloadBytes, err := json.Marshal(payload) // Serialize the payload into JSON. if err != nil { log.Println("Error marshaling payload:", err) return } - req, err := http.NewRequest(http.MethodPost, payloadUrl, bytes.NewBuffer(payloadBytes)) + req, err := http.NewRequest(http.MethodPost, payloadUrl, bytes.NewBuffer(payloadBytes)) // Create a POST request with the payload. if err != nil { log.Println("Error creating request:", err) return } - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json") // Set content type to JSON. client := &http.Client{} - resp, err := client.Do(req) + _, err = client.Do(req) // Send the request. if err != nil { log.Println("Error sending request:", err) return } - - _, err = io.ReadAll(resp.Body) - if err != nil { - log.Println("Error reading response body:", err) - return - } } diff --git a/Go/internal/handlers/empty_handler.go b/Go/internal/handlers/empty_handler.go index 240848f2e893cdad43153c6acdeb6f89150e23ea..a6d47804617f739efe782ad902f0e0d328ae6b4e 100644 --- a/Go/internal/handlers/empty_handler.go +++ b/Go/internal/handlers/empty_handler.go @@ -1,4 +1,4 @@ -// Package handlers provide HTTP request handlers for routing and handling requests within the Gutendex API. +// Package handlers provide HTTP request handlers for routing and handling requests within the application. package handlers import ( @@ -9,48 +9,42 @@ import ( ) const ( - ISE = "Internal Server Error" + ISE = "Internal Server Error" // ISE defines the error message returned when an internal server error occurs. ) -// EmptyHandler -// -// @Description: -// @param w -// @param r +// EmptyHandler serves the "root.html" file at the root URL ("/") and returns a 404 Not Found error for other paths. func EmptyHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { - http.NotFound(w, r) + http.NotFound(w, r) // Return a 404 Not Found error if the path is not the root. return } - filePath := "./web/root.html" + filePath := "./web/root.html" // Path to the "root.html" file. - // Set the "Content-Type" header. - w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Content-Type", "text/html; charset=utf-8") // Set the "Content-Type" header. + w.WriteHeader(http.StatusSeeOther) // Set the status code to 303 See Other, + // indicating redirection. - // Set the status code to indicate the redirection. - w.WriteHeader(http.StatusSeeOther) - - // Open the file. - file, err := os.Open(filePath) + file, err := os.Open(filePath) // Open the "root.html" file. if err != nil { - log.Print("Error opening root file: ", err) - http.Error(w, ISE, http.StatusInternalServerError) + log.Print("Error opening root file: ", err) // Log error if file opening fails. + http.Error(w, ISE, http.StatusInternalServerError) // Return a 500 Internal Server Error if the file cannot be opened. return } - defer func(file *os.File) { + defer func(file *os.File) { // Ensure the file is closed after serving it. err := file.Close() if err != nil { - log.Print("Error closing root file: ", err) - http.Error(w, ISE, http.StatusInternalServerError) - return + log.Print("Error closing root file: ", err) // Log error if file closing fails. + http.Error(w, ISE, http.StatusInternalServerError) // Return a 500 Internal Server Error + // if the file cannot be closed. } }(file) - _, err = io.Copy(w, file) + _, err = io.Copy(w, file) // Copy the file content to the response writer. if err != nil { - log.Print("Error copying root file to ResponseWriter: ", err) - http.Error(w, ISE, http.StatusInternalServerError) + log.Print("Error copying root file to ResponseWriter: ", err) // Log error if copying fails. + http.Error(w, ISE, http.StatusInternalServerError) // Return a 500 Internal Server Error + // if content cannot be copied. return } } diff --git a/Go/internal/handlers/endpoint/dashboard/dashboards_id_handler.go b/Go/internal/handlers/endpoint/dashboard/dashboards_id_handler.go index 261c422897f355e8243a24c1a91a8dc111dce710..db30e52dc0e2871e8e1bd4ca9d161116caf1dbbb 100644 --- a/Go/internal/handlers/endpoint/dashboard/dashboards_id_handler.go +++ b/Go/internal/handlers/endpoint/dashboard/dashboards_id_handler.go @@ -1,4 +1,4 @@ -// Package dashboard provides handlers for dashboard-related endpoints. +// Package dashboard provides handlers for managing dashboard-related functionalities through HTTP endpoints. package dashboard import ( @@ -6,6 +6,8 @@ import ( "fmt" "globeboard/db" _func "globeboard/internal/func" + "globeboard/internal/utils/constants" + "globeboard/internal/utils/constants/Endpoints" "globeboard/internal/utils/structs" "log" "net/http" @@ -14,163 +16,181 @@ import ( ) const ( - RetrivalError = "Error getting country information" + APIInfoRetrivalError = "Error getting country information" // Error message for when country information cannot be retrieved. + APICoordsRetrivalError = "Error getting Coordinates Information: " // Error message for when coordinates information cannot be retrieved. ) +// DashboardsIdHandler handles requests to the dashboard endpoint. func DashboardsIdHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { - case http.MethodGet: + case http.MethodGet: // Handle GET request. handleDashboardGetRequest(w, r) default: + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.DashboardsID, r.Method) http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is:\n"+http.MethodGet, http.StatusNotImplemented) return } } +// handleDashboardGetRequest processes GET requests to retrieve dashboards by ID. func handleDashboardGetRequest(w http.ResponseWriter, r *http.Request) { - ID := r.PathValue("ID") - query := r.URL.Query() - token := query.Get("token") - if token == "" { + ID := r.PathValue("ID") // Retrieve ID from URL path. + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Retrieve token from URL query parameters. + if token == "" { // Check if a token is provided. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.DashboardsID) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve UUID associated with API token. + if UUID == "" { // Check if UUID is retrieved. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.DashboardsID) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - if ID == "" || ID == " " { + if ID == "" || ID == " " { // Check if the ID is valid. + log.Printf(constants.ClientConnectNoID, r.Method, Endpoints.DashboardsID) http.Error(w, ProvideID, http.StatusBadRequest) return } - reg, err := db.GetSpecificRegistration(ID, UUID) + reg, err := db.GetSpecificRegistration(ID, UUID) // Retrieve registration by ID for user (UUID). if err != nil { - err := fmt.Sprint("Document doesn't exist: ", err) + log.Printf("Error getting registration: %v", err) + err := fmt.Sprintf("Dashboard doesn't exist: %v", err) http.Error(w, err, http.StatusNotFound) return } - dr := new(structs.DashboardResponse) + dr := new(structs.DashboardResponse) // Initialize new DashboardResponse struct. + // Set retrieved values to response. dr.ID = reg.ID dr.Country = reg.Country dr.IsoCode = reg.IsoCode - // Countries API + // Country information API integration. if getCountryInfo(w, reg, dr) { return } - // Currency API + // Currency information API integration. if getCurrencyInfo(w, reg, dr) { return } - // Open-Meteo API + // Weather information API integration. if getWeatherInfo(w, reg, dr) { return } - // Set the LastRetrieval time and format it to ISO8601 format to mirror Firestore Timestamp + // Set the LastRetrieval time and format it to ISO8601 format to mirror Firestore Server Timestamp. dr.LastRetrieval = time.Now().UTC().Format("2006-01-02T15:04:05.999Z") w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - err = json.NewEncoder(w).Encode(dr) + err = json.NewEncoder(w).Encode(dr) // Encode the dashboard response into JSON and write to the response writer. if err != nil { + log.Print(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - _func.LoopSendWebhooksDashboard(UUID, dr) + _func.LoopSendWebhooksDashboard(UUID, dr) // Send notifications to webhooks. } +// getWeatherInfo fetches weather information for a specific registration and updates the dashboard response. func getWeatherInfo(w http.ResponseWriter, reg *structs.CountryInfoInternal, dr *structs.DashboardResponse) bool { - if reg.Features.Temperature { - coords, err := _func.GetCoordinates(reg.IsoCode) + if reg.Features.Temperature { // Check if the temperature feature is enabled. + coords, err := _func.GetCoordinates(reg.IsoCode) // Get coordinates for the ISO code. if err != nil { - log.Print("Error getting Coordinates Information: ", err) - http.Error(w, RetrivalError, http.StatusInternalServerError) + log.Print(APICoordsRetrivalError, err) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) return true } - temp, err := _func.GetTemp(coords) + temp, err := _func.GetTemp(coords) // Get temperature for the coordinates. if err != nil { log.Print("Error getting Temperature Information: ", err) - http.Error(w, RetrivalError, http.StatusInternalServerError) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) return true } - dr.Features.Temperature = strconv.FormatFloat(temp, 'f', 1, 64) + dr.Features.Temperature = strconv.FormatFloat(temp, 'f', 1, 64) // Format temperature and set to dashboard response. } - if reg.Features.Precipitation { - precipitation, err := _func.GetPrecipitation(dr.Features.Coordinates) + if reg.Features.Precipitation { // Check if the precipitation feature is enabled. + coords, err := _func.GetCoordinates(reg.IsoCode) // Get coordinates for the ISO code. + if err != nil { + log.Print(APICoordsRetrivalError, err) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) + return true + } + precipitation, err := _func.GetPrecipitation(coords) // Get precipitation for the coordinates. if err != nil { log.Print("Error getting Temperature Information: ", err) - http.Error(w, RetrivalError, http.StatusInternalServerError) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) return true } - dr.Features.Precipitation = strconv.FormatFloat(precipitation, 'f', 2, 64) + dr.Features.Precipitation = strconv.FormatFloat(precipitation, 'f', 2, 64) // Format precipitation and set to dashboard response. } return false } +// getCurrencyInfo fetches currency exchange information for a specific registration and updates the dashboard response. func getCurrencyInfo(w http.ResponseWriter, reg *structs.CountryInfoInternal, dr *structs.DashboardResponse) bool { - if reg.Features.TargetCurrencies != nil && len(reg.Features.TargetCurrencies) != 0 { - exchangeRate, err := _func.GetExchangeRate(reg.IsoCode, reg.Features.TargetCurrencies) + if reg.Features.TargetCurrencies != nil && len(reg.Features.TargetCurrencies) > 0 { // Check if target-currencies feature is non nil and non-empty. + exchangeRate, err := _func.GetExchangeRate(reg.IsoCode, reg.Features.TargetCurrencies) // Get exchange rates for the target currencies. if err != nil { log.Print("Error getting Exchange Rate Information: ", err) - http.Error(w, RetrivalError, http.StatusInternalServerError) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) return true } - dr.Features.TargetCurrencies = exchangeRate + dr.Features.TargetCurrencies = exchangeRate // Set exchange rates to dashboard response. } return false } +// getCountryInfo fetches country-specific information for a specific registration and updates the dashboard response. func getCountryInfo(w http.ResponseWriter, reg *structs.CountryInfoInternal, dr *structs.DashboardResponse) bool { - if reg.Features.Capital { - capital, err := _func.GetCapital(reg.IsoCode) + if reg.Features.Capital { // Check if the capital feature is enabled. + capital, err := _func.GetCapital(reg.IsoCode) // Get capital for the ISO code. if err != nil { log.Print("Error getting Capital Information: ", err) - http.Error(w, RetrivalError, http.StatusInternalServerError) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) return true } - dr.Features.Capital = capital + dr.Features.Capital = capital // Set capital to dashboard response. } - if reg.Features.Coordinates { - coords, err := _func.GetCoordinates(reg.IsoCode) + if reg.Features.Coordinates { // Check if the coordinate feature is enabled. + coords, err := _func.GetCoordinates(reg.IsoCode) // Get coordinates for the ISO code. if err != nil { - log.Print("Error getting Coordinates Information: ", err) - http.Error(w, RetrivalError, http.StatusInternalServerError) + log.Print(APICoordsRetrivalError, err) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) return true } - dr.Features.Coordinates = &coords + dr.Features.Coordinates = &coords // Set coordinates to dashboard response. } - if reg.Features.Population { - pop, err := _func.GetPopulation(reg.IsoCode) + if reg.Features.Population { // Check if the population feature is enabled. + pop, err := _func.GetPopulation(reg.IsoCode) // Get population for the ISO code. if err != nil { log.Print("Error getting Population Information: ", err) - http.Error(w, RetrivalError, http.StatusInternalServerError) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) return true } - dr.Features.Population = pop + dr.Features.Population = pop // Set population to dashboard response. } - if reg.Features.Area { - area, err := _func.GetArea(reg.IsoCode) + if reg.Features.Area { // Check if area feature is enabled. + area, err := _func.GetArea(reg.IsoCode) // Get area for the ISO code. if err != nil { log.Print("Error getting Area Information: ", err) - http.Error(w, RetrivalError, http.StatusInternalServerError) + http.Error(w, APIInfoRetrivalError, http.StatusInternalServerError) return true } - dr.Features.Area = strconv.FormatFloat(area, 'f', 1, 64) + dr.Features.Area = strconv.FormatFloat(area, 'f', 1, 64) // Format area and set to dashboard response. } return false } diff --git a/Go/internal/handlers/endpoint/dashboard/notifications_handler.go b/Go/internal/handlers/endpoint/dashboard/notifications_handler.go index 10dcfcbba15af096b83693fe237b3911403d4205..28687ed38b743cdc99869df9918526c5ed752e08 100644 --- a/Go/internal/handlers/endpoint/dashboard/notifications_handler.go +++ b/Go/internal/handlers/endpoint/dashboard/notifications_handler.go @@ -1,4 +1,4 @@ -// Package dashboard provides handlers for dashboard-related endpoints. +// Package dashboard provides handlers for managing dashboard-related functionalities through HTTP endpoints. package dashboard import ( @@ -7,64 +7,72 @@ import ( "globeboard/db" _func "globeboard/internal/func" "globeboard/internal/utils/constants" + "globeboard/internal/utils/constants/Endpoints" "globeboard/internal/utils/structs" "log" "net/http" ) +// NotificationsHandler handles HTTP requests related to notification webhooks. func NotificationsHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { - case http.MethodPost: + case http.MethodPost: // Handle POST request handleNotifPostRequest(w, r) - case http.MethodGet: + case http.MethodGet: // Handle GET request handleNotifGetAllRequest(w, r) default: - http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is:\n"+http.MethodPost+"\n"+http.MethodGet+"\n"+http.MethodPatch, http.StatusNotImplemented) + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.Notifications, r.Method) + http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is:\n"+http.MethodPost+"\n"+http.MethodGet, http.StatusNotImplemented) return } } +// handleNotifPostRequest processes POST requests to create a new notification webhook. func handleNotifPostRequest(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - token := query.Get("token") - if token == "" { + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Retrieve token from the URL query parameters. + if token == "" { // Check if a token is provided. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.Notifications) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve UUID associated with the API token. + if UUID == "" { // Check if UUID is valid. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.Notifications) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - if r.Body == nil { + if r.Body == nil { // Check if request body is empty. + log.Printf(constants.ClientConnectEmptyBody, r.Method, Endpoints.Notifications) err := fmt.Sprintf("Please send a request body") http.Error(w, err, http.StatusBadRequest) return } - var webhook *structs.WebhookGet - if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil { + var webhook *structs.WebhookInternal + if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil { // Decode the JSON request body into webhook struct. err := fmt.Sprintf("Error decoding request body: %v", err) http.Error(w, err, http.StatusBadRequest) return } - UDID := _func.GenerateUID(constants.DocIdLength) - ID := _func.GenerateUID(constants.IdLength) + UDID := _func.GenerateUID(constants.DocIdLength) // Generate a unique document ID. + ID := _func.GenerateUID(constants.IdLength) // Generate a unique ID for the webhook. webhook.ID = ID webhook.UUID = UUID - err := db.AddWebhook(UDID, webhook) + err := db.AddWebhook(UDID, webhook) // Add the webhook to the database. if err != nil { log.Println("Error saving data to database" + err.Error()) http.Error(w, "Error storing data in database", http.StatusInternalServerError) return } - hook, err := db.GetSpecificWebhook(ID, UUID) + hook, err := db.GetSpecificWebhook(ID, UUID) // Retrieve the newly added webhook to confirm its addition. if err != nil { log.Print("Error getting document from database: ", err) http.Error(w, "Error confirming data added to database", http.StatusInternalServerError) @@ -72,46 +80,52 @@ func handleNotifPostRequest(w http.ResponseWriter, r *http.Request) { } response := map[string]interface{}{ - "id": hook.ID, + "id": hook.ID, // Prepare response data with the new webhook ID. } - w.Header().Set(ContentType, ApplicationJSON) + w.Header().Set(ContentType, ApplicationJSON) // Set the content type of the response. - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusCreated) // Set HTTP status to "Created". - err = json.NewEncoder(w).Encode(response) + err = json.NewEncoder(w).Encode(response) // Encode the response as JSON and send it. if err != nil { + log.Print(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } } +// handleNotifGetAllRequest processes GET requests to retrieve all notification webhooks for a user. func handleNotifGetAllRequest(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - token := query.Get("token") - if token == "" { + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Retrieve token from the URL query parameters. + if token == "" { // Check if a token is provided. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.Notifications) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve UUID associated with the API token. + if UUID == "" { // Check if UUID is valid. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.Notifications) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - regs, err := db.GetWebhooksUser(UUID) + regs, err := db.GetWebhooksUser(UUID) // Retrieve all webhooks associated with the user (UUID). if err != nil { + log.Printf("Error retrieving webhooks from database: %v", err) errmsg := fmt.Sprint("Error retrieving webhooks from database: ", err) http.Error(w, errmsg, http.StatusInternalServerError) return } - w.Header().Set(ContentType, ApplicationJSON) + w.Header().Set(ContentType, ApplicationJSON) // Set the content type of the response. - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusOK) // Set HTTP status to "OK". - err = json.NewEncoder(w).Encode(regs) + err = json.NewEncoder(w).Encode(regs) // Encode the webhooks as JSON and send it. if err != nil { + log.Print(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/Go/internal/handlers/endpoint/dashboard/notifications_id_handler.go b/Go/internal/handlers/endpoint/dashboard/notifications_id_handler.go index 5c2983b2b4ea517348080e29560c97f32b1403ba..22ad4d78747e9d86b53ff344468c90f9da96b4ee 100644 --- a/Go/internal/handlers/endpoint/dashboard/notifications_id_handler.go +++ b/Go/internal/handlers/endpoint/dashboard/notifications_id_handler.go @@ -1,87 +1,104 @@ -// Package dashboard provides handlers for dashboard-related endpoints. +// Package dashboard provides handlers for managing dashboard-related functionalities through HTTP endpoints. package dashboard import ( "encoding/json" "fmt" "globeboard/db" + "globeboard/internal/utils/constants" + "globeboard/internal/utils/constants/Endpoints" + "log" "net/http" ) +// NotificationsIdHandler handles HTTP requests related to specific notification settings by ID. func NotificationsIdHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { - case http.MethodGet: + case http.MethodGet: // Handle GET request handleNotifGetRequest(w, r) - case http.MethodDelete: + case http.MethodDelete: // Handle DELETE request handleNotifDeleteRequest(w, r) default: - http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is:\n"+http.MethodPost+"\n"+http.MethodGet+"\n"+http.MethodPatch, http.StatusNotImplemented) + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.NotificationsID, r.Method) + http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint are:\n"+http.MethodGet+"\n"+http.MethodDelete, http.StatusNotImplemented) return } } +// handleNotifGetRequest processes GET requests to retrieve a specific notification webhook by its ID. func handleNotifGetRequest(w http.ResponseWriter, r *http.Request) { - ID := r.PathValue("ID") - query := r.URL.Query() - token := query.Get("token") - if token == "" { + ID := r.PathValue("ID") // Retrieve the ID from the URL path. + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Retrieve token from the URL query parameters. + if token == "" { // Check if a token is provided. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.NotificationsID) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve UUID associated with the API token. + if UUID == "" { // Check if UUID is valid. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.NotificationsID) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - if ID == "" || ID == " " { + if ID == "" || ID == " " { // Check if the ID is valid. + log.Printf(constants.ClientConnectNoID, r.Method, Endpoints.NotificationsID) http.Error(w, ProvideID, http.StatusBadRequest) return } - hook, err := db.GetSpecificWebhook(ID, UUID) + hook, err := db.GetSpecificWebhook(ID, UUID) // Retrieve the specific webhook by ID and UUID. if err != nil { - err := fmt.Sprintf("Error getting document from database: %v", err) + log.Printf("Error getting webhook from database: %v", err) + err := fmt.Sprintf("Error getting webhook from database: %v", err) http.Error(w, err, http.StatusNotFound) return } - w.Header().Set(ContentType, ApplicationJSON) + w.Header().Set(ContentType, ApplicationJSON) // Set the content type of the response. - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusOK) // Set HTTP status to "OK". - err = json.NewEncoder(w).Encode(hook) + err = json.NewEncoder(w).Encode(hook) // Encode the webhook as JSON and send it. if err != nil { + log.Print(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } } +// handleNotifDeleteRequest processes DELETE requests to remove a specific notification webhook by its ID. func handleNotifDeleteRequest(w http.ResponseWriter, r *http.Request) { - ID := r.PathValue("ID") - query := r.URL.Query() - token := query.Get("token") - if token == "" { + ID := r.PathValue("ID") // Retrieve the ID from the URL path. + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Retrieve token from the URL query parameters. + if token == "" { // Check if a token is provided. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.NotificationsID) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve UUID associated with the API token. + if UUID == "" { // Check if UUID is valid. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.NotificationsID) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - if ID == "" || ID == " " { + if ID == "" || ID == " " { // Check if the ID is valid. + log.Printf(constants.ClientConnectNoID, r.Method, Endpoints.NotificationsID) http.Error(w, ProvideID, http.StatusBadRequest) return } - err := db.DeleteWebhook(ID, UUID) + err := db.DeleteWebhook(ID, UUID) // Delete the specific webhook by ID and UUID from the database. if err != nil { + log.Printf("Error deleting data from database: %v", err) err := fmt.Sprintf("Error deleting data from database: %v", err) http.Error(w, err, http.StatusInternalServerError) return } - w.WriteHeader(http.StatusNoContent) + w.WriteHeader(http.StatusNoContent) // Set HTTP status to "No Content" upon successful deletion. } diff --git a/Go/internal/handlers/endpoint/dashboard/registrations_handler.go b/Go/internal/handlers/endpoint/dashboard/registrations_handler.go index 67ee8d1c1ca30039d6796037a7e4919f2dcce2e5..99b60454f9e8afc89d3c59427e2c94217df461e2 100644 --- a/Go/internal/handlers/endpoint/dashboard/registrations_handler.go +++ b/Go/internal/handlers/endpoint/dashboard/registrations_handler.go @@ -1,4 +1,4 @@ -// Package dashboard provides handlers for dashboard-related endpoints. +// Package dashboard provides handlers for managing dashboard-related functionalities through HTTP endpoints. package dashboard import ( @@ -15,152 +15,168 @@ import ( "net/http" ) +// Constant strings used for API responses and header configurations const ( - ProvideAPI = "Please provide API Token" - APINotAccepted = "API key not accepted" - ContentType = "Content-Type" - ApplicationJSON = "application/json" + ProvideAPI = "Please provide API Token" // Message prompting for API token + APINotAccepted = "API key not accepted" // Message for an unauthorized API token + ContentType = "Content-Type" // HTTP header field for content-type + ApplicationJSON = "application/json" // MIME type for JSON ) +// RegistrationsHandler routes the HTTP request based on the method (POST, GET) to appropriate handlers func RegistrationsHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: - handleRegPostRequest(w, r) + handleRegPostRequest(w, r) // Handle POST requests case http.MethodGet: - handleRegGetAllRequest(w, r) + handleRegGetAllRequest(w, r) // Handle GET requests default: + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.Registrations, r.Method) http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is:\n"+http.MethodPost+"\n"+http.MethodGet+"\n"+http.MethodPatch, http.StatusNotImplemented) return } } +// DecodeCountryInfo decodes JSON data from the request body into a CountryInfoInternal struct func DecodeCountryInfo(data io.ReadCloser) (*structs.CountryInfoInternal, error) { var ci *structs.CountryInfoInternal if err := json.NewDecoder(data).Decode(&ci); err != nil { - return nil, err + return nil, err // Return error if decoding fails } - err := _func.ValidateCountryInfo(ci) + err := _func.ValidateCountryInfo(ci) // Validate the decoded information if err != nil { - return nil, err + return nil, err // Return validation errors } - return ci, nil + return ci, nil // Return the decoded and validated country information } +// handleRegPostRequest handles the POST requests for registration endpoint func handleRegPostRequest(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - token := query.Get("token") - if token == "" { + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Extract the 'token' parameter from the query. + if token == "" { // Validate token presence. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.Registrations) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve the UUID for the API key. + if UUID == "" { // Validate UUID presence. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.Registrations) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - if r.Body == nil { + if r.Body == nil { // Validate that the request body is not empty. + log.Printf(constants.ClientConnectEmptyBody, r.Method, Endpoints.Registrations) err := fmt.Sprintf("Please send a request body") http.Error(w, err, http.StatusBadRequest) return } - ci, err := DecodeCountryInfo(r.Body) + ci, err := DecodeCountryInfo(r.Body) // Decode request body into CountryInfoInternal struct. if err != nil { + log.Printf("Error decoding request body: %v", err) err := fmt.Sprintf("Error decoding request body: %v", err) http.Error(w, err, http.StatusBadRequest) return } - UDID := _func.GenerateUID(constants.DocIdLength) - URID := _func.GenerateUID(constants.IdLength) + UDID := _func.GenerateUID(constants.DocIdLength) // Generate a unique ID for document + URID := _func.GenerateUID(constants.IdLength) // Generate a unique ID for registration ci.ID = URID ci.UUID = UUID - err = db.AddRegistration(UDID, ci) + err = db.AddRegistration(UDID, ci) // Add Registration to the Database. if err != nil { log.Println("Error saving data to database" + err.Error()) http.Error(w, "Error storing data in database", http.StatusInternalServerError) return } - reg, err := db.GetSpecificRegistration(URID, UUID) + reg, err := db.GetSpecificRegistration(URID, UUID) // Retrieve specific registration details. if err != nil { log.Print("Error getting document from database: ", err) http.Error(w, "Error confirming data added to database", http.StatusInternalServerError) return } - response := map[string]interface{}{ + response := map[string]interface{}{ // construct JSON response. "id": reg.ID, "lastChange": reg.Lastchange, } - w.Header().Set(ContentType, ApplicationJSON) + w.Header().Set(ContentType, ApplicationJSON) // Set the content type of response - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusCreated) // Set HTTP status code to 201 Created - err = json.NewEncoder(w).Encode(response) + err = json.NewEncoder(w).Encode(response) // Encode and send the response if err != nil { + log.Print(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - cie := new(structs.CountryInfoExternal) + cie := new(structs.CountryInfoExternal) // Create new external country info struct. cie.ID = reg.ID cie.Country = reg.Country cie.IsoCode = reg.IsoCode cie.Features = reg.Features - _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.Registrations, Webhooks.EventRegister) + _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.Registrations, Webhooks.EventRegister) // Send webhook notifications } +// handleRegGetAllRequest handles the GET requests for registration endpoint to retrieve all registrations func handleRegGetAllRequest(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - token := query.Get("token") - if token == "" { + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Extract the 'token' parameter from the query. + if token == "" { // Validate token presence. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.Registrations) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Extract the UUID parameter from the API token. + if UUID == "" { // Validate UUID presence. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.Registrations) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - regs, err := db.GetRegistrations(UUID) + regs, err := db.GetRegistrations(UUID) // Retrieve the user's Registrations. if err != nil { - errmsg := fmt.Sprint("Error retrieving document from database: ", err) + log.Printf("Error retrieving documents from database: %s", err) + errmsg := fmt.Sprint("Error retrieving documents from database: ", err) http.Error(w, errmsg, http.StatusInternalServerError) return } - w.Header().Set(ContentType, ApplicationJSON) + w.Header().Set(ContentType, ApplicationJSON) // Set the content type of response - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusOK) // Set HTTP status code to 200 OK - var cies []*structs.CountryInfoExternal - for _, reg := range regs { + var cies []*structs.CountryInfoExternal // Construct CountryInfoExternal slice. + for _, reg := range regs { // Loop over the retrieved registrations and parse them to CountryInfoExternal struct. cie := new(structs.CountryInfoExternal) cie.ID = reg.ID cie.Country = reg.Country cie.IsoCode = reg.IsoCode cie.Features = reg.Features cie.Lastchange = reg.Lastchange - cies = append(cies, cie) + cies = append(cies, cie) // Append the individual CountryInfoExternal structs to the slice. } - err = json.NewEncoder(w).Encode(cies) + err = json.NewEncoder(w).Encode(cies) // Encode and send the response if err != nil { + log.Print(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } for _, cie := range cies { - _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.Registrations, Webhooks.EventInvoke) + _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.Registrations, Webhooks.EventInvoke) // Send webhook notifications on data retrieval } } diff --git a/Go/internal/handlers/endpoint/dashboard/registrations_id_handler.go b/Go/internal/handlers/endpoint/dashboard/registrations_id_handler.go index 1842594a6bf86eb1a1968a25497befdb991e4c29..98ebc1b5c8e6cfa8e49dac6f26dc05207c5a6979 100644 --- a/Go/internal/handlers/endpoint/dashboard/registrations_id_handler.go +++ b/Go/internal/handlers/endpoint/dashboard/registrations_id_handler.go @@ -1,3 +1,4 @@ +// Package dashboard provides handlers for managing dashboard-related functionalities through HTTP endpoints. package dashboard import ( @@ -6,6 +7,7 @@ import ( "fmt" "globeboard/db" _func "globeboard/internal/func" + "globeboard/internal/utils/constants" "globeboard/internal/utils/constants/Endpoints" "globeboard/internal/utils/constants/Webhooks" "globeboard/internal/utils/structs" @@ -14,199 +16,229 @@ import ( "net/http" ) +// Constants for error and informational messages. const ( - ProvideID = "Please Provide ID" + ProvideID = "Please Provide ID" // Message to request ID provision when missing. + RegistrationRetrivalError = "Error getting registration: %v" // Error message template for retrieval issues. + RegistrationPatchError = "Error patching registration: %v" // Error message template for patching issues. ) +// RegistrationsIdHandler handles requests for the /registrations/{ID} endpoint. func RegistrationsIdHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { - case http.MethodGet: + case http.MethodGet: // Handle GET requests. handleRegGetRequest(w, r) - case http.MethodPatch: + case http.MethodPatch: // Handle PATCH requests. handleRegPatchRequest(w, r) - case http.MethodDelete: + case http.MethodDelete: // Handle DELETE requests. handleRegDeleteRequest(w, r) default: + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.RegistrationsID, r.Method) http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is:\n"+http.MethodGet+"\n"+http.MethodPatch+"\n"+http.MethodDelete, http.StatusNotImplemented) return } } +// handleRegGetRequest processes GET requests for registration data by ID. func handleRegGetRequest(w http.ResponseWriter, r *http.Request) { - ID := r.PathValue("ID") - query := r.URL.Query() - token := query.Get("token") - if token == "" { + ID := r.PathValue("ID") // Extract the 'ID' parameter from the URL path. + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Extract the 'token' parameter from the query. + if token == "" { // Validate token presence. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.RegistrationsID) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve the UUID for the API key. + if UUID == "" { // Validate UUID presence. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.RegistrationsID) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - if ID == "" || ID == " " { + if ID == "" || ID == " " { // Validate ID presence. + log.Printf(constants.ClientConnectNoID, r.Method, Endpoints.RegistrationsID) http.Error(w, ProvideID, http.StatusBadRequest) return } - reg, err := db.GetSpecificRegistration(ID, UUID) + reg, err := db.GetSpecificRegistration(ID, UUID) // Retrieve registration data from the database. if err != nil { - log.Print("Error getting document from database: ", err) + log.Printf(RegistrationRetrivalError, err) http.Error(w, "Error retrieving data from database", http.StatusNotFound) return } - w.Header().Set(ContentType, ApplicationJSON) + w.Header().Set(ContentType, ApplicationJSON) // Set the Content-Type header. - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusOK) // Set the HTTP status code to 200. - cie := new(structs.CountryInfoExternal) + cie := new(structs.CountryInfoExternal) // Create new external country info struct. cie.ID = reg.ID cie.Country = reg.Country cie.IsoCode = reg.IsoCode cie.Features = reg.Features cie.Lastchange = reg.Lastchange - err = json.NewEncoder(w).Encode(cie) + err = json.NewEncoder(w).Encode(cie) // Encode the external country info into JSON and write to the response. if err != nil { + log.Print(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.RegistrationsID, Webhooks.EventInvoke) + _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.RegistrationsID, Webhooks.EventInvoke) // Trigger webhooks for the registration. } +// handleRegPatchRequest processes PATCH requests to update registration data by ID. func handleRegPatchRequest(w http.ResponseWriter, r *http.Request) { - ID := r.PathValue("ID") - query := r.URL.Query() - token := query.Get("token") - if token == "" { + ID := r.PathValue("ID") // Extract the 'ID' parameter from the URL path. + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Extract the 'token' parameter from the query. + if token == "" { // Validate token presence. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.RegistrationsID) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve the UUID for the API key. + if UUID == "" { // Validate UUID presence. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.RegistrationsID) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - if ID == "" || ID == " " { + if ID == "" || ID == " " { // Validate ID presence. + log.Printf(constants.ClientConnectNoID, r.Method, Endpoints.RegistrationsID) http.Error(w, ProvideID, http.StatusBadRequest) return } - if r.Body == nil { + if r.Body == nil { // Validate that the request body is not empty. + log.Printf(constants.ClientConnectEmptyBody, r.Method, Endpoints.RegistrationsID) err := fmt.Sprintf("Please send a request body") http.Error(w, err, http.StatusBadRequest) return } - ci, err, errcode := patchCountryInformation(r, ID, UUID) + ci, err, errcode := patchCountryInformation(r, ID, UUID) // Process the patch request. if err != nil { - err := fmt.Sprintf("Error patching data together: %v", err) + log.Printf(RegistrationPatchError, err) + err := fmt.Sprintf(RegistrationPatchError, err) http.Error(w, err, errcode) return } - err = _func.ValidateCountryInfo(ci) + err = _func.ValidateCountryInfo(ci) // Validate the patched country information. if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + log.Printf(RegistrationPatchError, err) + err := fmt.Sprintf(RegistrationPatchError, err) + http.Error(w, err, http.StatusBadRequest) return } - err = db.UpdateRegistration(ID, UUID, ci) + err = db.UpdateRegistration(ID, UUID, ci) // Update the registration in the database. if err != nil { + log.Printf("Error saving patched data to database: %v", err) err := fmt.Sprintf("Error saving patched data to database: %v", err) http.Error(w, err, http.StatusInternalServerError) return } - reg, err := db.GetSpecificRegistration(ID, UUID) + reg, err := db.GetSpecificRegistration(ID, UUID) // Retrieve the updated registration. if err != nil { + log.Printf(RegistrationRetrivalError, err) err := fmt.Sprint("Error retrieving updated document: ", err) http.Error(w, err, http.StatusNotFound) return } - cie := new(structs.CountryInfoExternal) + cie := new(structs.CountryInfoExternal) // Create new external country info struct. cie.ID = reg.ID cie.Country = reg.Country cie.IsoCode = reg.IsoCode cie.Features = reg.Features cie.Lastchange = reg.Lastchange - w.Header().Set("content-type", "application/json") - w.WriteHeader(http.StatusAccepted) + w.Header().Set("content-type", "application/json") // Set the Content-Type header. + w.WriteHeader(http.StatusAccepted) // Set the HTTP status code to 202. response := map[string]interface{}{ - "lastChange": cie.Lastchange, + "lastChange": cie.Lastchange, // Prepare the response data. } - err = json.NewEncoder(w).Encode(response) + err = json.NewEncoder(w).Encode(response) // Encode the response data into JSON and write to the response. if err != nil { - // Handle error + log.Print(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.RegistrationsID, Webhooks.EventChange) + _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.RegistrationsID, Webhooks.EventChange) // Trigger webhooks for the change event. } +// patchCountryInformation updates the country information based on the provided patch data. func patchCountryInformation(r *http.Request, ID, UUID string) (*structs.CountryInfoInternal, error, int) { - reg, err := db.GetSpecificRegistration(ID, UUID) + reg, err := db.GetSpecificRegistration(ID, UUID) // Retrieve the specific registration. if err != nil { + log.Printf(RegistrationRetrivalError, err) return nil, err, http.StatusNotFound } - bytes, err := json.Marshal(reg) + bytes, err := json.Marshal(reg) // Marshal the registration data to JSON bytes. if err != nil { + log.Print(err) return nil, err, http.StatusInternalServerError } - var originalData map[string]interface{} + var originalData map[string]interface{} // Unmarshal the JSON bytes back to a map. err = json.Unmarshal(bytes, &originalData) if err != nil { + log.Print(err) return nil, err, http.StatusInternalServerError } - all, err := io.ReadAll(r.Body) + all, err := io.ReadAll(r.Body) // Read all data from the request body. if err != nil { + log.Print(err) return nil, err, http.StatusInternalServerError } - var patchData map[string]interface{} + var patchData map[string]interface{} // Unmarshal the patch data from the request body. err = json.Unmarshal(all, &patchData) if err != nil { + log.Print(err) return nil, err, http.StatusInternalServerError } - patchFeatures, err, errcode := validatePatchData(patchData, originalData) + patchFeatures, err, errcode := validatePatchData(patchData, originalData) // Validate and extract the patch data. if err != nil { return nil, err, errcode } - if originalData["features"] != nil { + if originalData["features"] != nil { // Merge the patch features into the original features. originalFeatures := originalData["features"].(map[string]interface{}) for key, value := range patchFeatures { originalFeatures[key] = value } } - jsonData, err := json.Marshal(originalData) + jsonData, err := json.Marshal(originalData) // Marshal the updated data to JSON. if err != nil { + log.Print(err) return nil, err, http.StatusInternalServerError } - var countryInfo *structs.CountryInfoInternal + var countryInfo *structs.CountryInfoInternal // Unmarshal the JSON data to a CountryInfoInternal struct. err = json.Unmarshal(jsonData, &countryInfo) if err != nil { + log.Print(err) return nil, err, http.StatusInternalServerError } return countryInfo, nil, http.StatusOK } +// validatePatchData checks the validity of the patch data against the original data. func validatePatchData(patchData map[string]interface{}, originalData map[string]interface{}) (map[string]interface{}, error, int) { // Check if "country" or "isoCode" fields are provided and if they are non-empty and differ from the original data. if country, ok := patchData["country"]; ok { @@ -233,47 +265,53 @@ func validatePatchData(patchData map[string]interface{}, originalData map[string return patchFeatures, nil, http.StatusOK } +// handleRegDeleteRequest processes DELETE requests to remove registration data by ID. func handleRegDeleteRequest(w http.ResponseWriter, r *http.Request) { - ID := r.PathValue("ID") - query := r.URL.Query() - token := query.Get("token") - if token == "" { + ID := r.PathValue("ID") // Extract the 'ID' parameter from the URL path. + query := r.URL.Query() // Extract the query parameters. + token := query.Get("token") // Extract the 'token' parameter from the query. + if token == "" { // Validate token presence. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.RegistrationsID) http.Error(w, ProvideAPI, http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { + UUID := db.GetAPIKeyUUID(token) // Retrieve the UUID for the API key. + if UUID == "" { // Validate UUID presence. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.RegistrationsID) err := fmt.Sprintf(APINotAccepted) http.Error(w, err, http.StatusNotAcceptable) return } - if ID == "" || ID == " " { + if ID == "" || ID == " " { // Validate ID presence. + log.Printf(constants.ClientConnectNoID, r.Method, Endpoints.RegistrationsID) http.Error(w, ProvideID, http.StatusBadRequest) return } - reg, err := db.GetSpecificRegistration(ID, UUID) + reg, err := db.GetSpecificRegistration(ID, UUID) // Retrieve the specific registration to be deleted. if err != nil { - err := fmt.Sprint("Document doesn't exist: ", err) + log.Printf(RegistrationRetrivalError, err) + err := fmt.Sprint("Error getting registration: ", err) http.Error(w, err, http.StatusNotFound) return } - err = db.DeleteRegistration(ID, UUID) + err = db.DeleteRegistration(ID, UUID) // Delete the registration from the database. if err != nil { - err := fmt.Sprintf("Error deleting data from database: %v", err) + log.Printf("Error deleting registration from database: %v", err) + err := fmt.Sprintf("Error deleting registration from database: %v", err) http.Error(w, err, http.StatusInternalServerError) return } - w.WriteHeader(http.StatusNoContent) + w.WriteHeader(http.StatusNoContent) // Set the HTTP status code to 204 (No Content). - cie := new(structs.CountryInfoExternal) + cie := new(structs.CountryInfoExternal) // Create new external country info struct. cie.ID = reg.ID cie.Country = reg.Country cie.IsoCode = reg.IsoCode cie.Features = reg.Features cie.Lastchange = reg.Lastchange - _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.RegistrationsID, Webhooks.EventDelete) + _func.LoopSendWebhooksRegistrations(UUID, cie, Endpoints.RegistrationsID, Webhooks.EventDelete) // Trigger webhooks for the delete event. } diff --git a/Go/internal/handlers/endpoint/dashboard/status_handler.go b/Go/internal/handlers/endpoint/dashboard/status_handler.go index e5babb10b45b6c908db8467f83015a4d9244cfb4..11dc1a199b711329f8ebf515fecbd4affe87aaf7 100644 --- a/Go/internal/handlers/endpoint/dashboard/status_handler.go +++ b/Go/internal/handlers/endpoint/dashboard/status_handler.go @@ -1,4 +1,4 @@ -// Package dashboard provides handlers for dashboard-related endpoints. +// Package dashboard provides handlers for managing dashboard-related functionalities through HTTP endpoints. package dashboard import ( @@ -6,6 +6,7 @@ import ( "fmt" "globeboard/db" "globeboard/internal/utils/constants" + "globeboard/internal/utils/constants/Endpoints" "globeboard/internal/utils/constants/External" "globeboard/internal/utils/structs" "log" @@ -13,74 +14,88 @@ import ( "time" ) +// getEndpointStatus sends a GET request to the specified endpoint URL and returns the HTTP status code as a string. func getEndpointStatus(endpointURL string) string { - r, err := http.NewRequest(http.MethodGet, endpointURL, nil) + r, err := http.NewRequest(http.MethodGet, endpointURL, nil) // Create a new GET request for the endpoint URL. if err != nil { - err := fmt.Errorf("error in creating request: %v", err) - log.Println(err) + log.Printf("error in creating request: %v", err) + return "Failed to create request" } - r.Header.Add("content-type", "application/json") + r.Header.Add("content-type", "application/json") // Set content-type of request. - client := &http.Client{} - defer client.CloseIdleConnections() + client := &http.Client{Timeout: 10 * time.Second} // Initialize a new HTTP client with a timeout. + defer client.CloseIdleConnections() // Ensure that idle connections are closed upon function exit. - res, err := client.Do(r) + res, err := client.Do(r) // Execute the request. if err != nil { - err := fmt.Errorf("error in response: %v", err) - log.Println(err) + log.Printf("error in receiving response: %v", err) + return "Failed to connect" } + // Ensure the response body is closed after the function exits, checking for errors. + defer func() { + if err := res.Body.Close(); err != nil { + log.Printf("error closing response body: %v", err) + } + }() - return res.Status + return res.Status // Return the status code of the response. } +// StatusHandler routes requests based on HTTP method to handle status retrieval. func StatusHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - handleStatusGetRequest(w, r) + handleStatusGetRequest(w, r) // Handle GET requests with handleStatusGetRequest. default: - http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is: "+http.MethodGet, http.StatusNotImplemented) - return + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.Status, r.Method) + http.Error(w, fmt.Sprintf("REST Method: %s not supported. Only GET is supported for this endpoint", r.Method), http.StatusNotImplemented) } } +// handleStatusGetRequest processes GET requests to retrieve and report the status of various services and endpoints. func handleStatusGetRequest(w http.ResponseWriter, r *http.Request) { - token := r.URL.Query().Get("token") - if token == "" { + token := r.URL.Query().Get("token") // Retrieve the API token from query parameters. + if token == "" { // Validate token presence. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.Status) http.Error(w, "Please provide API Token", http.StatusUnauthorized) return } - UUID := db.GetAPIKeyUUID(token) - if UUID == "" { - err := fmt.Sprintf("API key not accepted") - http.Error(w, err, http.StatusNotAcceptable) + + UUID := db.GetAPIKeyUUID(token) // Retrieve the UUID associated with the API token. + if UUID == "" { // Validate UUID presence. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.Status) + http.Error(w, "API key not accepted", http.StatusNotAcceptable) return } - webhooksUser, err := db.GetWebhooksUser(UUID) + webhooksUser, err := db.GetWebhooksUser(UUID) // Retrieve user data associated with webhooks. if err != nil { - log.Print("Error retrieving users webhooks:", err) + log.Printf("Error retrieving user's webhooks: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } + // Create a status response object. status := structs.StatusResponse{ CountriesApi: getEndpointStatus(External.CountriesAPI + "alpha?codes=no"), MeteoApi: getEndpointStatus(External.OpenMeteoAPI), CurrencyApi: getEndpointStatus(External.CurrencyAPI + "nok"), - FirebaseDB: db.TestDBConnection(), + FirebaseDB: db.TestDBConnection(), // Test the database connection. Webhooks: len(webhooksUser), - Version: constants.APIVersion, - UptimeInSeconds: fmt.Sprintf("%f Seconds", time.Since(startTime).Seconds()), + Version: constants.APIVersion, // Include the API version. + UptimeInSeconds: fmt.Sprintf("%f Seconds", time.Since(startTime).Seconds()), // Calculate uptime. } - w.Header().Add("content-type", "application/json") + w.Header().Set("Content-Type", "application/json") // Set response content type to application/json. - err = json.NewEncoder(w).Encode(status) + err = json.NewEncoder(w).Encode(status) // Encode the status response to JSON and send it. if err != nil { - http.Error(w, "Error during encoding: "+err.Error(), http.StatusInternalServerError) + log.Print(err) + http.Error(w, fmt.Sprintf("Error during encoding: %v", err), http.StatusInternalServerError) return } } -var startTime = time.Now() +var startTime = time.Now() // Track the start time of the application. diff --git a/Go/internal/handlers/endpoint/util/apikey_handler.go b/Go/internal/handlers/endpoint/util/apikey_handler.go index 55904748e90b651341780e03a57c657648bcd3aa..206c1586593d4f36403f2ac1c210d1007fd15bb3 100644 --- a/Go/internal/handlers/endpoint/util/apikey_handler.go +++ b/Go/internal/handlers/endpoint/util/apikey_handler.go @@ -1,3 +1,4 @@ +// Package util provides HTTP handlers for user and API key management within the application. package util import ( @@ -8,83 +9,94 @@ import ( "globeboard/db" _func "globeboard/internal/func" "globeboard/internal/utils/constants" + "globeboard/internal/utils/constants/Endpoints" "log" "net/http" ) +// APIKeyHandler routes API Key management requests to the appropriate functions based on the HTTP method. func APIKeyHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { - case http.MethodGet: + case http.MethodGet: // Handle GET requests handleApiKeyGetRequest(w, r) - case http.MethodDelete: + case http.MethodDelete: // Handle DELETE requests handleApiKeyDeleteRequest(w, r) default: - http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is: "+http.MethodGet, http.StatusNotImplemented) + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.ApiKey, r.Method) + http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint are: GET, DELETE", http.StatusNotImplemented) return } } +// handleApiKeyDeleteRequest handles the deletion of an API key. func handleApiKeyDeleteRequest(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() - token := query.Get("token") + token := query.Get("token") // Retrieve the token from query parameters. - UUID := r.Header.Get("Authorization") + UUID := r.Header.Get("Authorization") // Retrieve the UUID from the Authorization header. - ctx := context.Background() + ctx := context.Background() // Create a new background context. - _, err := authenticate.Client.GetUser(ctx, UUID) + _, err := authenticate.Client.GetUser(ctx, UUID) // Verify the UUID with Firebase Authentication. if err != nil { - log.Printf("error verifying UUID: %v\n", err) - http.Error(w, "Not Authorized", http.StatusUnauthorized) + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.ApiKey) + log.Printf("Error verifying UUID: %v\n", err) + http.Error(w, "Not Authorized", http.StatusUnauthorized) // Respond with unauthorized if UUID is invalid. return } - if token == "" { - http.Error(w, "Please specify API Key to delete: '?token={API_Key}' ", http.StatusBadRequest) + if token == "" || token == " " { // Validate token presence. + log.Printf(constants.ClientConnectNoToken, r.Method, Endpoints.ApiKey) + http.Error(w, "Please specify API Key to delete: '?token={API_Key}'", http.StatusBadRequest) return } - err = db.DeleteApiKey(UUID, token) + err = db.DeleteApiKey(UUID, token) // Attempt to delete the API key. if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("Error deleting API Key: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) // Respond with internal server error if deletion fails. return } - w.WriteHeader(http.StatusNoContent) + w.WriteHeader(http.StatusNoContent) // Respond with no content on successful deletion. } +// handleApiKeyGetRequest handles the creation and retrieval of a new API key. func handleApiKeyGetRequest(w http.ResponseWriter, r *http.Request) { - UDID := _func.GenerateUID(constants.DocIdLength) - key := _func.GenerateAPIKey(constants.ApiKeyLength) + UDID := _func.GenerateUID(constants.DocIdLength) // Generate a unique document ID. + key := _func.GenerateAPIKey(constants.ApiKeyLength) // Generate a new API key. - UUID := r.Header.Get("Authorization") + UUID := r.Header.Get("Authorization") // Retrieve the UUID from the Authorization header. - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/json") // Set the content type of the response to application/json. - ctx := context.Background() + ctx := context.Background() // Create a new background context. - _, err := authenticate.Client.GetUser(ctx, UUID) + _, err := authenticate.Client.GetUser(ctx, UUID) // Verify the UUID with Firebase Authentication. if err != nil { - log.Printf("error verifying UUID: %v\n", err) - http.Error(w, "Not Authorized", http.StatusUnauthorized) + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.ApiKey) + log.Printf("Error verifying UUID: %v\n", err) + http.Error(w, "Not Authorized", http.StatusUnauthorized) // Respond with unauthorized if UUID is invalid. return } - err = db.AddApiKey(UDID, UUID, key) + err = db.AddApiKey(UDID, UUID, key) // Attempt to add the new API key to the database. if err != nil { - err := fmt.Sprintf("Error creating API Key: %v", err) - http.Error(w, err, http.StatusInternalServerError) + log.Printf("Error creating API Key: %v", err) + errorMessage := fmt.Sprintf("Error creating API Key: %v", err) + http.Error(w, errorMessage, http.StatusInternalServerError) // Respond with internal server error if addition fails. return } - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusCreated) // Set HTTP status to 201 Created on successful API key creation. response := map[string]string{ - "token": key, + "token": key, // Include the new API key in the response. } if err := json.NewEncoder(w).Encode(response); err != nil { - http.Error(w, "Error encoding JSON response: "+err.Error(), http.StatusInternalServerError) + http.Error(w, "Error encoding JSON response: "+err.Error(), http.StatusInternalServerError) // Handle errors in JSON encoding. return } } diff --git a/Go/internal/handlers/endpoint/util/user_delete_handler.go b/Go/internal/handlers/endpoint/util/user_delete_handler.go index a4f3769c13bf6ccbab2829083c71dbbbcccab32b..3f944797ed57ac956bcb9a87298207f1281c059a 100644 --- a/Go/internal/handlers/endpoint/util/user_delete_handler.go +++ b/Go/internal/handlers/endpoint/util/user_delete_handler.go @@ -1,38 +1,46 @@ +// Package util provides HTTP handlers for user and API key management within the application. package util import ( "context" authenticate "globeboard/auth" + "globeboard/internal/utils/constants" + "globeboard/internal/utils/constants/Endpoints" "log" "net/http" ) +// UserDeletionHandler handles HTTP requests for user deletion. func UserDeletionHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodDelete: - deleteUser(w, r) + deleteUser(w, r) // Handle DELETE requests default: - http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is:\n"+http.MethodPost, http.StatusNotImplemented) + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.UserDeletionID, r.Method) + http.Error(w, "REST Method: "+r.Method+" not supported. Only supported method for this endpoint is:\n"+http.MethodDelete, http.StatusNotImplemented) return } } +// deleteUser processes the user deletion using the user ID from the request path. func deleteUser(w http.ResponseWriter, r *http.Request) { - ID := r.PathValue("ID") - if ID == "" || ID == " " { - http.Error(w, "Please Provide User ID", http.StatusBadRequest) + ID := r.PathValue("ID") // Extract user ID from the URL path. + if ID == "" || ID == " " { // Check if the user ID is provided. + log.Printf(constants.ClientConnectUnauthorized, r.Method, Endpoints.UserDeletionID) + http.Error(w, "Please provide User ID", http.StatusBadRequest) // Return an error if user ID is missing. return } - ctx := context.Background() + ctx := context.Background() // Create a new background context. - err := authenticate.Client.DeleteUser(ctx, ID) + err := authenticate.Client.DeleteUser(ctx, ID) // Attempt to delete user in Firebase. if err != nil { - log.Printf("error deleting user: %v\n", err) - http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("Error deleting user: %v\n", err) // Log the error. + http.Error(w, err.Error(), http.StatusInternalServerError) // Report deletion error. return } - w.WriteHeader(http.StatusNoContent) - log.Print("Successfully Deleted User: ", ID) + w.WriteHeader(http.StatusNoContent) // Set HTTP status to 204 No Content on successful deletion. + log.Printf("Successfully deleted user: %v\n", ID) // Log successful deletion. } diff --git a/Go/internal/handlers/endpoint/util/user_register_handler.go b/Go/internal/handlers/endpoint/util/user_register_handler.go index 984fa4c0e49f8c0f5d540c9c415a30f97e21108c..f6939c464c1db57587b40379aab6c5f5614cffde 100644 --- a/Go/internal/handlers/endpoint/util/user_register_handler.go +++ b/Go/internal/handlers/endpoint/util/user_register_handler.go @@ -1,3 +1,4 @@ +// Package util provides HTTP handlers for user and API key management within the application. package util import ( @@ -8,116 +9,113 @@ import ( "globeboard/db" _func "globeboard/internal/func" "globeboard/internal/utils/constants" + "globeboard/internal/utils/constants/Endpoints" "log" "net/http" "regexp" ) const ( - ISE = "Internal Server Error" + ISE = "Internal Server Error" // ISE is the error message used when an internal server error occurs. ) +// UserRegistrationHandler handles HTTP requests for user registration. func UserRegistrationHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: - registerUser(w, r) + registerUser(w, r) // Handle POST requests default: + // Log and return an error for unsupported HTTP methods + log.Printf(constants.ClientConnectUnsupported, Endpoints.UserRegistration, r.Method) http.Error(w, "REST Method: "+r.Method+" not supported. Only supported methods for this endpoint is:\n"+http.MethodPost, http.StatusNotImplemented) return } } +// registerUser processes the user registration, including input validation and user creation. func registerUser(w http.ResponseWriter, r *http.Request) { - name := r.FormValue("username") - email := r.FormValue("email") - password := r.FormValue("password") + name := r.FormValue("username") // Extract username from form data. + email := r.FormValue("email") // Extract email from form data. + password := r.FormValue("password") // Extract password from form data. - // Validate email format - if !isValidEmail(email) { + if !isValidEmail(email) { // Validate the email. + // Log a message indicating that a client attempted to register using a malformed email. + log.Printf("Client attempted to register a user with malformed email.") http.Error(w, "Invalid email format", http.StatusBadRequest) return } - // Validate password strength - if !isValidPassword(password) { + if !isValidPassword(password) { // Validate password strength. + // Log a message indicating that a client attempted to register using a weak password. + log.Printf("Client attempted to register a user with a weak password.") http.Error(w, "Password does not meet complexity requirements", http.StatusBadRequest) return } - ctx := context.Background() + ctx := context.Background() // Create a new background context. + // Define user creation parameters. params := (&auth.UserToCreate{}). DisplayName(name). Email(email). Password(password) - u, err := authenticate.Client.CreateUser(ctx, params) + u, err := authenticate.Client.CreateUser(ctx, params) // Attempt to create user in Firebase. if err != nil { - log.Printf("error creating user: %v\n", err) - http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("Error creating user: %v\n", err) // Log the error. + http.Error(w, err.Error(), http.StatusInternalServerError) // Report creation error. return } - w.Header().Set("content-type", "application/json") + w.Header().Set("content-type", "application/json") // Set response content type. - UDID := _func.GenerateUID(constants.DocIdLength) - key := _func.GenerateAPIKey(constants.ApiKeyLength) + UDID := _func.GenerateUID(constants.DocIdLength) // Generate a unique document ID. + key := _func.GenerateAPIKey(constants.ApiKeyLength) // Generate a new API key. - err = db.AddApiKey(UDID, u.UID, key) + err = db.AddApiKey(UDID, u.UID, key) // Store the new API key in the database. if err != nil { - log.Printf("error saving API Key: %v\n", err) - http.Error(w, ISE, http.StatusInternalServerError) + log.Printf("Error saving API Key: %v\n", err) // Log the error. + http.Error(w, ISE, http.StatusInternalServerError) // Report API key storage error. return } + // Prepare the JSON response with the user ID and token. response := struct { - Token string `json:"token"` - UserID string `json:"userid"` + Token string `json:"token"` // API token. + UserID string `json:"userid"` // Firebase user ID. }{ Token: key, UserID: u.UID, } - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusCreated) // Set HTTP status to 201 Created. - encoder := json.NewEncoder(w) - err = encoder.Encode(response) + encoder := json.NewEncoder(w) // Initialize JSON encoder. + err = encoder.Encode(response) // Encode the response as JSON. if err != nil { - log.Printf("Error encoding JSON response: %v\n", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + log.Printf("Error encoding JSON response: %v\n", err) // Log encoding error. + http.Error(w, ISE, http.StatusInternalServerError) // Report encoding error. return } + + // Log successful creation events. + log.Printf("Successfully created user: %v with API Key: %v\n", response.UserID, response.Token) } +// isValidEmail checks if the provided email string matches the expected format. func isValidEmail(email string) bool { - regex := regexp.MustCompile(`(?i)^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) - return regex.MatchString(email) + regex := regexp.MustCompile(`(?i)^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) // Regular expression for email validation. + return regex.MatchString(email) // Return true + // if email matches the regex. } +// isValidPassword checks if the provided password meets complexity requirements. func isValidPassword(password string) bool { - // Check the length - if len(password) < 12 { - return false - } - - // Check for at least one uppercase letter - hasUppercase := regexp.MustCompile(`[A-Z]`).MatchString(password) - if !hasUppercase { + if len(password) < 12 { // Password length should be at least 12 characters. return false } - - // Check for at least one digit - hasDigit := regexp.MustCompile(`[0-9]`).MatchString(password) - if !hasDigit { - return false - } - - // Check for at least one special character - hasSpecial := regexp.MustCompile(`[!@#$&*]`).MatchString(password) - if !hasSpecial { - return false - } - - // If all checks pass - return true + hasUppercase := regexp.MustCompile(`[A-Z]`).MatchString(password) // Check for at least one uppercase letter. + hasDigit := regexp.MustCompile(`[0-9]`).MatchString(password) // Check for at least one digit. + hasSpecial := regexp.MustCompile(`[!@#$&*]`).MatchString(password) // Check for at least one special character. + return hasUppercase && hasDigit && hasSpecial // Return true if all conditions are met. } diff --git a/Go/internal/utils/constants/Endpoints/endpoints.go b/Go/internal/utils/constants/Endpoints/endpoints.go index 4cb434930f0da521d0b515c28fd8182e47f00ffc..bf77b59c30085e408c3861bf4c6e27162afa729e 100644 --- a/Go/internal/utils/constants/Endpoints/endpoints.go +++ b/Go/internal/utils/constants/Endpoints/endpoints.go @@ -1,21 +1,33 @@ -// Package Endpoints provides constant endpoint paths used in the application. +// Package Endpoints provides constants for fully constructed endpoint paths +// using base paths and versions defined in other packages. package Endpoints import ( - "globeboard/internal/utils/constants" - "globeboard/internal/utils/constants/Paths" + "globeboard/internal/utils/constants" // Imports constants for the API version. + "globeboard/internal/utils/constants/Paths" // Imports base paths for endpoints. ) const ( - ApiKey = Paths.Util + constants.APIVersion + "/key" + // ApiKey endpoint for API key operations. + ApiKey = Paths.Util + constants.APIVersion + "/key" + // UserRegistration endpoint for user registration operations. UserRegistration = Paths.Util + constants.APIVersion + "/user/register" - UserDeletion = Paths.Util + constants.APIVersion + "/user/delete" - UserDeletionId = Paths.Util + constants.APIVersion + "/user/delete/{ID}" - RegistrationsID = Paths.Dashboards + constants.APIVersion + "/registrations/{ID}" - Registrations = Paths.Dashboards + constants.APIVersion + "/registrations" - DashboardsID = Paths.Dashboards + constants.APIVersion + "/dashboard/{ID}" - Dashboards = Paths.Dashboards + constants.APIVersion + "/dashboard" - NotificationsID = Paths.Dashboards + constants.APIVersion + "/notifications/{ID}" - Notifications = Paths.Dashboards + constants.APIVersion + "/notifications" - Status = Paths.Dashboards + constants.APIVersion + "/status" + // UserDeletion endpoint URL for user registration operations without the ID wildcard. + UserDeletion = Paths.Util + constants.APIVersion + "/user/delete" + // UserDeletionID endpoint for user deletion operations by ID. + UserDeletionID = Paths.Util + constants.APIVersion + "/user/delete/{ID}" + // RegistrationsID endpoint for accessing specific registration by ID. + RegistrationsID = Paths.Dashboards + constants.APIVersion + "/registrations/{ID}" + // Registrations endpoint for accessing registrations without ID. + Registrations = Paths.Dashboards + constants.APIVersion + "/registrations" + // DashboardsID endpoint for accessing specific dashboard by ID. + DashboardsID = Paths.Dashboards + constants.APIVersion + "/dashboard/{ID}" + // Dashboards endpoint URL for dashboard operations without the ID wildcard. + Dashboards = Paths.Dashboards + constants.APIVersion + "/dashboard" + // NotificationsID endpoint for accessing specific notification by ID. + NotificationsID = Paths.Dashboards + constants.APIVersion + "/notifications/{ID}" + // Notifications endpoint for accessing notifications operations without ID. + Notifications = Paths.Dashboards + constants.APIVersion + "/notifications" + // Status endpoint for checking the status of the dashboard services. + Status = Paths.Dashboards + constants.APIVersion + "/status" ) diff --git a/Go/internal/utils/constants/External/external.go b/Go/internal/utils/constants/External/external.go index 7934e16b7004cbf1325a73529f19912f397dea54..ecc6dd035be3c7e42456f886eb28a26b924958f9 100644 --- a/Go/internal/utils/constants/External/external.go +++ b/Go/internal/utils/constants/External/external.go @@ -1,11 +1,8 @@ -// Package External provides external API endpoints used throughout the application. +// Package External defines constants for URLs to external API endpoints used within the application. package External -// CurrencyAPI represents the endpoint for the Currency API. -// OpenMeteoAPI represents the endpoint for the Open-Meteo API. -// CountriesAPI represents the endpoint for the RESTCountries API. const ( - CurrencyAPI = "http://129.241.150.113:9090/currency/" - OpenMeteoAPI = "https://api.open-meteo.com/v1/forecast" - CountriesAPI = "http://129.241.150.113:8080/v3.1/" + CurrencyAPI = "http://129.241.150.113:9090/currency/" // CurrencyAPI specifies the endpoint URL for the Currency API. + OpenMeteoAPI = "https://api.open-meteo.com/v1/forecast" // OpenMeteoAPI specifies the endpoint URL for the Open-Meteo API. + CountriesAPI = "http://129.241.150.113:8080/v3.1/" // CountriesAPI specifies the endpoint URL for the RESTCountries API. ) diff --git a/Go/internal/utils/constants/Firestore/firebase.go b/Go/internal/utils/constants/Firestore/firebase.go index 71af290e113cf8b8cedd1e5993481259cf31f49a..57e1067928e4026465296b97eb47075ce830ef37 100644 --- a/Go/internal/utils/constants/Firestore/firebase.go +++ b/Go/internal/utils/constants/Firestore/firebase.go @@ -1,7 +1,8 @@ +// Package Firestore defines constants for the names of Firestore collections used in the application. package Firestore const ( - ApiKeyCollection = "API_keys" - RegistrationCollection = "Registrations" - WebhookCollection = "Webhooks" + ApiKeyCollection = "API_keys" // ApiKeyCollection specifies the Firestore collection name for API keys. + RegistrationCollection = "Registrations" // RegistrationCollection specifies the Firestore collection name for country registrations. + WebhookCollection = "Webhooks" // WebhookCollection specifies the Firestore collection name for webhook data. ) diff --git a/Go/internal/utils/constants/Paths/paths.go b/Go/internal/utils/constants/Paths/paths.go index 1cce018d3d0ef3711e6e7a686ea338ab884e2283..d4e504c9e8818a3dbb0b4d41a0a7a90be48341d9 100644 --- a/Go/internal/utils/constants/Paths/paths.go +++ b/Go/internal/utils/constants/Paths/paths.go @@ -1,11 +1,8 @@ -// Package Paths provides constant paths used in the application. +// Package Paths defines constant URL paths used within the application to reference different endpoint roots. package Paths -// Root represents the root path. -// Util represents the root path to the util endpoints -// Dashboards represents the root path to the dashboard endpoints const ( - Root = "/" - Util = "/util/" - Dashboards = "/dashboards/" + Root = "/" // Root represents the root path of the application. + Util = "/util/" // Util represents the root path to the utility-specific endpoints. + Dashboards = "/dashboards/" // Dashboards represent the root path to the dashboard endpoints. ) diff --git a/Go/internal/utils/constants/Webhooks/webhooks.go b/Go/internal/utils/constants/Webhooks/webhooks.go index e39aa200c7d5c5554564caed67a24ee34f9ea09a..c30ee35218f74a91365497a91a1102aff98f2280 100644 --- a/Go/internal/utils/constants/Webhooks/webhooks.go +++ b/Go/internal/utils/constants/Webhooks/webhooks.go @@ -1,18 +1,19 @@ +// Package Webhooks defines constants for webhook titles, colors, and event types used for Webhook notifications. package Webhooks const ( - POSTTitle = "Registered New Country Data to GlobeBoard" - PUTTitle = "Changed Country Data on GlobeBoard" - DELETETitle = "Deleted Country Data from GlobeBoard" - GETTitle = "Invoked Country Data from GlobeBoard" + POSTTitle = "Registered New Country Data to GlobeBoard" // POSTTitle defines the title for POST webhook events. + PATCHTitle = "Changed Country Data on GlobeBoard" // PATCHTitle defines the title for PATCH webhook events. + DELETETitle = "Deleted Country Data from GlobeBoard" // DELETETitle defines the title for DELETE webhook events. + GETTitle = "Invoked Country Data from GlobeBoard" // GETTitle defines the title for GET webhook events. - POSTColor = 2664261 //Success Color - PUTColor = 16761095 //Update Color - DELETEColor = 14431557 //Warning Color - GETColor = 1548984 //Info Color + POSTColor = 2664261 // Success Color - light green + PUTColor = 16761095 // Update Color - bright orange + DELETEColor = 14431557 // Warning Color - pale red + GETColor = 1548984 // Info Color - light blue - EventRegister = "REGISTER" // POST - EventChange = "CHANGE" // PUT - EventDelete = "DELETE" // DELETE - EventInvoke = "INVOKE" // GET + EventRegister = "REGISTER" // EventRegister defines the event type for POST operations. + EventChange = "CHANGE" // EventChange defines the event type for PATCH operations. + EventDelete = "DELETE" // EventDelete defines the event type for DELETE operations. + EventInvoke = "INVOKE" // EventInvoke defines the event type for GET operations. ) diff --git a/Go/internal/utils/constants/constants.go b/Go/internal/utils/constants/constants.go index bca0e37fd438ed3fcdc27f95556e8df3d4418356..ea1de77d4e70da4c506584fe6ab16b4ee2bb0f90 100644 --- a/Go/internal/utils/constants/constants.go +++ b/Go/internal/utils/constants/constants.go @@ -1,9 +1,20 @@ -// Package constants provide constant values used throughout the application. +// Package constants provide constant values that are used within the application. package constants const ( - APIVersion = "v1" - ApiKeyLength = 20 - DocIdLength = 24 - IdLength = 16 + APIVersion = "v1" // APIVersion specifies the version of the API being used. + ApiKeyLength = 20 // ApiKeyLength specifies the length of API keys generated. + DocIdLength = 24 // DocIdLength specifies the length of document identifiers. + IdLength = 20 // IdLength specifies the length of general purpose identifiers. + + // ClientConnectUnsupported formats an error message for when a client tries to connect using an unsupported method. + ClientConnectUnsupported = "Client attempted to connect to %s with unsupported method: %s\n" + // ClientConnectNoToken formats an error message for connection attempts where no token is provided. + ClientConnectNoToken = "Failed %s attempt to %s: No Token.\n" + // ClientConnectNoID formats an error message for connection attempts where no ID is provided. + ClientConnectNoID = "Failed %s attempt to %s: No ID.\n" + // ClientConnectUnauthorized formats an error message for unauthorized connection attempts. + ClientConnectUnauthorized = "Unauthorized %s attempted to %s.\n" + // ClientConnectEmptyBody formats an error message for connection attempts with no body content. + ClientConnectEmptyBody = "Failed %s attempt to %s: No Body.\n" ) diff --git a/Go/internal/utils/structs/structs.go b/Go/internal/utils/structs/structs.go index e0892b96b3b1e4c71fd442ed7cc078f45f1cbd19..1fe04c347f4540bf008a0271950a0cf47a2e8f32 100644 --- a/Go/internal/utils/structs/structs.go +++ b/Go/internal/utils/structs/structs.go @@ -1,130 +1,129 @@ -// Package structs define structures used within the application. +// Package structs define data structures used within the application. package structs import "time" +// APIKey represents a structure for storing an API key associated with a unique identifier (UUID). type APIKey struct { - UUID string `json:"uuid"` - APIKey string `json:"api_key"` + UUID string `json:"uuid"` // The unique identifier for the API key + APIKey string `json:"api_key"` // The actual API key string } -// Registrations Structs - +// CountryInfoExternal is a structure to store external-facing country information. type CountryInfoExternal struct { - ID string `json:"id"` - Country string `json:"country"` - IsoCode string `json:"isoCode"` - Features Features `json:"features"` - Lastchange time.Time `json:"lastchange"` + ID string `json:"id"` // Unique identifier for the country information + Country string `json:"country"` // Name of the country + IsoCode string `json:"isoCode"` // ISO code for the country + Features Features `json:"features"` // Features available for the country + Lastchange time.Time `json:"lastchange"` // The last time the information was updated } +// CountryInfoInternal is a structure to store internal country information. type CountryInfoInternal struct { - ID string `json:"id"` - UUID string `json:"uuid"` - Country string `json:"country"` - IsoCode string `json:"isoCode"` - Features Features `json:"features"` - Lastchange time.Time `json:"lastchange"` + ID string `json:"id"` // Unique identifier for the country information + UUID string `json:"uuid"` // An additional UUID for internal use + Country string `json:"country"` // Name of the country + IsoCode string `json:"isoCode"` // ISO code for the country + Features Features `json:"features"` // Features available for the country + Lastchange time.Time `json:"lastchange"` // The last time the information was updated } +// Features struct encapsulates different geographical and demographic features of a country. type Features struct { - Temperature bool `json:"temperature"` - Precipitation bool `json:"precipitation"` - Capital bool `json:"capital"` - Coordinates bool `json:"coordinates"` - Population bool `json:"population"` - Area bool `json:"area"` - TargetCurrencies []string `json:"targetCurrencies"` + Temperature bool `json:"temperature"` // Boolean flag indicating retrival of temperature data + Precipitation bool `json:"precipitation"` // Boolean flag indicating retrival of precipitation data + Capital bool `json:"capital"` // Boolean flag indicating retrival of capital information + Coordinates bool `json:"coordinates"` // Boolean flag indicating retrival of geographical coordinates + Population bool `json:"population"` // Boolean flag indicating retrival of population data + Area bool `json:"area"` // Boolean flag indicating retrival of area data + TargetCurrencies []string `json:"targetCurrencies"` // List of target currencies } -// Dashboard Structs - +// DashboardResponse defines the structure for dashboard service responses. type DashboardResponse struct { - ID string `json:"id"` - Country string `json:"country"` - IsoCode string `json:"iso_code"` - Features FeaturesDashboard `json:"features"` - LastRetrieval string `json:"lastRetrieval"` + ID string `json:"id"` // Unique identifier for the dashboard entry + Country string `json:"country"` // Country name + IsoCode string `json:"iso_code"` // ISO code for the country + Features FeaturesDashboard `json:"features"` // Detailed features used in the dashboard + LastRetrieval string `json:"lastRetrieval"` // Last retrieval time of the data } +// FeaturesDashboard defines detailed features available on the dashboard for a country. type FeaturesDashboard struct { - Temperature string `json:"temperature,omitempty"` - Precipitation string `json:"precipitation,omitempty"` - Capital string `json:"capital,omitempty"` - Coordinates *CoordinatesDashboard `json:"coordinates,omitempty"` - Population int `json:"population,omitempty"` - Area string `json:"area,omitempty"` - TargetCurrencies map[string]float64 `json:"targetCurrencies,omitempty"` + Temperature string `json:"temperature,omitempty"` // Temperature information + Precipitation string `json:"precipitation,omitempty"` // Precipitation information + Capital string `json:"capital,omitempty"` // Capital city + Coordinates *CoordinatesDashboard `json:"coordinates,omitempty"` // Geographical coordinates + Population int `json:"population,omitempty"` // Population number + Area string `json:"area,omitempty"` // Area in square kilometers + TargetCurrencies map[string]float64 `json:"targetCurrencies,omitempty"` // Currency exchange rates } -// CoordinatesDashboard -// @Description: +// CoordinatesDashboard defines latitude and longitude for a geographical location. type CoordinatesDashboard struct { - Latitude string `json:"latitude,omitempty"` - Longitude string `json:"longitude,omitempty"` + Latitude string `json:"latitude,omitempty"` // Latitude + Longitude string `json:"longitude,omitempty"` // Longitude } -// Status structs - -// StatusResponse -// @Description: +// StatusResponse defines the current status of various APIs and services used by the application. type StatusResponse struct { - CountriesApi string `json:"countries_api"` - MeteoApi string `json:"meteo_api"` - CurrencyApi string `json:"currency_api"` - FirebaseDB string `json:"firebase_db"` - Webhooks int `json:"webhooks"` - Version string `json:"version"` - UptimeInSeconds string `json:"uptime"` + CountriesApi string `json:"countries_api"` // Status of the countries API + MeteoApi string `json:"meteo_api"` // Status of the meteorological API + CurrencyApi string `json:"currency_api"` // Status of the currency exchange API + FirebaseDB string `json:"firebase_db"` // Status of the Firebase database + Webhooks int `json:"webhooks"` // Number of active webhooks + Version string `json:"version"` // Current version of the application + UptimeInSeconds string `json:"uptime"` // Uptime in seconds } -// Webhooks Structs - +// WebhookResponse defines the structure for external webhook responses. type WebhookResponse struct { - ID string `json:"id"` - URL string `json:"url"` - Country string `json:"country,omitempty"` - Event []string `json:"event"` + ID string `json:"id"` // Unique identifier for the webhook + URL string `json:"url"` // URL of the webhook + Country string `json:"country,omitempty"` // Country associated with the webhook + Event []string `json:"event"` // Events that trigger the webhook } -type WebhookGet struct { - ID string `json:"id"` - UUID string `json:"uuid"` - URL string `json:"url"` - Country string `json:"country,omitempty"` - Event []string `json:"event"` +// WebhookInternal is a structure to store internal-facing webhook information. +type WebhookInternal struct { + ID string `json:"id"` // Unique identifier for the webhook + UUID string `json:"uuid"` // UUID associated with the webhook + URL string `json:"url"` // URL of the webhook + Country string `json:"country,omitempty"` // Country associated with the webhook + Event []string `json:"event"` // Events that trigger the webhook } +// Author defines an author element for use in structured messages. type Author struct { - Name string `json:"name"` + Name string `json:"name"` // Name of the author } +// Field defines a single field in a structured message. type Field struct { - Name string `json:"name"` - Value string `json:"value"` - Inline bool `json:"inline"` + Name string `json:"name"` // Name of the field + Value string `json:"value"` // Value of the field + Inline bool `json:"inline"` // Whether the field is inline } +// Footer defines a footer element for use in structured messages. type Footer struct { - Text string `json:"text"` + Text string `json:"text"` // Text content of the footer } -// Embed -// @Description: +// Embed defines the structure for embedded content in messages. type Embed struct { - Title string `json:"title"` - Author Author `json:"author"` - Description string `json:"description"` - Timestamp string `json:"timestamp"` - Color int `json:"color"` - Fields []Field `json:"fields"` - Footer Footer `json:"footer"` + Title string `json:"title"` // Title of the embed + Author Author `json:"author"` // Author information + Description string `json:"description"` // Description text + Timestamp string `json:"timestamp"` // Timestamp for the embed + Color int `json:"color"` // Color code for the embed bar + Fields []Field `json:"fields"` // Fields included in the embed + Footer Footer `json:"footer"` // Footer information } -// WebhookPayload -// @Description: +// WebhookPayload defines the payload structure for webhook messages. type WebhookPayload struct { - Username string `json:"username"` - AvatarURL string `json:"avatar_url"` - Embeds []Embed `json:"embeds"` + Username string `json:"username"` // Username for the webhook message + AvatarURL string `json:"avatar_url"` // URL to the avatar image + Embeds []Embed `json:"embeds"` // Embedded content in the message } diff --git a/README.md b/README.md index 95847d52e36d707ca7d5be09578955d122f16aad..77851124c76999c9981cf3350768e219435d9e6e 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,6 @@ GlobeBoard Go REST API is a webapplication REST API service designed to provide This code was developed by: - Torgrim Thorsen [@Nintendo_Alex](https://git.gvk.idi.ntnu.no/Nintendo_Alex) -In collaboration with: - -- Robin Jahre [@robinja](https://git.gvk.idi.ntnu.no/Nrobinja) -- Phrot Vedal [@BossTheModern](https://git.gvk.idi.ntnu.no/BossTheModern) - ## Features - User Registration through Firestore Auth