APPLICATION_COMMIT_1
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
target/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
.env
|
||||
*.log
|
||||
@@ -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):
|
||||
|
||||
29
pom.xml
Normal file
29
pom.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.8</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<groupId>com.oracle.pdbportal</groupId>
|
||||
<artifactId>pdb-self-service-portal</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<oci.sdk.version>3.89.1</oci.sdk.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
|
||||
<dependency><groupId>com.oracle.oci.sdk</groupId><artifactId>oci-java-sdk-database</artifactId><version>${oci.sdk.version}</version></dependency>
|
||||
<dependency><groupId>com.oracle.database.jdbc</groupId><artifactId>ojdbc11</artifactId><version>23.8.0.25.04</version></dependency>
|
||||
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
|
||||
</build>
|
||||
</project>
|
||||
36
src/main/java/com/oracle/pdbportal/CryptoService.java
Normal file
36
src/main/java/com/oracle/pdbportal/CryptoService.java
Normal file
@@ -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); }
|
||||
}
|
||||
}
|
||||
57
src/main/java/com/oracle/pdbportal/OciPdbProvisioner.java
Normal file
57
src/main/java/com/oracle/pdbportal/OciPdbProvisioner.java
Normal file
@@ -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) { }
|
||||
}
|
||||
15
src/main/java/com/oracle/pdbportal/PdbPortalApplication.java
Normal file
15
src/main/java/com/oracle/pdbportal/PdbPortalApplication.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
39
src/main/java/com/oracle/pdbportal/PdbRequest.java
Normal file
39
src/main/java/com/oracle/pdbportal/PdbRequest.java
Normal file
@@ -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 }
|
||||
20
src/main/java/com/oracle/pdbportal/PdbRequestController.java
Normal file
20
src/main/java/com/oracle/pdbportal/PdbRequestController.java
Normal file
@@ -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<PdbRequest> 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<String,String> badRequest(Exception e) { return Map.of("error", e.getMessage()); }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.oracle.pdbportal;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.*;
|
||||
|
||||
interface PdbRequestRepository extends JpaRepository<PdbRequest, UUID> {
|
||||
List<PdbRequest> findByStatus(RequestStatus status);
|
||||
boolean existsByPdbNameIgnoreCase(String pdbName);
|
||||
}
|
||||
50
src/main/java/com/oracle/pdbportal/PdbRequestService.java
Normal file
50
src/main/java/com/oracle/pdbportal/PdbRequestService.java
Normal file
@@ -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<PdbRequest> 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) { }
|
||||
11
src/main/java/com/oracle/pdbportal/PortalProperties.java
Normal file
11
src/main/java/com/oracle/pdbportal/PortalProperties.java
Normal file
@@ -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) { }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
25
src/main/resources/application.yml
Normal file
25
src/main/resources/application.yml
Normal file
@@ -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}
|
||||
18
src/main/resources/static/app.js
Normal file
18
src/main/resources/static/app.js
Normal file
@@ -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 => `<tr><td>${item.pdbName}</td><td>${item.storageGb}</td><td>${item.requestedBy}</td><td>${item.status}</td><td>${item.status === 'PENDING' ? `<button onclick="approve('${item.id}')">Aprovar</button>` : ''}</td></tr>`).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();
|
||||
7
src/main/resources/static/index.html
Normal file
7
src/main/resources/static/index.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Portal PDB</title>
|
||||
<style>body{font:16px system-ui;max-width:900px;margin:3rem auto;padding:0 1rem}form,table{width:100%;margin:1rem 0}label,input,button{display:block;margin:.5rem 0}input{padding:.5rem;width:24rem;max-width:100%}th,td{padding:.6rem;text-align:left;border-bottom:1px solid #ddd}button{padding:.5rem .8rem}#message{min-height:1.5rem}</style></head>
|
||||
<body><h1>Solicitação de PDB</h1><p>Autentique-se pelo navegador: <code>developer</code> para solicitar ou <code>admin</code> para aprovar.</p>
|
||||
<form id="request"><label>Nome do PDB <input name="pdbName" required maxlength="30" pattern="[A-Za-z][A-Za-z0-9]*"></label><label>Storage inicial da DATA (GB) <input name="storageGb" required type="number" min="10"></label><label>Senha inicial do PDBADMIN <input name="initialPassword" required type="password"></label><button>Enviar para aprovação</button></form>
|
||||
<p id="message"></p><h2>Solicitações</h2><button id="refresh">Atualizar</button><table><thead><tr><th>PDB</th><th>GB</th><th>Solicitante</th><th>Status</th><th>Ação</th></tr></thead><tbody id="rows"></tbody></table>
|
||||
<script src="/app.js"></script></body></html>
|
||||
Reference in New Issue
Block a user