From 87c7990d891b5be2a5a5c94680dbf74de62c4eae Mon Sep 17 00:00:00 2001 From: Tiago da Silva Date: Fri, 17 Jul 2026 14:58:24 -0300 Subject: [PATCH] APPLICATION_COMMIT_1 --- .gitignore | 6 ++ README.md | 5 +- pom.xml | 29 ++++++++++ .../com/oracle/pdbportal/CryptoService.java | 36 ++++++++++++ .../oracle/pdbportal/OciPdbProvisioner.java | 57 +++++++++++++++++++ .../pdbportal/PdbPortalApplication.java | 15 +++++ .../java/com/oracle/pdbportal/PdbRequest.java | 39 +++++++++++++ .../pdbportal/PdbRequestController.java | 20 +++++++ .../pdbportal/PdbRequestRepository.java | 9 +++ .../oracle/pdbportal/PdbRequestService.java | 50 ++++++++++++++++ .../oracle/pdbportal/PortalProperties.java | 11 ++++ .../pdbportal/SecurityConfiguration.java | 26 +++++++++ src/main/resources/application.yml | 25 ++++++++ src/main/resources/static/app.js | 18 ++++++ src/main/resources/static/index.html | 7 +++ 15 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/com/oracle/pdbportal/CryptoService.java create mode 100644 src/main/java/com/oracle/pdbportal/OciPdbProvisioner.java create mode 100644 src/main/java/com/oracle/pdbportal/PdbPortalApplication.java create mode 100644 src/main/java/com/oracle/pdbportal/PdbRequest.java create mode 100644 src/main/java/com/oracle/pdbportal/PdbRequestController.java create mode 100644 src/main/java/com/oracle/pdbportal/PdbRequestRepository.java create mode 100644 src/main/java/com/oracle/pdbportal/PdbRequestService.java create mode 100644 src/main/java/com/oracle/pdbportal/PortalProperties.java create mode 100644 src/main/java/com/oracle/pdbportal/SecurityConfiguration.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/static/app.js create mode 100644 src/main/resources/static/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac76469 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +target/ +.idea/ +.vscode/ +*.iml +.env +*.log diff --git a/README.md b/README.md index 4f9ba7c..aaf90bd 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,14 @@ Set these as deployment secrets, not in source control: - `PDB_PORTAL_CDB_OCID`: the Container Database OCID (the `ocid1.database...` value). - `PDB_PORTAL_CDB_ADMIN_PASSWORD`: supplied at runtime by OCI Vault or the deployment secret manager. +- `PDB_PORTAL_TDE_WALLET_PASSWORD` (when required by the CDB). - `PDB_PORTAL_ENCRYPTION_KEY`: a Base64-encoded 256-bit AES key used to protect the submitted PDB password while approval is pending. -- `PDB_PORTAL_JDBC_URL_TEMPLATE`: e.g. `jdbc:oracle:thin:@//scan-host:1521/{pdbName}`. +- `PDB_PORTAL_JDBC_URL_TEMPLATE` (optional): override only if needed, e.g. `jdbc:oracle:thin:@//scan-host:1521/{pdbName}`. By default the portal reads the PDB connection string returned by OCI. When deployed on OCI, grant a dynamic group containing the application runtime permission to manage PDBs in the target compartment. The backend uses a Resource Principal; a personal OCI API key must only be used for local development, never embedded in the portal. +For a local OCI test, set `PDB_PORTAL_OCI_AUTH_MODE=api_key`. The SDK then reads your usual OCI config file (`%USERPROFILE%\\.oci\\config`, profile `DEFAULT`) and its `key_file` entry. You may instead set `PDB_PORTAL_OCI_CONFIG_PATH` and `PDB_PORTAL_OCI_PROFILE`. The private key stays outside this repository. + ## Run locally Install JDK 21 and Maven, then set the variables above (a non-production local key is acceptable only for development): diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..83ff299 --- /dev/null +++ b/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.8 + + + com.oracle.pdbportal + pdb-self-service-portal + 0.0.1-SNAPSHOT + + 21 + 3.89.1 + + + org.springframework.bootspring-boot-starter-web + org.springframework.bootspring-boot-starter-data-jpa + org.springframework.bootspring-boot-starter-security + com.oracle.oci.sdkoci-java-sdk-database${oci.sdk.version} + com.oracle.database.jdbcojdbc1123.8.0.25.04 + com.h2databaseh2runtime + org.springframework.bootspring-boot-starter-testtest + + + org.springframework.bootspring-boot-maven-plugin + + diff --git a/src/main/java/com/oracle/pdbportal/CryptoService.java b/src/main/java/com/oracle/pdbportal/CryptoService.java new file mode 100644 index 0000000..486d7d9 --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/CryptoService.java @@ -0,0 +1,36 @@ +package com.oracle.pdbportal; + +import org.springframework.stereotype.Service; +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +@Service +class CryptoService { + private final byte[] key; + CryptoService(PortalProperties properties) { + if (properties.encryptionKey() == null || properties.encryptionKey().isBlank()) + throw new IllegalStateException("PDB_PORTAL_ENCRYPTION_KEY is required"); + key = Base64.getDecoder().decode(properties.encryptionKey()); + if (key.length != 32) throw new IllegalStateException("PDB_PORTAL_ENCRYPTION_KEY must be a 256-bit Base64 key"); + } + String encrypt(String clearText) { return crypt(Cipher.ENCRYPT_MODE, clearText.getBytes(StandardCharsets.UTF_8), null); } + String decrypt(String payload) { return new String(Base64.getDecoder().decode(crypt(Cipher.DECRYPT_MODE, Base64.getDecoder().decode(payload), null)), StandardCharsets.UTF_8); } + private String crypt(int mode, byte[] input, byte[] ignored) { + try { + byte[] iv = new byte[12]; + if (mode == Cipher.ENCRYPT_MODE) new SecureRandom().nextBytes(iv); + else { iv = java.util.Arrays.copyOfRange(input, 0, 12); input = java.util.Arrays.copyOfRange(input, 12, input.length); } + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(mode, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv)); + byte[] value = cipher.doFinal(input); + if (mode == Cipher.DECRYPT_MODE) return Base64.getEncoder().encodeToString(value); + byte[] combined = new byte[iv.length + value.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(value, 0, combined, iv.length, value.length); + return Base64.getEncoder().encodeToString(combined); + } catch (Exception e) { throw new IllegalStateException("Could not protect a request secret", e); } + } +} diff --git a/src/main/java/com/oracle/pdbportal/OciPdbProvisioner.java b/src/main/java/com/oracle/pdbportal/OciPdbProvisioner.java new file mode 100644 index 0000000..163734d --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/OciPdbProvisioner.java @@ -0,0 +1,57 @@ +package com.oracle.pdbportal; + +import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.database.DatabaseClient; +import com.oracle.bmc.database.model.CreatePluggableDatabaseDetails; +import com.oracle.bmc.database.requests.CreatePluggableDatabaseRequest; +import com.oracle.bmc.database.requests.GetPluggableDatabaseRequest; +import org.springframework.stereotype.Service; +import java.io.IOException; +import java.util.UUID; + +@Service +class OciPdbProvisioner { + private final PortalProperties properties; + OciPdbProvisioner(PortalProperties properties) { this.properties = properties; } + ProvisioningResult create(String pdbName, String pdbAdminPassword) { + requireConfigured(); + try (var client = client()) { + var builder = CreatePluggableDatabaseDetails.builder() + .containerDatabaseId(properties.cdbOcid()).pdbName(pdbName).pdbAdminPassword(pdbAdminPassword) + .containerDatabaseAdminPassword(properties.cdbAdminPassword().toCharArray()) + .shouldPdbAdminAccountBeLocked(false).shouldCreatePdbBackup(false); + if (properties.tdeWalletPassword() != null && !properties.tdeWalletPassword().isBlank()) builder.tdeWalletPassword(properties.tdeWalletPassword()); + var details = builder.build(); + var response = client.createPluggableDatabase(CreatePluggableDatabaseRequest.builder() + .createPluggableDatabaseDetails(details).opcRetryToken(UUID.randomUUID().toString()).build()); + return new ProvisioningResult(response.getPluggableDatabase().getId(), response.getOpcWorkRequestId()); + } + } + boolean isAvailable(String pdbOcid) { + requireConfigured(); + try (var client = client()) { + return "AVAILABLE".equals(client.getPluggableDatabase(GetPluggableDatabaseRequest.builder() + .pluggableDatabaseId(pdbOcid).build()).getPluggableDatabase().getLifecycleState().toString()); + } + } + String pdbDefaultConnectionString(String pdbOcid) { + try (var client = client()) { + var pdb = client.getPluggableDatabase(GetPluggableDatabaseRequest.builder().pluggableDatabaseId(pdbOcid).build()).getPluggableDatabase(); + if (pdb.getConnectionStrings() == null || pdb.getConnectionStrings().getPdbDefault() == null) throw new IllegalStateException("OCI did not return a PDB default connection string"); + return pdb.getConnectionStrings().getPdbDefault(); + } + } + private void requireConfigured() { + if (properties.cdbOcid() == null || properties.cdbOcid().isBlank() || properties.cdbAdminPassword() == null || properties.cdbAdminPassword().isBlank()) + throw new IllegalStateException("CDB OCID and CDB admin password must be supplied by runtime secrets"); + } + private DatabaseClient client() { + if (!"api_key".equalsIgnoreCase(properties.ociAuthMode())) return DatabaseClient.builder().build(ResourcePrincipalAuthenticationDetailsProvider.builder().build()); + try { + if (properties.ociConfigPath() == null || properties.ociConfigPath().isBlank()) return DatabaseClient.builder().build(new ConfigFileAuthenticationDetailsProvider(properties.ociProfile())); + return DatabaseClient.builder().build(new ConfigFileAuthenticationDetailsProvider(properties.ociConfigPath(), properties.ociProfile())); + } catch (IOException e) { throw new IllegalStateException("OCI config file could not be loaded", e); } + } + record ProvisioningResult(String pdbOcid, String workRequestId) { } +} diff --git a/src/main/java/com/oracle/pdbportal/PdbPortalApplication.java b/src/main/java/com/oracle/pdbportal/PdbPortalApplication.java new file mode 100644 index 0000000..0cb7205 --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/PdbPortalApplication.java @@ -0,0 +1,15 @@ +package com.oracle.pdbportal; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +@EnableConfigurationProperties(PortalProperties.class) +public class PdbPortalApplication { + public static void main(String[] args) { + SpringApplication.run(PdbPortalApplication.class, args); + } +} diff --git a/src/main/java/com/oracle/pdbportal/PdbRequest.java b/src/main/java/com/oracle/pdbportal/PdbRequest.java new file mode 100644 index 0000000..0c76fd8 --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/PdbRequest.java @@ -0,0 +1,39 @@ +package com.oracle.pdbportal; + +import jakarta.persistence.*; +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "pdb_requests", uniqueConstraints = @UniqueConstraint(columnNames = "pdbName")) +public class PdbRequest { + @Id @GeneratedValue private UUID id; + @Column(nullable = false, length = 30) private String pdbName; + @Column(nullable = false) private int storageGb; + @Column(nullable = false) private String requestedBy; + @Column(nullable = false) @Enumerated(EnumType.STRING) private RequestStatus status; + @Lob @Column(nullable = false) private String encryptedPdbPassword; + private String approvedBy; + private String pdbOcid; + private String workRequestId; + @Lob private String failureReason; + @Column(nullable = false) private Instant createdAt = Instant.now(); + private Instant approvedAt; + + protected PdbRequest() { } + PdbRequest(String pdbName, int storageGb, String requestedBy, String encryptedPdbPassword) { + this.pdbName = pdbName; this.storageGb = storageGb; this.requestedBy = requestedBy; + this.encryptedPdbPassword = encryptedPdbPassword; this.status = RequestStatus.PENDING; + } + public UUID getId() { return id; } public String getPdbName() { return pdbName; } + public int getStorageGb() { return storageGb; } public String getRequestedBy() { return requestedBy; } + public RequestStatus getStatus() { return status; } public String getPdbOcid() { return pdbOcid; } + public String getWorkRequestId() { return workRequestId; } public String getFailureReason() { return failureReason; } + String password() { return encryptedPdbPassword; } + void approve(String user) { status = RequestStatus.APPROVED; approvedBy = user; approvedAt = Instant.now(); } + void provisioning(String pdbOcid, String workRequestId) { status = RequestStatus.PROVISIONING; this.pdbOcid = pdbOcid; this.workRequestId = workRequestId; } + void completed() { status = RequestStatus.COMPLETED; encryptedPdbPassword = null; } + void failed(String reason) { status = RequestStatus.FAILED; failureReason = reason; } +} + +enum RequestStatus { PENDING, APPROVED, PROVISIONING, COMPLETED, FAILED, REJECTED } diff --git a/src/main/java/com/oracle/pdbportal/PdbRequestController.java b/src/main/java/com/oracle/pdbportal/PdbRequestController.java new file mode 100644 index 0000000..ea69115 --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/PdbRequestController.java @@ -0,0 +1,20 @@ +package com.oracle.pdbportal; + +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; +import java.util.*; + +@RestController @RequestMapping("/api/requests") +class PdbRequestController { + private final PdbRequestService service; + PdbRequestController(PdbRequestService service) { this.service = service; } + @PostMapping @ResponseStatus(HttpStatus.CREATED) @PreAuthorize("hasRole('DEV')") + PdbRequest create(@RequestBody CreatePdbRequest request, Authentication authentication) { return service.submit(request, authentication.getName()); } + @GetMapping @PreAuthorize("hasAnyRole('DEV','ADMIN')") List list() { return service.list(); } + @PostMapping("/{id}/approve") @PreAuthorize("hasRole('ADMIN')") + PdbRequest approve(@PathVariable UUID id, Authentication authentication) { return service.approve(id, authentication.getName()); } + @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class}) @ResponseStatus(HttpStatus.BAD_REQUEST) + Map badRequest(Exception e) { return Map.of("error", e.getMessage()); } +} diff --git a/src/main/java/com/oracle/pdbportal/PdbRequestRepository.java b/src/main/java/com/oracle/pdbportal/PdbRequestRepository.java new file mode 100644 index 0000000..b235c04 --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/PdbRequestRepository.java @@ -0,0 +1,9 @@ +package com.oracle.pdbportal; + +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.*; + +interface PdbRequestRepository extends JpaRepository { + List findByStatus(RequestStatus status); + boolean existsByPdbNameIgnoreCase(String pdbName); +} diff --git a/src/main/java/com/oracle/pdbportal/PdbRequestService.java b/src/main/java/com/oracle/pdbportal/PdbRequestService.java new file mode 100644 index 0000000..45b75b8 --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/PdbRequestService.java @@ -0,0 +1,50 @@ +package com.oracle.pdbportal; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.sql.DriverManager; +import java.util.*; + +@Service +class PdbRequestService { + private final PdbRequestRepository repository; private final CryptoService crypto; + private final OciPdbProvisioner provisioner; private final PortalProperties properties; + PdbRequestService(PdbRequestRepository r, CryptoService c, OciPdbProvisioner p, PortalProperties props) { repository=r; crypto=c; provisioner=p; properties=props; } + @Transactional PdbRequest submit(CreatePdbRequest input, String user) { + String name = input.pdbName().trim().toUpperCase(Locale.ROOT); + if (!name.matches("[A-Z][A-Z0-9]{0,29}")) throw new IllegalArgumentException("PDB name must begin with a letter and contain at most 30 alphanumeric characters"); + if (repository.existsByPdbNameIgnoreCase(name)) throw new IllegalArgumentException("A request already exists for this PDB name"); + if (input.storageGb() < properties.minStorageGb() || input.storageGb() > properties.maxStorageGb()) throw new IllegalArgumentException("Storage request is outside the permitted range"); + if (!input.initialPassword().matches("(?=(?:.*[A-Z]){2})(?=(?:.*[a-z]){2})(?=(?:.*\\d){2})(?=(?:.*[_#-]){2}).{9,}")) throw new IllegalArgumentException("Initial password does not meet OCI PDB password rules"); + return repository.save(new PdbRequest(name, input.storageGb(), user, crypto.encrypt(input.initialPassword()))); + } + @Transactional PdbRequest approve(UUID id, String admin) { + PdbRequest request = find(id); if (request.getStatus()!=RequestStatus.PENDING) throw new IllegalStateException("Only pending requests can be approved"); + request.approve(admin); + try { var result = provisioner.create(request.getPdbName(), crypto.decrypt(request.password())); request.provisioning(result.pdbOcid(), result.workRequestId()); } + catch (RuntimeException e) { request.failed("OCI provisioning request could not be submitted"); throw e; } + return request; + } + @Scheduled(fixedDelayString = "${PDB_PORTAL_RECONCILE_MS:60000}") @Transactional + void reconcile() { + repository.findByStatus(RequestStatus.PROVISIONING).forEach(request -> { + try { if (provisioner.isAvailable(request.getPdbOcid())) createDataTablespace(request); } + catch (RuntimeException ignored) { /* OCI may still be processing; retry on the next cycle. */ } + }); + } + private void createDataTablespace(PdbRequest request) { + String serviceConnection = properties.jdbcUrlTemplate()==null || properties.jdbcUrlTemplate().isBlank() + ? provisioner.pdbDefaultConnectionString(request.getPdbOcid()) + : properties.jdbcUrlTemplate().replace("{pdbName}", request.getPdbName()); + String url = serviceConnection.startsWith("jdbc:") ? serviceConnection : "jdbc:oracle:thin:@" + serviceConnection; + try (var connection=DriverManager.getConnection(url, properties.pdbAdminUser(), crypto.decrypt(request.password())); var statement=connection.createStatement()) { + statement.execute("CREATE TABLESPACE DATA DATAFILE SIZE " + request.getStorageGb() + "G AUTOEXTEND OFF"); + request.completed(); + } catch (Exception e) { request.failed("Tablespace creation failed; inspect secure application logs"); } + } + List list() { return repository.findAll(); } + private PdbRequest find(UUID id) { return repository.findById(id).orElseThrow(() -> new NoSuchElementException("Request not found")); } +} + +record CreatePdbRequest(String pdbName, int storageGb, String initialPassword) { } diff --git a/src/main/java/com/oracle/pdbportal/PortalProperties.java b/src/main/java/com/oracle/pdbportal/PortalProperties.java new file mode 100644 index 0000000..7b360bd --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/PortalProperties.java @@ -0,0 +1,11 @@ +package com.oracle.pdbportal; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "pdb-portal") +public record PortalProperties(String cdbOcid, String cdbAdminPassword, String tdeWalletPassword, String encryptionKey, + String jdbcUrlTemplate, String pdbAdminUser, int minStorageGb, + int maxStorageGb, String ociAuthMode, String ociConfigPath, + String ociProfile, DevUsers devUsers) { + public record DevUsers(String developerPassword, String adminPassword) { } +} diff --git a/src/main/java/com/oracle/pdbportal/SecurityConfiguration.java b/src/main/java/com/oracle/pdbportal/SecurityConfiguration.java new file mode 100644 index 0000000..c79daff --- /dev/null +++ b/src/main/java/com/oracle/pdbportal/SecurityConfiguration.java @@ -0,0 +1,26 @@ +package com.oracle.pdbportal; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration @EnableMethodSecurity +class SecurityConfiguration { + @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Bean UserDetailsService users(PortalProperties p, PasswordEncoder encoder) { + return new InMemoryUserDetailsManager( + User.withUsername("developer").password(encoder.encode(p.devUsers().developerPassword())).roles("DEV").build(), + User.withUsername("admin").password(encoder.encode(p.devUsers().adminPassword())).roles("ADMIN").build()); + } + @Bean SecurityFilterChain security(HttpSecurity http) throws Exception { + return http.csrf(csrf -> csrf.disable()).authorizeHttpRequests(registry -> registry + .requestMatchers("/", "/index.html", "/app.js").permitAll().anyRequest().authenticated()).httpBasic(basic -> {}).build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..85577c7 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,25 @@ +spring: + datasource: + url: ${PDB_PORTAL_REPOSITORY_URL:jdbc:h2:mem:pdbportal;MODE=Oracle;DB_CLOSE_DELAY=-1} + username: ${PDB_PORTAL_REPOSITORY_USER:sa} + password: ${PDB_PORTAL_REPOSITORY_PASSWORD:} + jpa: + hibernate: + ddl-auto: update + open-in-view: false + +pdb-portal: + cdb-ocid: ${PDB_PORTAL_CDB_OCID:} + cdb-admin-password: ${PDB_PORTAL_CDB_ADMIN_PASSWORD:} + tde-wallet-password: ${PDB_PORTAL_TDE_WALLET_PASSWORD:} + encryption-key: ${PDB_PORTAL_ENCRYPTION_KEY:} + jdbc-url-template: ${PDB_PORTAL_JDBC_URL_TEMPLATE:} + pdb-admin-user: ${PDB_PORTAL_PDB_ADMIN_USER:PDBADMIN} + oci-auth-mode: ${PDB_PORTAL_OCI_AUTH_MODE:resource_principal} + oci-config-path: ${PDB_PORTAL_OCI_CONFIG_PATH:} + oci-profile: ${PDB_PORTAL_OCI_PROFILE:DEFAULT} + min-storage-gb: ${PDB_PORTAL_MIN_STORAGE_GB:10} + max-storage-gb: ${PDB_PORTAL_MAX_STORAGE_GB:200} + dev-users: + developer-password: ${PDB_PORTAL_DEV_PASSWORD:change-me} + admin-password: ${PDB_PORTAL_ADMIN_PASSWORD:change-me} diff --git a/src/main/resources/static/app.js b/src/main/resources/static/app.js new file mode 100644 index 0000000..305fa09 --- /dev/null +++ b/src/main/resources/static/app.js @@ -0,0 +1,18 @@ +const message = document.querySelector('#message'); +async function api(path, options = {}) { + const response = await fetch(path, {headers: {'Content-Type': 'application/json'}, ...options}); + if (!response.ok) throw new Error((await response.json().catch(() => ({}))).error || `HTTP ${response.status}`); + return response.json(); +} +async function refresh() { + try { + const items = await api('/api/requests'); + document.querySelector('#rows').innerHTML = items.map(item => `${item.pdbName}${item.storageGb}${item.requestedBy}${item.status}${item.status === 'PENDING' ? `` : ''}`).join(''); + } catch (error) { message.textContent = error.message; } +} +async function approve(id) { try { await api(`/api/requests/${id}/approve`, {method: 'POST'}); message.textContent = 'Provisionamento submetido à OCI.'; refresh(); } catch (error) { message.textContent = error.message; } } +document.querySelector('#request').addEventListener('submit', async event => { + event.preventDefault(); const data = Object.fromEntries(new FormData(event.target)); data.storageGb = Number(data.storageGb); + try { await api('/api/requests', {method: 'POST', body: JSON.stringify(data)}); event.target.reset(); message.textContent = 'Solicitação enviada para aprovação.'; refresh(); } catch (error) { message.textContent = error.message; } +}); +document.querySelector('#refresh').addEventListener('click', refresh); refresh(); diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..acd5e53 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,7 @@ + +Portal PDB + +

Solicitação de PDB

Autentique-se pelo navegador: developer para solicitar ou admin para aprovar.

+
+

Solicitações

PDBGBSolicitanteStatusAção
+