From ffe151b59c5d1b27a3d4bb780591c9a9a10d3670 Mon Sep 17 00:00:00 2001 From: Kruti Shah Date: Wed, 14 Jan 2026 17:26:26 +0000 Subject: [PATCH] ISSUE-33 (#96) ## Description - db integration Co-authored-by: hitanshu310 Co-authored-by: = <=> Co-authored-by: Kruti Shah Reviewed-on: https://gitea.hithomelabs.com/Hithomelabs/CFTunnels/pulls/96 Co-authored-by: Kruti Shah Co-committed-by: Kruti Shah --- build.gradle | 1 + .../Controllers/TunnelController.java | 15 ++++ .../hithomelabs/CFTunnels/Entity/Mapping.java | 34 +++++++++ .../hithomelabs/CFTunnels/Entity/Request.java | 45 ++++++++++++ .../hithomelabs/CFTunnels/Entity/Tunnel.java | 29 ++++++++ .../hithomelabs/CFTunnels/Entity/User.java | 30 ++++++++ .../Repositories/MappingRepository.java | 11 +++ .../Repositories/RequestRepository.java | 11 +++ .../Repositories/TunnelRepository.java | 13 ++++ .../Repositories/UserRepository.java | 14 ++++ .../Services/MappingRequestService.java | 71 +++++++++++++++++++ .../resources/application-test.properties | 8 ++- src/main/resources/schema.sql | 56 +++++++-------- .../Controllers/TunnelControllerTest.java | 4 ++ 14 files changed, 309 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Entity/Mapping.java create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Entity/Request.java create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Entity/Tunnel.java create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Entity/User.java create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Repositories/MappingRepository.java create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Repositories/RequestRepository.java create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Repositories/TunnelRepository.java create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Repositories/UserRepository.java create mode 100644 src/main/java/com/hithomelabs/CFTunnels/Services/MappingRequestService.java diff --git a/build.gradle b/build.gradle index 0800341..90dc143 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'org.postgresql:postgresql' implementation 'org.hibernate.validator:hibernate-validator' + runtimeOnly 'com.h2database:h2' } tasks.named('test') { diff --git a/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java b/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java index 3362a83..53fb4f9 100644 --- a/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java +++ b/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java @@ -4,11 +4,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.hithomelabs.CFTunnels.Config.AuthoritiesToGroupMapping; import com.hithomelabs.CFTunnels.Config.CloudflareConfig; import com.hithomelabs.CFTunnels.Config.RestTemplateConfig; +import com.hithomelabs.CFTunnels.Entity.Request; import com.hithomelabs.CFTunnels.Headers.AuthKeyEmailHeader; import com.hithomelabs.CFTunnels.Models.Config; import com.hithomelabs.CFTunnels.Models.Ingress; import com.hithomelabs.CFTunnels.Models.TunnelResponse; import com.hithomelabs.CFTunnels.Services.CloudflareAPIService; +import com.hithomelabs.CFTunnels.Services.MappingRequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.http.*; @@ -44,6 +46,9 @@ public class TunnelController implements ErrorController { @Autowired CloudflareAPIService cloudflareAPIService; + @Autowired + MappingRequestService mappingRequestService; + @PreAuthorize("hasAnyRole('USER')") @GetMapping("/whoami") public Map whoAmI(@AuthenticationPrincipal OidcUser oidcUser) { @@ -132,4 +137,14 @@ public class TunnelController implements ErrorController { return ResponseEntity.ok(jsonResponse); } + + @PreAuthorize("hasAnyRole('DEVELOPER')") + @PutMapping("/tunnel/{tunnelId}/request") + public ResponseEntity createTunnelMappingRequest(@PathVariable String tunnelId, @AuthenticationPrincipal OidcUser oidcUser, @RequestBody Ingress ingess){ + Request request = mappingRequestService.createMappingRequest(tunnelId, ingess, oidcUser); + if(request.getId() != null) + return ResponseEntity.status(HttpStatus.CREATED).body(request); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + } diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/Mapping.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/Mapping.java new file mode 100644 index 0000000..0081205 --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/Mapping.java @@ -0,0 +1,34 @@ +package com.hithomelabs.CFTunnels.Entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + + +import java.util.UUID; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "mappings") +public class Mapping { + + @Id + @GeneratedValue + @Column(columnDefinition = "uuid", nullable = false, unique = true) + private UUID id; + + @Column(nullable = false) + private int port; + + @Column(length = 50, nullable = false) + private String subdomain; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tunnel_id", nullable = false) + private Tunnel tunnel; +} diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/Request.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/Request.java new file mode 100644 index 0000000..8d73de8 --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/Request.java @@ -0,0 +1,45 @@ +package com.hithomelabs.CFTunnels.Entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "requests") +public class Request { + + @Id + @GeneratedValue + @Column(columnDefinition = "uuid", unique = true, nullable = false) + private UUID id; + + @OneToOne + @JoinColumn(name = "mapping_id", unique = true, nullable = false) + private Mapping mapping; + + @ManyToOne + @JoinColumn(name = "created_by", nullable = false) + private User createdBy; + + @ManyToOne + @JoinColumn(name = "accepted_by") + private User acceptedBy; + + public enum RequestStatus { + PENDING, + APPROVED, + REJECTED + } + + @Enumerated(EnumType.STRING) + @Column(length = 10, nullable = false) + private RequestStatus status; +} diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/Tunnel.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/Tunnel.java new file mode 100644 index 0000000..9c56663 --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/Tunnel.java @@ -0,0 +1,29 @@ +package com.hithomelabs.CFTunnels.Entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Table(name="tunnels") +public class Tunnel { + + @Id + @GeneratedValue + @Column(columnDefinition = "uuid", insertable = false, updatable = false, nullable = false) + private UUID id; + + @Column(length = 10, unique = true, nullable = false) + private String environment; + + @Column(name = "cf_tunnel_id", columnDefinition = "uuid", unique = true, nullable = false) + private UUID cfTunnelId; +} diff --git a/src/main/java/com/hithomelabs/CFTunnels/Entity/User.java b/src/main/java/com/hithomelabs/CFTunnels/Entity/User.java new file mode 100644 index 0000000..5783357 --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Entity/User.java @@ -0,0 +1,30 @@ +package com.hithomelabs.CFTunnels.Entity; +import jakarta.persistence.*; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "users") +public class User { + @Id + @GeneratedValue + @Column(columnDefinition = "uuid", insertable = false, updatable = false, nullable = false) + private UUID id; + + @Column(length = 50, nullable = false) + @Size(max = 50) + private String name; + + @Column(length = 50, nullable = false) + @Size(max = 50) + private String email; +} diff --git a/src/main/java/com/hithomelabs/CFTunnels/Repositories/MappingRepository.java b/src/main/java/com/hithomelabs/CFTunnels/Repositories/MappingRepository.java new file mode 100644 index 0000000..73b469c --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Repositories/MappingRepository.java @@ -0,0 +1,11 @@ +package com.hithomelabs.CFTunnels.Repositories; + +import com.hithomelabs.CFTunnels.Entity.Mapping; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface MappingRepository extends JpaRepository { +} diff --git a/src/main/java/com/hithomelabs/CFTunnels/Repositories/RequestRepository.java b/src/main/java/com/hithomelabs/CFTunnels/Repositories/RequestRepository.java new file mode 100644 index 0000000..fe79625 --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Repositories/RequestRepository.java @@ -0,0 +1,11 @@ +package com.hithomelabs.CFTunnels.Repositories; + +import com.hithomelabs.CFTunnels.Entity.Request; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface RequestRepository extends JpaRepository { +} diff --git a/src/main/java/com/hithomelabs/CFTunnels/Repositories/TunnelRepository.java b/src/main/java/com/hithomelabs/CFTunnels/Repositories/TunnelRepository.java new file mode 100644 index 0000000..178f44e --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Repositories/TunnelRepository.java @@ -0,0 +1,13 @@ +package com.hithomelabs.CFTunnels.Repositories; + +import com.hithomelabs.CFTunnels.Entity.Tunnel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface TunnelRepository extends JpaRepository { + Optional findByCfTunnelId(UUID cfTunnelId); +} diff --git a/src/main/java/com/hithomelabs/CFTunnels/Repositories/UserRepository.java b/src/main/java/com/hithomelabs/CFTunnels/Repositories/UserRepository.java new file mode 100644 index 0000000..a1038a7 --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Repositories/UserRepository.java @@ -0,0 +1,14 @@ +package com.hithomelabs.CFTunnels.Repositories; + +import com.hithomelabs.CFTunnels.Entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface UserRepository extends JpaRepository { + + Optional findByEmail(String email); +} diff --git a/src/main/java/com/hithomelabs/CFTunnels/Services/MappingRequestService.java b/src/main/java/com/hithomelabs/CFTunnels/Services/MappingRequestService.java new file mode 100644 index 0000000..d026f65 --- /dev/null +++ b/src/main/java/com/hithomelabs/CFTunnels/Services/MappingRequestService.java @@ -0,0 +1,71 @@ +package com.hithomelabs.CFTunnels.Services; + +import com.hithomelabs.CFTunnels.Entity.Mapping; +import com.hithomelabs.CFTunnels.Entity.Request; +import com.hithomelabs.CFTunnels.Entity.Tunnel; +import com.hithomelabs.CFTunnels.Entity.User; +import com.hithomelabs.CFTunnels.Models.Ingress; +import com.hithomelabs.CFTunnels.Repositories.MappingRepository; +import com.hithomelabs.CFTunnels.Repositories.RequestRepository; +import com.hithomelabs.CFTunnels.Repositories.TunnelRepository; +import com.hithomelabs.CFTunnels.Repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.UUID; + +@Service +public class MappingRequestService { + + @Autowired + UserRepository userRepository; + + @Autowired + MappingRepository mappingRepository; + + @Autowired + RequestRepository requestRepository; + + @Autowired + TunnelRepository tunnelRepository; + + public Mapping createMapping(UUID tunnelId, Ingress ingress){ + Tunnel tunnel = tunnelRepository.findByCfTunnelId(tunnelId).orElseThrow(() -> new RuntimeException("Tunnel not found")); + Mapping mapping = createMappingFromTunnelIngress(tunnel, ingress); + return mappingRepository.save(mapping); + } + + public Request createRequest(Mapping mapping, User user){ + Request request = new Request(); + request.setMapping(mapping); + request.setCreatedBy(user); + request.setStatus(Request.RequestStatus.PENDING); + return requestRepository.save(request); + } + + public Request createMappingRequest(String tunnelId, Ingress ingress, OidcUser oidcUser){ + User user = userRepository.findByEmail(oidcUser.getEmail()).orElseGet(()-> mapUser(oidcUser)); + Mapping mapping = createMapping(UUID.fromString(tunnelId), ingress); + return createRequest(mapping, user); + } + + public User mapUser(OidcUser oidcUser){ + String email = oidcUser.getEmail(); + String name = oidcUser.getNickName(); + User user = new User(); + user.setEmail(email); + user.setName(name); + userRepository.save(user); + return user; + } + + public Mapping createMappingFromTunnelIngress(Tunnel tunnel, Ingress ingress){ + Mapping mapping = new Mapping(); + mapping.setTunnel(tunnel); + mapping.setPort(Integer.parseInt(ingress.getService().split(":")[2])); + mapping.setSubdomain(ingress.getHostname().split("\\.")[0]); + return mapping; + } +} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index e5c014b..a0ccff7 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -1 +1,7 @@ -api.baseUrl=https://testcf.hithomelabs.com \ No newline at end of file +api.baseUrl=https://testcf.hithomelabs.com + +spring.datasource.url: jdbc:h2:mem:testdb +spring.datasource.driver-class-name: org.h2.Driver +spring.datasource.username: sa +spring.datasource.password: +spring.datasource.jpa.hibernate.ddl-auto: none \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 1fa876d..368b406 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,37 +1,29 @@ --- schema.sql +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; --- Roles table -CREATE TABLE IF NOT EXISTS roles ( - role_id SERIAL PRIMARY KEY, - role_name VARCHAR(50) UNIQUE NOT NULL -); - --- Users table -CREATE TABLE IF NOT EXISTS users ( - user_id SERIAL PRIMARY KEY, - user_name VARCHAR(100) NOT NULL, - password VARCHAR(255) NOT NULL -); - --- User-Role Mapping table (many-to-many relationship) -CREATE TABLE IF NOT EXISTS user_role_mapping ( - mapping_id SERIAL PRIMARY KEY, - user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, - role_id INTEGER NOT NULL REFERENCES roles(role_id) ON DELETE CASCADE -); - --- Tunnels table CREATE TABLE IF NOT EXISTS tunnels ( - tunnel_id SERIAL PRIMARY KEY, - tunnel_name VARCHAR(100) NOT NULL, - tunnel_type VARCHAR(50) NOT NULL + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + environment VARCHAR(10) NOT NULL, + cf_tunnel_id UUID UNIQUE NOT NULL ); --- Mapping Requests table -CREATE TABLE IF NOT EXISTS mapping_requests ( - request_id SERIAL PRIMARY KEY, - request_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - status VARCHAR(20) NOT NULL, - user_id INTEGER REFERENCES users(user_id) ON DELETE SET NULL, - tunnel_id INTEGER REFERENCES tunnels(tunnel_id) ON DELETE SET NULL +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(50) NOT NULL ); + +CREATE TABLE IF NOT EXISTS mappings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tunnel_id UUID NOT NULL REFERENCES tunnels(id) ON DELETE CASCADE, + port INT NOT NULL, + subdomain VARCHAR(50) NOT NULL +-- UNIQUE (tunnel_id, port), +-- UNIQUE (tunnel_id, subdomain) +); + +CREATE TABLE IF NOT EXISTS requests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mapping_id UUID NOT NULL REFERENCES mappings(id) ON DELETE CASCADE, + created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + accepted_by UUID REFERENCES users(id) ON DELETE SET NULL, + status VARCHAR(20) NOT NULL CHECK (status IN ('PENDING', 'APPROVED', 'REJECTED')) +); \ No newline at end of file diff --git a/src/test/java/com/hithomelabs/CFTunnels/Controllers/TunnelControllerTest.java b/src/test/java/com/hithomelabs/CFTunnels/Controllers/TunnelControllerTest.java index 644288e..ff7d303 100644 --- a/src/test/java/com/hithomelabs/CFTunnels/Controllers/TunnelControllerTest.java +++ b/src/test/java/com/hithomelabs/CFTunnels/Controllers/TunnelControllerTest.java @@ -10,6 +10,7 @@ import com.hithomelabs.CFTunnels.Models.Config; import com.hithomelabs.CFTunnels.Models.Groups; import com.hithomelabs.CFTunnels.Models.TunnelResponse; import com.hithomelabs.CFTunnels.Services.CloudflareAPIService; +import com.hithomelabs.CFTunnels.Services.MappingRequestService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -66,6 +67,9 @@ class TunnelControllerTest { @MockitoBean RestTemplateConfig restTemplateConfig; + @MockitoBean + MappingRequestService mappingRequestService; + private static final String tunnelResponseSmallIngressFile = "tunnelResponseSmallIngress.json"; private static final String tunnelResponseLargeIngressFile = "tunnelResponseLargeIngress.json";