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
| PDB | GB | Solicitante | Status | Ação |
|---|
+