diff --git a/README.md b/README.md new file mode 100644 index 00000000..fa4154b3 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# AI-BankApp-DevOps + +A Spring Boot banking application used as a base for learning end-to-end DevOps — from Docker to Kubernetes to GitOps. + +## Tech Stack + +- **Backend:** Spring Boot 3.4.1, Java 21, Spring Security, JPA/Hibernate +- **Frontend:** Thymeleaf, Bootstrap 5, Glassmorphism UI with dark/light theme +- **Database:** MySQL 8.0 +- **AI:** Ollama (self-hosted LLM chatbot, zero cost) +- **DevOps:** Docker, GitHub Actions, Kubernetes, Helm, Terraform, Prometheus, Grafana, ArgoCD + +## Branches + +| Branch | Description | +|--------|-------------| +| `start` | Modernized app — full backend + frontend (developer handoff) | +| `docker` | Adds Dockerfile, multi-stage build, docker-compose | +| `ai` | Adds AI chatbot powered by Ollama | +| `main` | End-to-end DevOps (WIP) | + +See [ROADMAP.md](ROADMAP.md) for the full progression. + +## Quick Start + +### Run locally (needs Java 21 + MySQL) + +```bash +# Create database +mysql -u root -p -e "CREATE DATABASE bankappdb;" + +# Run the app +./mvnw spring-boot:run +``` + +### Run with Docker (recommended) + +```bash +# Switch to docker branch +git checkout docker + +# Start everything +docker compose up -d --build + +# Visit http://localhost:8080 +``` + +### Run with AI Chatbot + +```bash +# Switch to ai branch +git checkout ai + +# Start everything (includes Ollama) +docker compose up -d --build + +# Pull the AI model (one-time) +docker exec bankapp-ollama ollama pull tinyllama + +# Visit http://localhost:8080 +``` + +## Features + +- User registration & login with BCrypt passwords +- Deposit, withdraw, transfer between accounts +- Transaction history with color-coded entries +- Dark/light theme toggle (persists across sessions) +- AI chatbot that knows your balance and recent transactions +- Prometheus metrics at `/actuator/prometheus` +- Health check at `/actuator/health` diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..c4165a95 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,88 @@ +# DevOps Roadmap — BankApp + +A step-by-step progression from code to production-grade DevOps. +Each phase builds on the previous one. Check off as you go. + +--- + +## Phase 1: Application (`start` branch) +- [x] Spring Boot backend with MySQL +- [x] Thymeleaf frontend with modern UI +- [x] Spring Security (login, register, CSRF) +- [x] Actuator + Prometheus metrics endpoint +- [x] Externalized config via environment variables + +## Phase 2: Docker (`docker` branch) +- [x] Dockerfile (simple) +- [x] Dockerfile.multistage (optimized image) +- [x] docker-compose.yml (app + MySQL) +- [ ] .dockerignore file +- [ ] Push image to Docker Hub + +## Phase 3: CI/CD (`cicd` branch) +- [ ] GitHub Actions workflow — build & test on PR +- [ ] Build Docker image in CI +- [ ] Push image to Docker Hub from CI +- [ ] Tag images with git SHA + `latest` + +## Phase 4: Kubernetes (`k8s` branch) +- [ ] Deployment manifest (app) +- [ ] Service manifest (ClusterIP) +- [ ] ConfigMap (app config) +- [ ] Secret (DB credentials) +- [ ] MySQL StatefulSet or external DB +- [ ] Ingress with host-based routing +- [ ] Deploy to a local cluster (minikube / kind) + +## Phase 5: Helm (`helm` branch) +- [ ] Helm chart for BankApp +- [ ] values.yaml for dev / prod +- [ ] Install via `helm install` + +## Phase 6: IaC with Terraform (`terraform` branch) +- [ ] Provision AWS EKS cluster (or equivalent) +- [ ] RDS MySQL instance +- [ ] VPC, subnets, security groups +- [ ] State stored in S3 + DynamoDB lock + +## Phase 7: Monitoring (`monitoring` branch) +- [ ] Prometheus scraping `/actuator/prometheus` +- [ ] Grafana dashboard for app metrics +- [ ] Alerting rules (high error rate, pod restarts) + +## Phase 8: GitOps (`gitops` branch) +- [ ] ArgoCD installed on cluster +- [ ] App synced from Git repo to K8s +- [ ] Auto-sync on push to main + +## Phase 9: Security & Quality (`security` branch) +- [ ] Trivy image scan in CI pipeline +- [ ] SonarQube code quality scan +- [ ] OWASP dependency check +- [ ] Non-root container user + +## Phase 10: AI Chatbot (`ai` branch) +- [ ] Ollama container in docker-compose (self-hosted, zero cost) +- [ ] Chat REST API in Spring Boot calling Ollama +- [ ] Floating chat widget on dashboard +- [ ] Context-aware — knows user's balance and transactions +- [ ] Deploy Ollama on K8s with GPU/CPU resource limits + +## Phase 11: Production Readiness (`prod` branch) +- [ ] TLS / HTTPS via cert-manager +- [ ] Horizontal Pod Autoscaler (HPA) +- [ ] Resource limits and requests +- [ ] Liveness & readiness probes +- [ ] Multi-environment setup (dev / staging / prod) + +--- + +## The Story for Interviews + +> "I took a Spring Boot banking application, integrated a self-hosted AI chatbot +> using Ollama, containerized everything with Docker, built a CI/CD pipeline with +> GitHub Actions, deployed to Kubernetes using Helm charts, provisioned cloud +> infrastructure with Terraform, set up monitoring with Prometheus and Grafana, +> and implemented GitOps with ArgoCD for automated deployments." + +Each phase = one branch = one talking point. diff --git a/mvnw b/mvnw old mode 100644 new mode 100755 diff --git a/pom.xml b/pom.xml index 38bd6d74..c9942e6c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.3 + 3.4.1 com.example @@ -27,7 +27,7 @@ - 17 + 21 @@ -46,15 +46,26 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-validation + org.thymeleaf.extras thymeleaf-extras-springsecurity6 + + io.micrometer + micrometer-registry-prometheus + - mysql - mysql-connector-java - 8.0.33 + com.mysql + mysql-connector-j runtime diff --git a/src/main/java/com/example/bankapp/config/SecurityConfig.java b/src/main/java/com/example/bankapp/config/SecurityConfig.java new file mode 100644 index 00000000..f7c0a8cf --- /dev/null +++ b/src/main/java/com/example/bankapp/config/SecurityConfig.java @@ -0,0 +1,40 @@ +package com.example.bankapp.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/register", "/login", "/css/**", "/js/**", "/actuator/**").permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginPage("/login") + .defaultSuccessUrl("/dashboard", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/example/bankapp/controller/BankController.java b/src/main/java/com/example/bankapp/controller/BankController.java new file mode 100644 index 00000000..94ee8923 --- /dev/null +++ b/src/main/java/com/example/bankapp/controller/BankController.java @@ -0,0 +1,86 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.model.Account; +import com.example.bankapp.service.AccountService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.math.BigDecimal; + +@Controller +public class BankController { + + private final AccountService accountService; + + public BankController(AccountService accountService) { + this.accountService = accountService; + } + + @GetMapping("/login") + public String loginPage() { + return "login"; + } + + @GetMapping("/register") + public String registerPage() { + return "register"; + } + + @PostMapping("/register") + public String register(@RequestParam String username, + @RequestParam String password, + Model model) { + if (accountService.registerAccount(username, password)) { + return "redirect:/login?registered"; + } + model.addAttribute("error", true); + return "register"; + } + + @GetMapping("/dashboard") + public String dashboard(@AuthenticationPrincipal Account account, Model model) { + model.addAttribute("account", account); + return "dashboard"; + } + + @PostMapping("/deposit") + public String deposit(@AuthenticationPrincipal Account account, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + accountService.deposit(account, amount); + return "redirect:/dashboard"; + } + + @PostMapping("/withdraw") + public String withdraw(@AuthenticationPrincipal Account account, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + if (!accountService.withdraw(account, amount)) { + redirectAttributes.addFlashAttribute("error", "Insufficient funds."); + } + return "redirect:/dashboard"; + } + + @PostMapping("/transfer") + public String transfer(@AuthenticationPrincipal Account account, + @RequestParam String toUsername, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + String error = accountService.transferAmount(account, toUsername, amount); + if (error != null) { + redirectAttributes.addFlashAttribute("error", error); + } + return "redirect:/dashboard"; + } + + @GetMapping("/transactions") + public String transactions(@AuthenticationPrincipal Account account, Model model) { + model.addAttribute("transactions", accountService.getTransactionHistory(account)); + return "transactions"; + } +} diff --git a/src/main/java/com/example/bankapp/model/Account.java b/src/main/java/com/example/bankapp/model/Account.java new file mode 100644 index 00000000..a85b5c5c --- /dev/null +++ b/src/main/java/com/example/bankapp/model/Account.java @@ -0,0 +1,112 @@ +package com.example.bankapp.model; + +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Entity +@Table(name = "accounts") +public class Account implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String username; + + @Column(nullable = false) + private String password; + + @Column(nullable = false, precision = 19, scale = 2) + private BigDecimal balance = BigDecimal.ZERO; + + @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List transactions = new ArrayList<>(); + + public Account() { + } + + public Account(String username, String password) { + this.username = username; + this.password = password; + this.balance = BigDecimal.ZERO; + } + + // Getters and setters + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public BigDecimal getBalance() { + return balance; + } + + public void setBalance(BigDecimal balance) { + this.balance = balance; + } + + public List getTransactions() { + return transactions; + } + + public void setTransactions(List transactions) { + this.transactions = transactions; + } + + // UserDetails implementation + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/example/bankapp/model/Transaction.java b/src/main/java/com/example/bankapp/model/Transaction.java new file mode 100644 index 00000000..799de060 --- /dev/null +++ b/src/main/java/com/example/bankapp/model/Transaction.java @@ -0,0 +1,78 @@ +package com.example.bankapp.model; + +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "transactions") +public class Transaction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, precision = 19, scale = 2) + private BigDecimal amount; + + @Column(nullable = false) + private String type; + + @Column(nullable = false) + private LocalDateTime timestamp; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id") + private Account account; + + public Transaction() { + } + + public Transaction(BigDecimal amount, String type, LocalDateTime timestamp, Account account) { + this.amount = amount; + this.type = type; + this.timestamp = timestamp; + this.account = account; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } +} diff --git a/src/main/java/com/example/bankapp/repository/AccountRepository.java b/src/main/java/com/example/bankapp/repository/AccountRepository.java new file mode 100644 index 00000000..72553370 --- /dev/null +++ b/src/main/java/com/example/bankapp/repository/AccountRepository.java @@ -0,0 +1,10 @@ +package com.example.bankapp.repository; + +import com.example.bankapp.model.Account; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/src/main/java/com/example/bankapp/repository/TransactionRepository.java b/src/main/java/com/example/bankapp/repository/TransactionRepository.java new file mode 100644 index 00000000..89408151 --- /dev/null +++ b/src/main/java/com/example/bankapp/repository/TransactionRepository.java @@ -0,0 +1,10 @@ +package com.example.bankapp.repository; + +import com.example.bankapp.model.Transaction; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TransactionRepository extends JpaRepository { + List findByAccountIdOrderByTimestampDesc(Long accountId); +} diff --git a/src/main/java/com/example/bankapp/service/AccountService.java b/src/main/java/com/example/bankapp/service/AccountService.java new file mode 100644 index 00000000..5fbffbf9 --- /dev/null +++ b/src/main/java/com/example/bankapp/service/AccountService.java @@ -0,0 +1,98 @@ +package com.example.bankapp.service; + +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import com.example.bankapp.repository.AccountRepository; +import com.example.bankapp.repository.TransactionRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class AccountService implements UserDetailsService { + + private final AccountRepository accountRepository; + private final TransactionRepository transactionRepository; + private final PasswordEncoder passwordEncoder; + + public AccountService(AccountRepository accountRepository, + TransactionRepository transactionRepository, + PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.transactionRepository = transactionRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return accountRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + } + + public boolean registerAccount(String username, String password) { + if (accountRepository.findByUsername(username).isPresent()) { + return false; + } + Account account = new Account(username, passwordEncoder.encode(password)); + accountRepository.save(account); + return true; + } + + @Transactional + public void deposit(Account account, BigDecimal amount) { + account.setBalance(account.getBalance().add(amount)); + accountRepository.save(account); + + Transaction transaction = new Transaction(amount, "Deposit", LocalDateTime.now(), account); + transactionRepository.save(transaction); + } + + @Transactional + public boolean withdraw(Account account, BigDecimal amount) { + if (account.getBalance().compareTo(amount) < 0) { + return false; + } + account.setBalance(account.getBalance().subtract(amount)); + accountRepository.save(account); + + Transaction transaction = new Transaction(amount, "Withdrawal", LocalDateTime.now(), account); + transactionRepository.save(transaction); + return true; + } + + @Transactional + public String transferAmount(Account from, String toUsername, BigDecimal amount) { + if (from.getUsername().equals(toUsername)) { + return "Cannot transfer to yourself."; + } + if (from.getBalance().compareTo(amount) < 0) { + return "Insufficient funds."; + } + Account to = accountRepository.findByUsername(toUsername).orElse(null); + if (to == null) { + return "Recipient not found."; + } + + from.setBalance(from.getBalance().subtract(amount)); + to.setBalance(to.getBalance().add(amount)); + accountRepository.save(from); + accountRepository.save(to); + + LocalDateTime now = LocalDateTime.now(); + transactionRepository.save(new Transaction(amount, "Transfer Out", now, from)); + transactionRepository.save(new Transaction(amount, "Transfer In", now, to)); + + return null; // null means success + } + + public List getTransactionHistory(Account account) { + return transactionRepository.findByAccountIdOrderByTimestampDesc(account.getId()); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 08663a63..f934de60 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,11 +1,22 @@ spring.application.name=bankapp + # MySQL Database configuration -spring.datasource.url=jdbc:mysql://localhost:3306/bankappdb?useSSL=false&serverTimezone=UTC -spring.datasource.username=root -spring.datasource.password=Test@123 +spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:bankappdb}?useSSL=false&allowPublicKeyRetrieval=true +spring.datasource.username=${MYSQL_USER:root} +spring.datasource.password=${MYSQL_PASSWORD:Test@123} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA & Hibernate configuration spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect + +# Virtual threads (Java 21) +spring.threads.virtual.enabled=true + +# Actuator +management.endpoints.web.exposure.include=health,info,metrics,prometheus +management.endpoint.health.show-details=when-authorized + +# Structured logging +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n diff --git a/src/main/resources/static/css/bankapp.css b/src/main/resources/static/css/bankapp.css new file mode 100644 index 00000000..34a9046d --- /dev/null +++ b/src/main/resources/static/css/bankapp.css @@ -0,0 +1,507 @@ +/* ===== CSS Custom Properties ===== */ +:root { + --bg-primary: #0a0a0a; + --bg-surface: rgba(255, 255, 255, 0.03); + --text-primary: #f1f5f9; + --text-muted: #94a3b8; + --accent: #3b82f6; + --accent-hover: #2563eb; + --accent-glow: rgba(59, 130, 246, 0.4); + --success: #22c55e; + --danger: #ef4444; + --border-color: rgba(255, 255, 255, 0.08); + --border-radius: 16px; + --border-radius-sm: 10px; + --transition-speed: 0.3s; + --glass-bg: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.1); + --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + --gradient-bg: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%); + --font-stack: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +[data-theme="light"] { + --bg-primary: #f8fafc; + --bg-surface: rgba(0, 0, 0, 0.02); + --text-primary: #0f172a; + --text-muted: #64748b; + --border-color: rgba(0, 0, 0, 0.08); + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-border: rgba(0, 0, 0, 0.08); + --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + --gradient-bg: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #dbeafe 100%); +} + +/* ===== Reset & Base ===== */ +*, *::before, *::after { + box-sizing: border-box; +} + +body { + font-family: var(--font-stack); + background: var(--gradient-bg); + color: var(--text-primary); + margin: 0; + padding: 0; + min-height: 100vh; + transition: background var(--transition-speed), color var(--transition-speed); +} + +a { + color: var(--accent); + text-decoration: none; + transition: color var(--transition-speed); +} + +a:hover { + color: var(--accent-hover); +} + +/* ===== Navbar ===== */ +.navbar-app { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--glass-border); + padding: 0.75rem 1.5rem; + position: sticky; + top: 0; + z-index: 1000; +} + +.navbar-app .navbar-brand { + font-weight: 700; + font-size: 1.25rem; + color: var(--text-primary) !important; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.navbar-app .nav-link { + color: var(--text-muted) !important; + font-weight: 500; + padding: 0.5rem 1rem !important; + border-radius: var(--border-radius-sm); + transition: all var(--transition-speed); +} + +.navbar-app .nav-link:hover, +.navbar-app .nav-link.active { + color: var(--text-primary) !important; + background: var(--bg-surface); +} + +.theme-toggle { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + color: var(--text-primary); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all var(--transition-speed); + font-size: 1.1rem; +} + +.theme-toggle:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* ===== Glass Card ===== */ +.glass-card { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: var(--border-radius); + box-shadow: var(--glass-shadow); + padding: 2rem; + transition: transform var(--transition-speed), box-shadow var(--transition-speed); +} + +.glass-card:hover { + transform: translateY(-2px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); +} + +[data-theme="light"] .glass-card:hover { + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); +} + +/* ===== Auth Pages (Login/Register) ===== */ +.auth-wrapper { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.auth-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +.auth-card { + width: 100%; + max-width: 440px; + animation: fadeInUp 0.5s ease-out; +} + +.auth-card h2 { + font-weight: 700; + margin-bottom: 0.5rem; +} + +.auth-card .text-muted { + color: var(--text-muted) !important; +} + +/* ===== Form Styles ===== */ +.form-floating > .form-control { + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + transition: border-color var(--transition-speed), box-shadow var(--transition-speed); +} + +.form-floating > .form-control:focus { + background: var(--bg-surface); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); + color: var(--text-primary); +} + +.form-floating > label { + color: var(--text-muted); +} + +.form-floating > .form-control:focus ~ label, +.form-floating > .form-control:not(:placeholder-shown) ~ label { + color: var(--accent); +} + +/* ===== Buttons ===== */ +.btn-accent { + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--border-radius-sm); + padding: 0.75rem 1.5rem; + font-weight: 600; + transition: all var(--transition-speed); +} + +.btn-accent:hover { + background: var(--accent-hover); + color: #fff; + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--accent-glow); +} + +.btn-glass { + background: var(--glass-bg); + color: var(--text-primary); + border: 1px solid var(--glass-border); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1.5rem; + font-weight: 500; + transition: all var(--transition-speed); +} + +.btn-glass:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* ===== Password Toggle ===== */ +.input-group-password { + position: relative; +} + +.input-group-password .form-control { + padding-right: 3rem; +} + +.password-toggle { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + z-index: 5; + padding: 0.25rem; +} + +.password-toggle:hover { + color: var(--accent); +} + +/* ===== Dashboard ===== */ +.dashboard-wrapper { + padding: 2rem 1rem; + max-width: 1100px; + margin: 0 auto; +} + +.balance-card { + text-align: center; + padding: 2.5rem 2rem; + margin-bottom: 2rem; +} + +.balance-label { + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.balance-amount { + font-size: 3rem; + font-weight: 800; + color: var(--text-primary); + line-height: 1; +} + +.balance-amount .currency { + color: var(--accent); +} + +.account-info { + display: flex; + justify-content: center; + gap: 2rem; + margin-top: 1rem; + color: var(--text-muted); + font-size: 0.875rem; +} + +.action-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.action-card { + padding: 1.5rem; +} + +.action-card h5 { + font-weight: 600; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.action-card .form-control { + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1rem; +} + +.action-card .form-control:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.action-card .form-label { + color: var(--text-muted); + font-size: 0.875rem; + font-weight: 500; +} + +/* ===== Transactions Table ===== */ +.transactions-wrapper { + padding: 2rem 1rem; + max-width: 900px; + margin: 0 auto; +} + +.table-glass { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +.table-glass thead th { + background: var(--bg-surface); + color: var(--text-muted); + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.table-glass tbody td { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + vertical-align: middle; +} + +.table-glass tbody tr { + transition: background var(--transition-speed); +} + +.table-glass tbody tr:hover { + background: var(--bg-surface); +} + +.amount-positive { + color: var(--success); + font-weight: 600; +} + +.amount-negative { + color: var(--danger); + font-weight: 600; +} + +.badge-type { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; +} + +.badge-deposit { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.badge-withdrawal { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.badge-transfer-out { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.badge-transfer-in { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.empty-state { + text-align: center; + padding: 4rem 2rem; + color: var(--text-muted); +} + +.empty-state i { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +/* ===== Alert Styles ===== */ +.alert-glass { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: var(--danger); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1rem; +} + +.alert-glass-success { + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + color: var(--success); +} + +/* ===== Footer ===== */ +.footer-app { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-top: 1px solid var(--glass-border); + padding: 1.25rem; + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; + margin-top: auto; +} + +.footer-app a { + color: var(--accent); + font-weight: 500; +} + +/* ===== Animations ===== */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeInUp 0.5s ease-out; +} + +.fade-in-delay-1 { animation-delay: 0.1s; animation-fill-mode: both; } +.fade-in-delay-2 { animation-delay: 0.2s; animation-fill-mode: both; } +.fade-in-delay-3 { animation-delay: 0.3s; animation-fill-mode: both; } + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .balance-amount { + font-size: 2.25rem; + } + + .account-info { + flex-direction: column; + gap: 0.5rem; + } + + .action-cards { + grid-template-columns: 1fr; + } + + .glass-card { + padding: 1.5rem; + } + + .table-glass thead th, + .table-glass tbody td { + padding: 0.75rem 0.5rem; + font-size: 0.875rem; + } +} + +@media (max-width: 480px) { + .balance-amount { + font-size: 1.75rem; + } + + .auth-card { + padding: 1.5rem; + } +} diff --git a/src/main/resources/static/js/theme.js b/src/main/resources/static/js/theme.js new file mode 100644 index 00000000..e27c1f74 --- /dev/null +++ b/src/main/resources/static/js/theme.js @@ -0,0 +1,33 @@ +(function () { + const STORAGE_KEY = 'bankapp-theme'; + + function getPreferred() { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) return stored; + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + + function apply(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem(STORAGE_KEY, theme); + const icon = document.getElementById('themeIcon'); + if (icon) { + icon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill'; + } + } + + // Apply immediately to prevent flash + apply(getPreferred()); + + document.addEventListener('DOMContentLoaded', function () { + const btn = document.getElementById('themeToggle'); + if (btn) { + btn.addEventListener('click', function () { + const current = document.documentElement.getAttribute('data-theme') || 'dark'; + apply(current === 'dark' ? 'light' : 'dark'); + }); + } + // Re-apply to update icon after DOM is ready + apply(getPreferred()); + }); +})(); diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 24eb6990..1d84ad6a 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -1,201 +1,86 @@ - - - Dashboard - Goldencat Bank - - - + + - - -
-
-

-

-
+
+ + +
+ +
+
Total Balance
+
+ $0.00 +
+ +
- - + +
+ +
-
- -
- -
-
-
- - + +
+ +
+
Deposit
+ +
+ +
- +
-
- -
- -
-
-
- - + +
+
Withdraw
+ +
+ +
- +
-
- -
- -
-
-
- - + +
+
Transfer
+ +
+ +
-
- - +
+ +
- +
-

+
- - - + diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html new file mode 100644 index 00000000..07185ea6 --- /dev/null +++ b/src/main/resources/templates/fragments/layout.html @@ -0,0 +1,62 @@ + + + + + + + + BankApp + + + + + + + + + + + + + +
+

© 2026 BankApp. Built with Spring Boot.

+
+ + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 57fd3666..b160bda6 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -1,156 +1,72 @@ - - - Login - Goldencat Bank - - - - + + - - -
-

Login

-
-
- - +
+ + +
+
+
+

Welcome back

+

Sign in to your account

+
+ +
+ Invalid username or password. +
+ +
+ You have been logged out. +
+ +
+ Registration successful. Please sign in. +
+ + +
+ + +
+ +
+ + + +
+ + + + +

+ Don't have an account? Create one +

-
- - -
- - -

Don't have an account? Register here

- -
- Invalid username or password.
-
- - +
+
+ + diff --git a/src/main/resources/templates/register.html b/src/main/resources/templates/register.html index 4fbeb045..a54ca3a3 100644 --- a/src/main/resources/templates/register.html +++ b/src/main/resources/templates/register.html @@ -1,156 +1,95 @@ - - - Register - Goldencat Bank - - - - + + - - -
-

Register a New Account

-
-
- - +
+ + +
+
+
+

Create Account

+

Join BankApp today

+
+ +
+ Username already taken. Please choose another. +
+ + +
+ + +
+ +
+ + + +
+ +
+
+
+
+ +
+ + + + +

+ Already have an account? Sign in +

-
- - -
- - -

Already have an account? Login here

- -
- User already present.
-
- - +
+
+ + diff --git a/src/main/resources/templates/transactions.html b/src/main/resources/templates/transactions.html index 892042c8..e0e48bbe 100644 --- a/src/main/resources/templates/transactions.html +++ b/src/main/resources/templates/transactions.html @@ -1,129 +1,64 @@ - - - Transaction History - Goldencat Bank - - - + + - - -
-

Transaction History

-
- - - - - - - - - - - - - - - - - -
IDTypeAmountDate
- -
+
+ + +
+
+

Transaction History

+ + Dashboard + +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + +
IDTypeAmountDate
+ + +
+
+
+ + +
+ +
No transactions yet
+

Your transaction history will appear here once you make your first deposit.

+ + Make a Deposit + +
+
-

Back to Dashboard

+
+