From eec0b2d35668bae44974d83a8d8efb21395302f1 Mon Sep 17 00:00:00 2001 From: "Aarthi Manivannan, Premanathan Aarthi Manivannan" Date: Sun, 14 Dec 2025 19:03:33 +0100 Subject: [PATCH] Update 6 files - /src/main/Resources/application.yml - /src/main/config/AppProperties.java - /src/main/service/ExternalApiService.java - /src/main/controller/GenerateController.java - /src/test/AppPropertiesTest.java - /README.md --- README.md | 54 ++++--------- src/main/Resources/application.yml | 7 ++ src/main/config/AppProperties.java | 13 +++ src/main/controller/GenerateController.java | 26 ++++++ src/main/service/ExternalApiService.java | 90 +++++++++++++++++++++ src/test/AppPropertiesTest.java | 20 +++++ 6 files changed, 172 insertions(+), 38 deletions(-) create mode 100644 src/main/Resources/application.yml create mode 100644 src/main/config/AppProperties.java create mode 100644 src/main/controller/GenerateController.java create mode 100644 src/main/service/ExternalApiService.java create mode 100644 src/test/AppPropertiesTest.java diff --git a/README.md b/README.md index eb1c002..becf2b7 100644 --- a/README.md +++ b/README.md @@ -88,49 +88,27 @@ Show your appreciation to those who have contributed to the project. ## License For open source projects, say how it is licensed. +--- -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. -## 🔐 Secure API Key Management (Sprint 4 – V2D Document) +## Sprint 4 – Secure API Key Management -This project uses secure environment variables to store and manage all external API keys -(required for LLM/Transcription APIs). No API key is ever committed into the repository. +In Sprint 4, secure handling of API keys was implemented for the V2D (Video to Document) framework. -### ✔ How the API Key Works -The application reads the key from an environment variable named: +### Implementation Overview +- API keys are **not stored in the source code** +- The backend loads the key from an **environment variable** +- A single configuration works for all users without manual setup +- Secrets are protected from being exposed in the repository or frontend -`LLM_API_KEY` - -Spring Boot loads it automatically using the following configuration in `application.yml`: +### Configuration +The backend expects the following environment variable: -### ✔ Local Development (developer machines) -Developers must manually set their API key locally: +This variable is injected at runtime by the deployment or CI/CD environment and referenced in `application.yml`. +### Security Benefits +- Prevents accidental exposure of API keys +- Ensures secure collaboration in GitLab +- Follows best practices for secret management -### ✔ GitLab CI/CD Setup (secure by default) -To provide the key for all environments securely: - -1. Go to **GitLab → Settings → CI/CD → Variables** -2. Add variable: - - **Key:** `LLM_API_KEY` - - **Value:** your real API key - - **Masked:** ✓ Enable - - **Protected:** (optional) -3. Save. - -Pipelines will automatically use the secure key without exposing it. - -### ✔ Security Guarantees -- The API key is **not stored** in the repository -- `.env` files are ignored through `.gitignore` -- The key is **never printed**, logged, or exposed to users -- Every new user of V2D Document can use the system **without needing their own key** - -### ✔ Files Added in This User Story -- `src/main/resources/application.yml` -- `src/main/java/com/v2d/document/config/AppProperties.java` -- `src/main/java/com/v2d/document/service/ExternalApiService.java` -- `src/test/java/com/v2d/document/config/AppPropertiesTest.java` - -This completes Sprint 4 User Story: **Backend – Secure Management & Storage of API Keys**. +--- diff --git a/src/main/Resources/application.yml b/src/main/Resources/application.yml new file mode 100644 index 0000000..04b5693 --- /dev/null +++ b/src/main/Resources/application.yml @@ -0,0 +1,7 @@ +spring: + application: + name: v2d-document + +app: + external: + apiKey: ${LLM_API_KEY:} diff --git a/src/main/config/AppProperties.java b/src/main/config/AppProperties.java new file mode 100644 index 0000000..a080c4c --- /dev/null +++ b/src/main/config/AppProperties.java @@ -0,0 +1,13 @@ +package com.v2d.document.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "app.external") +public class AppProperties { + private String apiKey; + + public String getApiKey() { return apiKey; } + public void setApiKey(String apiKey) { this.apiKey = apiKey; } +} diff --git a/src/main/controller/GenerateController.java b/src/main/controller/GenerateController.java new file mode 100644 index 0000000..bd56478 --- /dev/null +++ b/src/main/controller/GenerateController.java @@ -0,0 +1,26 @@ +package com.v2d.document.controller; + +import com.v2d.document.service.ExternalApiService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/generate") +public class GenerateController { + + private final ExternalApiService externalApiService; + + public GenerateController(ExternalApiService externalApiService) { + this.externalApiService = externalApiService; + } + + @PostMapping + public ResponseEntity generate(@RequestBody Map body) { + // Build provider payload from the user's body (transform safely) + String payload = "{\"text\": \"use this text\"}"; // adapt for real usage + String providerResponse = externalApiService.callProvider(payload); + return ResponseEntity.ok(providerResponse); + } +} diff --git a/src/main/service/ExternalApiService.java b/src/main/service/ExternalApiService.java new file mode 100644 index 0000000..677c02f --- /dev/null +++ b/src/main/service/ExternalApiService.java @@ -0,0 +1,90 @@ +package com.v2d.document.service; + +import com.v2d.document.config.AppProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +@Service +public class ExternalApiService { + private final Logger log = LoggerFactory.getLogger(ExternalApiService.class); + private final AppProperties props; + private final HttpClient http = HttpClient.newHttpClient(); + + public ExternalApiService(AppProperties props) { + this.props = props; + } + + public String callProvider(String jsonPayload) { + String key = props.getApiKey(); + if (key == null || key.isBlank()) { + log.warn("External API key is not configured."); + throw new IllegalStateException("External API key missing"); + } + + try { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create("https://api.example.com/endpoint")) // replace with real endpoint + .header("Authorization", "Bearer " + key) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString()); + return resp.body(); + } catch (Exception e) { + log.error("External API call failed: {}", e.getMessage()); + throw new RuntimeException("External API call failed", e); + } + } +} +package com.v2d.document.service; + +import com.v2d.document.config.AppProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +@Service +public class ExternalApiService { + private final Logger log = LoggerFactory.getLogger(ExternalApiService.class); + private final AppProperties props; + private final HttpClient http = HttpClient.newHttpClient(); + + public ExternalApiService(AppProperties props) { + this.props = props; + } + + public String callProvider(String jsonPayload) { + String key = props.getApiKey(); + if (key == null || key.isBlank()) { + log.warn("External API key is not configured."); + throw new IllegalStateException("External API key missing"); + } + + try { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create("https://api.example.com/endpoint")) // replace with real endpoint + .header("Authorization", "Bearer " + key) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString()); + return resp.body(); + } catch (Exception e) { + log.error("External API call failed: {}", e.getMessage()); + throw new RuntimeException("External API call failed", e); + } + } +} diff --git a/src/test/AppPropertiesTest.java b/src/test/AppPropertiesTest.java new file mode 100644 index 0000000..76a2a1f --- /dev/null +++ b/src/test/AppPropertiesTest.java @@ -0,0 +1,20 @@ +package com.v2d.document.config; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AppPropertiesTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(AppProperties.class) + .withPropertyValues("app.external.apiKey=TEST_KEY"); + + @Test + void bindsApiKey() { + contextRunner.run(context -> { + AppProperties props = context.getBean(AppProperties.class); + assertThat(props.getApiKey()).isEqualTo("TEST_KEY"); + }); + } +}