From b2ac418ae5b31b3800d35899cb8301b6d18a5a62 Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:50:39 +0000
Subject: [PATCH 01/18] Hithomelabs/CFTunnels#114: Add JavaDoc documentation to
main application class
---
.../CFTunnels/CfTunnelsApplication.java | 40 +++++++++++++++++--
1 file changed, 36 insertions(+), 4 deletions(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java b/src/main/java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java
index 1ea81fd..1b46911 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java
@@ -1,13 +1,45 @@
package com.hithomelabs.CFTunnels;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
+/**
+ * Main Spring Boot application class for Cloudflare Tunnels API.
+ *
+ * This application provides a RESTful API for managing Cloudflare Tunnels,
+ * allowing users to create tunnel mappings to services with an approval workflow.
+ *
+ * Features:
+ *
+ * - Create, update, and delete Cloudflare tunnels
+ * - Add ingress mappings to tunnels
+ * - Request/approval workflow for mapping changes
+ * - OIDC-based authentication with role-based access
+ *
+ *
+ * Technology Stack:
+ *
+ * - Java 17
+ * - Spring Boot 3.x
+ * - Spring Data JPA
+ * - Spring Security with OIDC
+ * - H2 Database (configurable for PostgreSQL)
+ * - Cloudflare API
+ *
+ *
+ * Access the API documentation at:
+ * {@code /swagger-ui.html} for the Swagger/OpenAPI UI
+ *
+ * @see Cloudflare Tunnel Documentation
+ * @since 1.0.0
+ */
@SpringBootApplication
public class CfTunnelsApplication {
+ /**
+ * Main entry point for the application.
+ *
+ * @param args command line arguments passed to the application
+ */
public static void main(String[] args) {
SpringApplication.run(CfTunnelsApplication.class, args);
}
-}
+}
\ No newline at end of file
--
2.45.2
From 6bf138ea7aacb058ec74c269f05b619299fe0a05 Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:51:07 +0000
Subject: [PATCH 02/18] Hithomelabs/CFTunnels#114: Add JavaDoc to Tunnel entity
---
.../hithomelabs/CFTunnels/Entity/Tunnel.java | 43 ++++++++++++++++++-
1 file changed, 41 insertions(+), 2 deletions(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/Tunnel.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/Tunnel.java
index 3457c9e..8892caf 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/Entity/Tunnel.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/Tunnel.java
@@ -8,21 +8,60 @@ import lombok.Setter;
import java.util.UUID;
+/**
+ * JPA Entity representing a Cloudflare Tunnel configuration.
+ *
+ * This entity stores the locally cached configuration of a Cloudflare Tunnel,
+ * linking the tunnel's unique identifier from Cloudflare with its local
+ * environment name for easier reference and management.
+ *
+ * The entity is uniquely constrained by tunnel ID and environment name,
+ * ensuring only one configuration exists per tunnel per environment.
+ *
+ * Database Table: {@code tunnels}
+ *
+ * Relationships:
+ *
+ * - One Tunnel has many Mappings (via tunnel_id foreign key)
+ *
+ *
+ * @see Mapping
+ * @see Request
+ */
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
-@Table(name="tunnels")
+@Table(name = "tunnels")
public class Tunnel {
+ /**
+ * Unique identifier for the tunnel (UUID).
+ *
+ * This corresponds to the Cloudflare-assigned tunnel ID
+ * and serves as the primary key for this entity.
+ */
@Id
@Column(columnDefinition = "uuid", insertable = false, updatable = false, nullable = false)
private UUID id;
+ /**
+ * Environment name associated with this tunnel configuration.
+ *
+ * Examples: "production", "staging", "development"
+ * Each tunnel can be configured for multiple environments.
+ */
@Column(length = 10, unique = true, nullable = false)
private String environment;
+ /**
+ * Display name of the tunnel as configured in Cloudflare.
+ *
+ * This is the user-friendly name assigned to the tunnel
+ * when it was created in Cloudflare Zero Trust Dashboard
+ * or via the Cloudflare API.
+ */
@Column(length = 50, unique = true, nullable = false)
private String name;
-}
+}
\ No newline at end of file
--
2.45.2
From e8d535efdabb0d389f91be015cb535c13a33bf4a Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:51:19 +0000
Subject: [PATCH 03/18] Hithomelabs/CFTunnels#114: Add JavaDoc to Mapping
entity
---
.../hithomelabs/CFTunnels/Entity/Mapping.java | 52 ++++++++++++++++++-
1 file changed, 50 insertions(+), 2 deletions(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/Mapping.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/Mapping.java
index 113f740..e89f674 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/Entity/Mapping.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/Mapping.java
@@ -6,9 +6,29 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-
import java.util.UUID;
+/**
+ * JPA Entity representing an ingress mapping configuration for a Cloudflare Tunnel.
+ *
+ * A mapping defines how incoming traffic should be routed through the tunnel
+ * to your internal services. It specifies the protocol, port, and subdomain
+ * where the service will be accessible.
+ *
+ * Database Table: {@code mappings}
+ *
+ * Example Usage:
+ *
+ * Mapping mapping = new Mapping();
+ * mapping.setPort(8080);
+ * mapping.setProtocol(Protocol.HTTP);
+ * mapping.setSubdomain("api");
+ *
+ *
+ * @see Tunnel
+ * @see Protocol
+ * @see Request
+ */
@Entity
@Getter
@Setter
@@ -17,22 +37,50 @@ import java.util.UUID;
@Table(name = "mappings")
public class Mapping {
+ /**
+ * Unique identifier for this mapping (auto-generated UUID).
+ */
@Id
@GeneratedValue
@Column(columnDefinition = "uuid", nullable = false, unique = true)
private UUID id;
+ /**
+ * Port number where the internal service is running.
+ *
+ * Example: 8080 for a Spring Boot application's default port.
+ */
@Column(nullable = false)
private int port;
+ /**
+ * Protocol used for the connection.
+ *
+ * Supported values: HTTP, HTTPS, TCP, SSH
+ * @see Protocol
+ */
@Enumerated(EnumType.STRING)
@Column(length = 10, nullable = false)
private Protocol protocol;
+ /**
+ * Subdomain prefix for accessing this service.
+ *
+ * Example: "api" would make the service available at
+ * {@code api.yourdomain.com}
+ */
@Column(length = 50, nullable = false)
private String subdomain;
+ /**
+ * The tunnel this mapping is associated with.
+ *
+ * Each mapping belongs to exactly one tunnel.
+ * This is a lazy-loaded relationship to optimize performance.
+ *
+ * @see Tunnel
+ */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tunnel_id", nullable = false)
private Tunnel tunnel;
-}
+}
\ No newline at end of file
--
2.45.2
From a7e7ea4a39a9cb23aaa1af79b0aabf2b32565a5e Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:51:28 +0000
Subject: [PATCH 04/18] Hithomelabs/CFTunnels#114: Add JavaDoc to Protocol enum
---
.../CFTunnels/Entity/Protocol.java | 37 ++++++++++++++++++-
1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/Protocol.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/Protocol.java
index e927c47..0932524 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/Entity/Protocol.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/Protocol.java
@@ -1,8 +1,43 @@
package com.hithomelabs.CFTunnels.Entity;
+/**
+ * Enum representing supported protocols for Cloudflare Tunnel ingress mappings.
+ *
+ * Cloudflare Tunnel supports multiple protocols for routing traffic
+ * to your internal services. This enum defines the available options.
+ *
+ * Supported Protocols:
+ *
+ * - {@link #HTTP} - Standard HTTP protocol (port 80)
+ * - {@link #HTTPS} - Secure HTTP protocol (port 443)
+ * - {@link #TCP} - Raw TCP connections
+ * - {@link #SSH} - SSH protocol for remote access
+ *
+ *
+ * @see Mapping
+ */
public enum Protocol {
+ /**
+ * HTTP protocol for web services.
+ * Typically used with internal services running on port 80.
+ */
HTTP,
+
+ /**
+ * HTTPS protocol for secure web services.
+ * Use this for services with TLS/SSL certificates.
+ */
HTTPS,
+
+ /**
+ * Raw TCP protocol for non-HTTP services.
+ * Useful for databases, message queues, or custom protocols.
+ */
TCP,
+
+ /**
+ * SSH protocol for secure shell access.
+ * Enables secure remote access to servers.
+ */
SSH
-}
+}
\ No newline at end of file
--
2.45.2
From e8f158aeceff41b8678882ae75967d74053ca914 Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:51:39 +0000
Subject: [PATCH 05/18] Hithomelabs/CFTunnels#114: Add JavaDoc to Request
entity
---
.../hithomelabs/CFTunnels/Entity/Request.java | 77 ++++++++++++++++++-
1 file changed, 76 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/Request.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/Request.java
index 8d73de8..c66bcb4 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/Entity/Request.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/Request.java
@@ -8,6 +8,31 @@ import lombok.Setter;
import java.util.UUID;
+/**
+ * JPA Entity representing a mapping change request in the approval workflow.
+ *
+ * This entity tracks requests to add or modify tunnel ingress mappings.
+ * It implements an approval workflow where:
+ *
+ * - Users create requests (status: PENDING)
+ * - Approvers review and approve/reject (status: APPROVED/REJECTED)
+ *
+ *
+ * Database Table: {@code requests}
+ *
+ * Workflow:
+ *
+ * 1. User submits mapping request via REST API
+ * 2. Request is created with PENDING status
+ * 3. APPROVER role reviews the request
+ * 4. If approved: mapping is applied to Cloudflare tunnel
+ * 5. If rejected: request is marked REJECTED
+ *
+ *
+ * @see Mapping
+ * @see User
+ * @see RequestStatus
+ */
@Entity
@Getter
@Setter
@@ -16,30 +41,80 @@ import java.util.UUID;
@Table(name = "requests")
public class Request {
+ /**
+ * Unique identifier for this request (auto-generated UUID).
+ */
@Id
@GeneratedValue
@Column(columnDefinition = "uuid", unique = true, nullable = false)
private UUID id;
+ /**
+ * The mapping configuration being requested.
+ *
+ * This is a one-to-one relationship - each request
+ * contains exactly one mapping configuration.
+ *
+ * @see Mapping
+ */
@OneToOne
@JoinColumn(name = "mapping_id", unique = true, nullable = false)
private Mapping mapping;
+ /**
+ * User who created this request.
+ *
+ * This field is required and tracks accountability.
+ *
+ * @see User
+ */
@ManyToOne
@JoinColumn(name = "created_by", nullable = false)
private User createdBy;
+ /**
+ * User who approved or rejected this request.
+ *
+ * This is null until the request is processed.
+ *
+ * @see User
+ */
@ManyToOne
@JoinColumn(name = "accepted_by")
private User acceptedBy;
+ /**
+ * Status of the mapping request in the workflow.
+ *
+ * @see RequestStatus
+ */
public enum RequestStatus {
+ /**
+ * Request is waiting for approval.
+ */
PENDING,
+
+ /**
+ * Request has been approved.
+ * The mapping should now be applied to the tunnel.
+ */
APPROVED,
+
+ /**
+ * Request has been rejected.
+ * No changes will be made to the tunnel.
+ */
REJECTED
}
+ /**
+ * Current status of this request.
+ *
+ * Initially set to PENDING when created.
+ *
+ * @see RequestStatus
+ */
@Enumerated(EnumType.STRING)
@Column(length = 10, nullable = false)
private RequestStatus status;
-}
+}
\ No newline at end of file
--
2.45.2
From c6b466530fc82ed3df8876ea9121128c8efe7a3e Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:51:48 +0000
Subject: [PATCH 06/18] Hithomelabs/CFTunnels#114: Add JavaDoc to User entity
---
.../hithomelabs/CFTunnels/Entity/User.java | 40 ++++++++++++++++++-
1 file changed, 39 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/User.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/User.java
index 5783357..eb1336b 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/Entity/User.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/User.java
@@ -1,4 +1,5 @@
package com.hithomelabs.CFTunnels.Entity;
+
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
@@ -8,6 +9,26 @@ import lombok.Setter;
import java.util.UUID;
+/**
+ * JPA Entity representing a user in the system.
+ *
+ * This entity stores user information synced from the OIDC provider
+ * during authentication. Users are assigned roles that determine their
+ * access levels to the API endpoints.
+ *
+ * Database Table: {@code users}
+ *
+ * Roles:
+ *
+ * - USER - Basic access to view tunnels
+ * - DEVELOPER - Can create/modify mappings
+ * - APPROVER - Can approve/reject requests
+ * - ADMIN - Full access including tunnel configuration
+ *
+ *
+ * @see Request
+ * @see Mapping
+ */
@Entity
@Getter
@Setter
@@ -15,16 +36,33 @@ import java.util.UUID;
@AllArgsConstructor
@Table(name = "users")
public class User {
+
+ /**
+ * Unique identifier for the user (UUID).
+ *
+ * This corresponds to the user's ID in the OIDC provider.
+ */
@Id
@GeneratedValue
@Column(columnDefinition = "uuid", insertable = false, updatable = false, nullable = false)
private UUID id;
+ /**
+ * User's display name.
+ *
+ * This is typically the full name from the OIDC provider.
+ */
@Column(length = 50, nullable = false)
@Size(max = 50)
private String name;
+ /**
+ * User's email address.
+ *
+ * Used as the unique identifier for authentication
+ * and for associating users with their roles.
+ */
@Column(length = 50, nullable = false)
@Size(max = 50)
private String email;
-}
+}
\ No newline at end of file
--
2.45.2
From df7ea9a2df9c821a22f3bd520e3d92e73cfc4360 Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:52:08 +0000
Subject: [PATCH 07/18] Hithomelabs/CFTunnels#114: Add JavaDoc to
CloudflareAPIService
---
.../Services/CloudflareAPIService.java | 101 +++++++++++++++++-
1 file changed, 98 insertions(+), 3 deletions(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/Services/CloudflareAPIService.java b/src/main/java/com/hithomelabs/CFTunnels/Services/CloudflareAPIService.java
index cc7eaeb..6d0084a 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/Services/CloudflareAPIService.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/Services/CloudflareAPIService.java
@@ -22,21 +22,60 @@ import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.UUID;
+/**
+ * Service for interacting with the Cloudflare Tunnel API.
+ *
+ * This service provides methods to manage Cloudflare Tunnels,
+ * including fetching tunnel configurations, creating/updating tunnels,
+ * and adding ingress mappings. It handles all communication with the
+ * Cloudflare API using the configured credentials.
+ *
+ * API Endpoints Used:
+ *
+ * - GET /accounts/{accountId}/cfd_tunnel - List all tunnels
+ * - GET /accounts/{accountId}/cfd_tunnel/{tunnelId}/configurations - Get tunnel config
+ * - PUT /accounts/{accountId}/cfd_tunnel/{tunnelId}/configurations - Update tunnel config
+ *
+ *
+ * @see CloudflareConfig
+ * @see Tunnel
+ * @see Ingress
+ */
@Service
public class CloudflareAPIService {
+ /**
+ * Configuration for Cloudflare API credentials and settings.
+ * Loaded from application.properties using the {@code cloudflare.*} prefix.
+ */
@Autowired
CloudflareConfig cloudflareConfig;
+ /**
+ * Header provider for Cloudflare API authentication.
+ * Generates the X-Auth-Key and X-Auth-Email headers.
+ */
@Autowired
AuthKeyEmailHeader authKeyEmailHeader;
+ /**
+ * HTTP client for making API requests.
+ */
@Autowired
RestTemplate restTemplate;
+ /**
+ * Repository for storing tunnel configurations locally.
+ */
@Autowired
TunnelRepository tunnelRepository;
+ /**
+ * Fetches all Cloudflare tunnels from the API.
+ *
+ * @return Response containing the list of tunnels from Cloudflare
+ * @see TunnelsResponse
+ */
public ResponseEntity getCloudflareTunnels() {
String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel";
@@ -46,9 +85,18 @@ public class CloudflareAPIService {
return responseEntity;
}
+ /**
+ * Fetches the configuration for a specific tunnel.
+ *
+ * @param tunnelId The Cloudflare tunnel ID (UUID string)
+ * @param restTemplate HTTP client to use for the request
+ * @param responseType Class to deserialize the response into
+ * @param Response type
+ * @return Response containing the tunnel configuration
+ */
public ResponseEntity getCloudflareTunnelConfigurations(String tunnelId, RestTemplate restTemplate, Class responseType) {
- // * * Resource URL to hit get request at
+ // Resource URL to hit get request at
String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel/" + tunnelId + "/configurations";
HttpEntity httpEntity = new HttpEntity<>("",authKeyEmailHeader.getHttpHeaders());
@@ -56,9 +104,22 @@ public class CloudflareAPIService {
return responseEntity;
}
+ /**
+ * Updates the configuration for a specific tunnel.
+ *
+ * This method is used to add, modify, or remove ingress mappings
+ * by providing a complete configuration object.
+ *
+ * @param tunnelId The Cloudflare tunnel ID (UUID string)
+ * @param restTemplate HTTP client to use for the request
+ * @param responseType Class to deserialize the response into
+ * @param config The new configuration to apply
+ * @param Response type
+ * @return Response containing the updated tunnel configuration
+ */
public ResponseEntity putCloudflareTunnelConfigurations(String tunnelId, RestTemplate restTemplate, Class responseType, Config config) {
- // * * Resource URL to hit get request at
+ // Resource URL to hit get request at
String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel/" + tunnelId + "/configurations";
HttpHeaders httpHeaders = authKeyEmailHeader.getHttpHeaders();
@@ -68,10 +129,29 @@ public class CloudflareAPIService {
return responseEntity;
}
+ /**
+ * Converts a Cloudflare API tunnel result to a local Tunnel entity.
+ *
+ * @param tunnelResult The tunnel result from Cloudflare API
+ * @param env The environment name to associate
+ * @return New Tunnel entity instance
+ */
private Tunnel getTunnelFromTunnelResponse(TunnelResult tunnelResult, String env){
return new Tunnel(UUID.fromString(tunnelResult.getId()), env, tunnelResult.getName());
}
+ /**
+ * Creates or updates a tunnel's local configuration.
+ *
+ * This method fetches the tunnel from Cloudflare API, validates it exists,
+ * and stores a local copy with the environment association.
+ *
+ * @param tunnelId The Cloudflare tunnel ID
+ * @param environment Environment name (e.g., "production", "staging")
+ * @return The created or updated Tunnel entity
+ * @throws ExternalServiceException if Cloudflare API returns an error
+ * @throws NoSuchElementException if the tunnel doesn't exist in Cloudflare
+ */
public Tunnel createOrUpdateTunnel(String tunnelId, String environment) throws ExternalServiceException, NoSuchElementException {
ResponseEntity responseEntity = getCloudflareTunnels();
@@ -92,10 +172,25 @@ public class CloudflareAPIService {
return toBeConfigured;
}
+ /**
+ * Retrieves all tunnels that have been locally configured.
+ *
+ * @return List of locally configured tunnels
+ */
public List getAllConfiguredTunnels() {
return tunnelRepository.findAll();
}
+ /**
+ * Adds an ingress mapping to an existing tunnel.
+ *
+ * The new ingress is inserted at the second-to-last position in the
+ * ingress list (Cloudflare requires a catch-all rule at the end).
+ *
+ * @param tunnelId The Cloudflare tunnel ID
+ * @param ingress The ingress configuration to add
+ * @return Response with the updated tunnel configuration
+ */
public ResponseEntity addTunnelIngress(String tunnelId, Ingress ingress) {
ResponseEntity currentConfig = getCloudflareTunnelConfigurations(tunnelId, restTemplate, TunnelResponse.class);
@@ -105,4 +200,4 @@ public class CloudflareAPIService {
return putCloudflareTunnelConfigurations(tunnelId, restTemplate, TunnelResponse.class, config);
}
-}
+}
\ No newline at end of file
--
2.45.2
From 4197e645cdb205f4543a60a8ae254a960cfb9d73 Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:52:21 +0000
Subject: [PATCH 08/18] Hithomelabs/CFTunnels#114: Add JavaDoc to Ingress model
---
.../hithomelabs/CFTunnels/Models/Ingress.java | 71 ++++++++++++++++++-
1 file changed, 70 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/Models/Ingress.java b/src/main/java/com/hithomelabs/CFTunnels/Models/Ingress.java
index 005a02b..05d22ab 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/Models/Ingress.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/Models/Ingress.java
@@ -8,19 +8,88 @@ import lombok.Setter;
import java.util.List;
import java.util.Map;
+/**
+ * Model representing an ingress rule for a Cloudflare Tunnel.
+ *
+ * Ingress rules define how incoming requests should be routed through
+ * the tunnel to your internal services. Each rule specifies:
+ *
+ * - {@link #hostname} - The domain/subdomain to match
+ * - {@link #service} - The internal service URL
+ * - {@link #originRequest} - Optional settings for origin requests
+ * - {@link #path} - Optional path prefix to match
+ *
+ *
+ * Example JSON:
+ *
+ * {
+ * "hostname": "api.example.com",
+ * "service": "http://localhost:8080",
+ * "originRequest": {
+ * "noTLSVerify": true
+ * },
+ * "path": "/api"
+ * }
+ *
+ *
+ * @see Cloudflare Ingress Docs
+ */
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Ingress {
+ /**
+ * The target service URL.
+ *
+ * Format: {@code protocol://host:port}
+ * Example: {@code http://localhost:8080}
+ */
private String service;
+
+ /**
+ * Hostname pattern to match for this ingress rule.
+ *
+ * Can be a full domain (api.example.com) or use wildcards
+ * (*.example.com) to match subdomains.
+ * If null, this rule acts as a catch-all.
+ */
private String hostname;
+
+ /**
+ * Optional settings for requests to the origin server.
+ *
+ * Supported options:
+ *
+ * - noTLSVerify - Skip TLS verification
+ * - connectTimeout - Connection timeout in seconds
+ * - tlsTimeout - TLS handshake timeout
+ * - httpHostHeader - Host header to send
+ *
+ */
private Map originRequest;
+
+ /**
+ * Optional path to match before routing.
+ *
+ * Example: "/api" would only route requests with
+ * paths starting with /api to this service.
+ */
private String path;
+ /**
+ * Removes an ingress rule by hostname from a list.
+ *
+ * This utility method finds and removes the first ingress
+ * matching the given hostname.
+ *
+ * @param ingressList List of ingress rules to modify
+ * @param toBeDeleted Hostname of the rule to remove
+ * @return true if an ingress was removed, false otherwise
+ */
public static boolean deleteByHostName(List ingressList, String toBeDeleted){
return ingressList.removeIf(ingress -> ingress.getHostname() != null && ingress.getHostname().equals(toBeDeleted));
}
-}
+}
\ No newline at end of file
--
2.45.2
From 7e5d20e8acf4b626e66392d4899a3f07bcc48348 Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Tue, 14 Apr 2026 11:52:50 +0000
Subject: [PATCH 09/18] Hithomelabs/CFTunnels#114: Add JavaDoc and API
documentation to TunnelController
---
.../Controllers/TunnelController.java | 229 +++++++++++++++++-
1 file changed, 217 insertions(+), 12 deletions(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java b/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java
index 33c4d07..cb77525 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java
@@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.dao.DataAccessException;
import org.springframework.http.*;
-import org.springframework.http.*;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -36,6 +35,41 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
+/**
+ * REST Controller for managing Cloudflare Tunnels.
+ *
+ * This controller provides the public API for managing Cloudflare Tunnels
+ * and their ingress mappings. All endpoints require authentication via OIDC
+ * and are protected by role-based access control.
+ *
+ * Base URL: {@code /cloudflare}
+ *
+ * Authentication: OIDC-based with role-based access
+ *
+ * Available Roles:
+ *
+ * - USER - View tunnels and requests
+ * - DEVELOPER - Create/modify/delete mappings
+ * - APPROVER - Approve/reject requests
+ * - ADMIN - Full tunnel configuration access
+ *
+ *
+ * Example Usage:
+ *
+ * # Get all tunnels (requires USER role)
+ * curl -H "Authorization: Bearer <token>" \
+ * https://api.example.com/cloudflare/tunnels
+ *
+ * # Add a mapping (requires ADMIN role)
+ * curl -X POST -H "Authorization: Bearer <token>" \
+ * -H "Content-Type: application/json" \
+ * -d '{"hostname":"api.example.com","service":"http://localhost:8080"}' \
+ * https://api.example.com/cloudflare/tunnels/{tunnelId}/mappings
+ *
+ *
+ * @see CloudflareAPIService
+ * @see MappingRequestService
+ */
@RestController
@RequestMapping("/cloudflare")
public class TunnelController implements ErrorController {
@@ -63,9 +97,23 @@ public class TunnelController implements ErrorController {
@Autowired
private UserRepository userRepository;
+ /**
+ * Current environment (loaded from spring.profiles.active).
+ */
@Value("${spring.profiles.active}")
private String environment;
+ /**
+ * Get current user information.
+ *
+ * Returns the authenticated user's username and roles.
+ *
+ * @param oidcUser The authenticated OIDC user
+ * @return Map containing username and roles
+ *
+ * @security Requires USER role
+ * @response 200 OK
+ */
@PreAuthorize("hasAnyRole('USER')")
@GetMapping("/whoami")
public Map whoAmI(@AuthenticationPrincipal OidcUser oidcUser) {
@@ -79,6 +127,19 @@ public class TunnelController implements ErrorController {
);
}
+ /**
+ * Get all tunnels from Cloudflare API.
+ *
+ * Fetches the complete list of tunnels from Cloudflare,
+ * including their status and configuration from the Cloudflare API.
+ *
+ * @return Map containing list of all tunnels
+ *
+ * @security Requires USER role
+ * @response 200 OK with tunnel list
+ * @response 500 Internal Server Error if API call fails
+ * @see Cloudflare API
+ */
@PreAuthorize("hasAnyRole('USER')")
@GetMapping("/tunnels")
@Operation( security = { @SecurityRequirement(name = "oidcAuth") } )
@@ -92,6 +153,19 @@ public class TunnelController implements ErrorController {
return ResponseEntity.ok(jsonResponse);
}
+ /**
+ * Get locally configured tunnels.
+ *
+ * Returns the tunnels that have been configured locally
+ * with environment associations.
+ *
+ * @return Map containing list of configured tunnels
+ *
+ * @security Requires USER role
+ * @response 200 OK with tunnel list
+ * @response 500 Internal Server Error if database access fails
+ * @see CloudflareAPIService#getAllConfiguredTunnels()
+ */
@PreAuthorize("hasAnyRole('USER')")
@GetMapping("/configured/tunnels")
public ResponseEntity
*
* @return Map containing list of all tunnels
- *
- * @security Requires USER role
- * @response 200 OK with tunnel list
- * @response 500 Internal Server Error if API call fails
+ * @throws SecurityException if user lacks required role
* @see Cloudflare API
*/
@PreAuthorize("hasAnyRole('USER')")
@@ -160,10 +155,7 @@ public class TunnelController implements ErrorController {
* with environment associations.
*
* @return Map containing list of configured tunnels
- *
- * @security Requires USER role
- * @response 200 OK with tunnel list
- * @response 500 Internal Server Error if database access fails
+ * @throws SecurityException if user lacks required role
* @see CloudflareAPIService#getAllConfiguredTunnels()
*/
@PreAuthorize("hasAnyRole('USER')")
@@ -186,10 +178,7 @@ public class TunnelController implements ErrorController {
* Returns all pending, approved, and rejected mapping requests.
*
* @return Map containing list of all requests
- *
- * @security Requires USER role
- * @response 200 OK with request list
- * @response 500 Internal Server Error if database access fails
+ * @throws SecurityException if user lacks required role
*/
@PreAuthorize("hasAnyRole('USER')")
@GetMapping("/requests")
@@ -213,10 +202,7 @@ public class TunnelController implements ErrorController {
*
* @param tunnelId The Cloudflare tunnel ID (UUID)
* @return Map containing tunnel configuration
- *
- * @security Requires DEVELOPER role
- * @response 200 OK with configuration
- * @response 500 Internal Server Error
+ * @throws SecurityException if user lacks required role
* @see Cloudflare API
*/
@PreAuthorize("hasAnyRole('DEVELOPER')")
@@ -241,11 +227,8 @@ public class TunnelController implements ErrorController {
* @param tunnelId The Cloudflare tunnel ID (UUID)
* @param ingress The ingress rule to add
* @return Map containing the updated configuration
- *
- * @security Requires ADMIN role
- * @response 200 OK with updated configuration
- * @response 400 Bad Request if ingress is invalid
- * @response 500 Internal Server Error
+ * @throws SecurityException if user lacks required role
+ * @throws JsonProcessingException if JSON processing fails
*
* @example
* {
@@ -284,10 +267,8 @@ public class TunnelController implements ErrorController {
* @param tunnelId The Cloudflare tunnel ID (UUID)
* @param ingress Ingress containing hostname to delete (only hostname field is used)
* @return Map containing the result
- *
- * @security Requires DEVELOPER role
- * @response 200 OK with updated configuration
- * @response 409 Conflict if hostname not found
+ * @throws SecurityException if user lacks required role
+ * @throws JsonProcessingException if JSON processing fails
*/
@PreAuthorize("hasAnyRole('DEVELOPER')")
@DeleteMapping("/tunnels/{tunnelId}/mappings")
@@ -329,11 +310,7 @@ public class TunnelController implements ErrorController {
* @param oidcUser The authenticated user
* @param ingess The ingress configuration to request
* @return The created request with PENDING status
- *
- * @security Requires DEVELOPER role
- * @response 201 Created with request
- * @response 400 Bad Request if invalid
- *
+ * @throws SecurityException if user lacks required role
* @see MappingRequestService#createMappingRequest(String, Ingress, OidcUser)
*/
@PreAuthorize("hasAnyRole('DEVELOPER')")
@@ -354,11 +331,7 @@ public class TunnelController implements ErrorController {
* @param requestId The ID of the request to approve
* @param oidcUser The approver (must have APPROVER role)
* @return The updated request with APPROVED status
- *
- * @security Requires APPROVER role
- * @response 200 OK with approved request
- * @response 404 Not Found if request doesn't exist
- * @response 409 Conflict if request already processed
+ * @throws SecurityException if user lacks required role
*/
@PreAuthorize("hasAnyRole('APPROVER')")
@PutMapping("/requests/{requestId}/approve")
@@ -386,11 +359,7 @@ public class TunnelController implements ErrorController {
* @param requestId The ID of the request to reject
* @param oidcUser The rejecter (must have APPROVER role)
* @return The updated request with REJECTED status
- *
- * @security Requires APPROVER role
- * @response 200 OK with rejected request
- * @response 404 Not Found
- * @response 409 Conflict
+ * @throws SecurityException if user lacks required role
*/
@PreAuthorize("hasAnyRole('APPROVER')")
@PutMapping("/requests/{requestId}/reject")
@@ -425,12 +394,7 @@ public class TunnelController implements ErrorController {
* @param tunnelId The Cloudflare tunnel ID (UUID)
* @param user The authenticated user
* @return The tunnel configuration
- *
- * @security Requires ADMIN role
- * @response 200 OK with tunnel
- * @response 204 No Content
- * @response 404 Not Found
- * @response 500 Internal Server Error
+ * @throws SecurityException if user lacks required role
*/
@PreAuthorize("hasAnyRole('ADMIN')")
@PutMapping("/tunnels/configure/{tunnelId}")
@@ -454,4 +418,4 @@ public class TunnelController implements ErrorController {
}
}
-}
\ No newline at end of file
+}
--
2.45.2
From 047433fe60d3bd3865d55a6163f55ff3fa25e2b5 Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Wed, 15 Apr 2026 18:44:51 +0000
Subject: [PATCH 14/18] Hithomelabs/CFTunnels#115: Add PR target branch note to
README
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index fd7050c..2a6518e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# CFTunnels - Cloudflare Tunnels Management API
+> **Note**: All pull requests should be raised against the `test` branch.
+
A Spring Boot REST API for managing Cloudflare Tunnels with ingress mappings and an approval workflow.
## Overview
@@ -181,4 +183,4 @@ Private - All rights reserved
- [Cloudflare Tunnel Documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)
- [Cloudflare API v4](https://api.cloudflare.com/)
-- [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/)
\ No newline at end of file
+- [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/)
--
2.45.2
From e4a51ed9f8cc965cd9ca55fa773784c73064b98b Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Sat, 18 Apr 2026 15:05:51 +0000
Subject: [PATCH 15/18] Fix build by using buildscript pattern instead of
plugin DSL
---
build.gradle | 23 +++++++++++++++++------
1 file changed, 17 insertions(+), 6 deletions(-)
diff --git a/build.gradle b/build.gradle
index 1034cb7..06c54f5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,9 +1,19 @@
-plugins {
- id 'java'
- id 'org.springframework.boot' version '3.4.5'
- id 'io.spring.dependency-management' version '1.1.7'
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath("org.springframework.boot:spring-boot-gradle-plugin:3.4.5")
+ }
}
+plugins {
+ id 'java'
+ id 'io.spring.dependency-management' version '1.1.7'
+}
+
+apply plugin: 'org.springframework.boot'
+
group = 'com.hithomelabs'
version = '0.0.1-SNAPSHOT'
@@ -45,9 +55,10 @@ repositories {
dependencies {
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.8.5'
implementation group: 'org.springframework.boot', name:'spring-boot-starter-oauth2-client', version: '3.5.5'
+ implementation 'org.springframework.boot:spring-boot-starter'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
- implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
@@ -59,4 +70,4 @@ dependencies {
tasks.named('test') {
useJUnitPlatform()
-}
+}
\ No newline at end of file
--
2.45.2
From 7fdfdacf500410b77f910ce93adf506742d187fa Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Sat, 18 Apr 2026 15:06:11 +0000
Subject: [PATCH 16/18] Fix by adding explicit import statements
---
.../java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java b/src/main/java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java
index 42bacd5..bd64397 100644
--- a/src/main/java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java
+++ b/src/main/java/com/hithomelabs/CFTunnels/CfTunnelsApplication.java
@@ -1,5 +1,8 @@
package com.hithomelabs.CFTunnels;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
/**
* Main Spring Boot application class for Cloudflare Tunnels API.
*
@@ -42,4 +45,4 @@ public class CfTunnelsApplication {
SpringApplication.run(CfTunnelsApplication.class, args);
}
-}
+}
\ No newline at end of file
--
2.45.2
From 6304c695772764f12e6088c3daaf8a2519e39cdd Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Sat, 18 Apr 2026 15:06:25 +0000
Subject: [PATCH 17/18] Fix ENV variable with default value
---
src/main/resources/application.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 283d942..97344a4 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -2,7 +2,7 @@ spring.application.name=CFTunnels
cloudflare.accountId=${CLOUDFLARE_ACCOUNT_ID}
cloudflare.apiKey=${CLOUDFLARE_API_KEY}
cloudflare.email=${CLOUDFLARE_EMAIL}
-spring.profiles.active=${ENV}
+spring.profiles.active=${ENV:default}
/ * * Masking sure app works behind a reverse proxy
server.forward-headers-strategy=framework
--
2.45.2
From 29b767d634343a99e62a21592966df236a39a9d0 Mon Sep 17 00:00:00 2001
From: Dave the Dev
Date: Sat, 18 Apr 2026 15:29:40 +0000
Subject: [PATCH 18/18] Hithomelabs/CFTunnels#114: Fix properties parser
blocking issue
Fixed the malformed comment in application.properties:
- Changed "/ * * Masking" to "# Making" to create a valid comment
- This was causing the properties parser to fail
---
src/main/resources/application.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 97344a4..eb4ab8b 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -4,7 +4,7 @@ cloudflare.apiKey=${CLOUDFLARE_API_KEY}
cloudflare.email=${CLOUDFLARE_EMAIL}
spring.profiles.active=${ENV:default}
-/ * * Masking sure app works behind a reverse proxy
+# Making sure app works behind a reverse proxy
server.forward-headers-strategy=framework
spring.security.oauth2.client.registration.cftunnels.client-id=${OAUTH_CLIENT_ID}
--
2.45.2