the admin pages are now behind an admin password set up at start up

This commit is contained in:
Rostislav Raykov
2024-12-01 22:23:27 +02:00
parent c94ec5effc
commit 4511465d7c
15 changed files with 293 additions and 59 deletions
Vendored
+1 -1
View File
@@ -25,7 +25,7 @@
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password.html for downloading maven
# MVNW_USERNAME/MVNW_PASSWORD - user and app-password.html for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
@@ -0,0 +1,25 @@
package org.rostislav.quickdrop.config;
import org.rostislav.quickdrop.interceptor.AdminPasswordInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final AdminPasswordInterceptor adminPasswordInterceptor;
@Autowired
public WebConfig(AdminPasswordInterceptor adminPasswordInterceptor) {
this.adminPasswordInterceptor = adminPasswordInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminPasswordInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/admin/setup", "/static/**", "/css/**", "/js/**", "/images/**");
}
}
@@ -1,5 +1,6 @@
package org.rostislav.quickdrop.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.rostislav.quickdrop.entity.ApplicationSettingsEntity;
import org.rostislav.quickdrop.entity.FileEntity;
import org.rostislav.quickdrop.model.AnalyticsDataView;
@@ -8,11 +9,13 @@ import org.rostislav.quickdrop.model.FileEntityView;
import org.rostislav.quickdrop.service.AnalyticsService;
import org.rostislav.quickdrop.service.ApplicationSettingsService;
import org.rostislav.quickdrop.service.FileService;
import org.springframework.security.crypto.bcrypt.BCrypt;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@@ -32,7 +35,11 @@ public class AdminViewController {
}
@GetMapping("/dashboard")
public String getDashboardPage(Model model) {
public String getDashboardPage(Model model, HttpServletRequest request) {
if (!checkForAdminPassword(request)) {
return "redirect:/admin/password";
}
List<FileEntity> files = fileService.getFiles();
model.addAttribute("files", files.stream().map(
@@ -44,9 +51,26 @@ public class AdminViewController {
return "admin/dashboard";
}
@GetMapping("/setup")
public String showSetupPage() {
if (applicationSettingsService.isAdminPasswordSet()) {
return "redirect:/admin/dashboard";
}
return "welcome";
}
@PostMapping("/setup")
public String setAdminPassword(String adminPassword) {
applicationSettingsService.setAdminPassword(adminPassword);
return "redirect:/admin/dashboard";
}
@GetMapping("/settings")
public String getSettingsPage(Model model) {
public String getSettingsPage(Model model, HttpServletRequest request) {
if (!checkForAdminPassword(request)) {
return "redirect:/admin/password";
}
ApplicationSettingsEntity settings = applicationSettingsService.getApplicationSettings();
ApplicationSettingsViewModel applicationSettingsViewModel = new ApplicationSettingsViewModel(settings);
@@ -57,11 +81,36 @@ public class AdminViewController {
}
@PostMapping("/save")
public String saveSettings(ApplicationSettingsViewModel settings) {
public String saveSettings(ApplicationSettingsViewModel settings, HttpServletRequest request) {
if (!checkForAdminPassword(request)) {
return "redirect:/admin/password";
}
settings.setMaxFileSize(megabytesToBytes(settings.getMaxFileSize()));
applicationSettingsService.updateApplicationSettings(settings, settings.getAppPassword());
return "redirect:/admin/dashboard";
}
@PostMapping("/password")
public String checkAdminPassword(@RequestParam String password, HttpServletRequest request) {
String adminPasswordHash = applicationSettingsService.getAdminPasswordHash();
if (BCrypt.checkpw(password, adminPasswordHash)) {
request.getSession().setAttribute("adminPassword", adminPasswordHash);
return "redirect:/admin/dashboard";
} else {
return "redirect:/admin/password";
}
}
@GetMapping("/password")
public String showAdminPasswordPage() {
return "/admin/admin-password";
}
private boolean checkForAdminPassword(HttpServletRequest request) {
String password = (String) request.getSession().getAttribute("adminPassword");
String adminPasswordHash = applicationSettingsService.getAdminPasswordHash();
return password != null && password.equals(adminPasswordHash);
}
}
@@ -61,7 +61,7 @@ public class FileViewController {
if (fileEntity.passwordHash != null &&
(password == null || !fileService.checkPassword(uuid, password))) {
model.addAttribute("uuid", uuid);
return "filePassword";
return "file-password";
}
populateModelAttributes(fileEntity, model, request);
@@ -89,7 +89,7 @@ public class FileViewController {
return "redirect:/file/" + uuid;
} else {
model.addAttribute("uuid", uuid);
return "filePassword";
return "file-password";
}
}
@@ -2,13 +2,11 @@ package org.rostislav.quickdrop.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.view.RedirectView;
@Controller
public class IndexViewController {
@GetMapping("/")
public RedirectView index() {
return new RedirectView("/file/upload");
public String getIndexPage() {
return "redirect:/file/upload";
}
}
@@ -9,6 +9,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
public class PasswordController {
@GetMapping("/login")
public String passwordPage() {
return "password";
return "app-password";
}
@GetMapping("/admin")
public String adminPasswordPage() {
return "admin/admin-password";
}
}
@@ -0,0 +1,32 @@
package org.rostislav.quickdrop.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.rostislav.quickdrop.service.ApplicationSettingsService;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class AdminPasswordInterceptor implements HandlerInterceptor {
private final ApplicationSettingsService applicationSettingsService;
public AdminPasswordInterceptor(ApplicationSettingsService applicationSettingsService) {
this.applicationSettingsService = applicationSettingsService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
if (!applicationSettingsService.isAdminPasswordSet()
&& !requestURI.startsWith("/admin/setup")
&& !requestURI.startsWith("/static/")
&& !requestURI.startsWith("/css/")
&& !requestURI.startsWith("/js/")
&& !requestURI.startsWith("/images/")) {
response.sendRedirect("/admin/setup");
return false;
}
return true;
}
}
@@ -98,4 +98,15 @@ public class ApplicationSettingsService {
public String getAdminPasswordHash() {
return applicationSettings.getAdminPasswordHash();
}
public boolean isAdminPasswordSet() {
return !applicationSettings.getAdminPasswordHash().isEmpty();
}
public void setAdminPassword(String adminPassword) {
ApplicationSettingsEntity applicationSettingsEntity = applicationSettingsRepository.findById(1L).orElseThrow();
applicationSettingsEntity.setAdminPasswordHash(BCrypt.hashpw(adminPassword, BCrypt.gensalt()));
applicationSettingsRepository.save(applicationSettingsEntity);
this.applicationSettings = applicationSettingsEntity;
}
}
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
Enter Admin Password</title>
<meta content="width=device-width, initial-scale=1"
name="viewport">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet">
<link href="/images/favicon.png"
rel="icon"
type="image/png">
</head>
<body>
<!-- Main Content -->
<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-12 col-md-6 col-lg-4">
<h2 class="text-center mb-4">
Admin Password Required</h2>
<div class="card shadow">
<div class="card-body">
<form id="adminPasswordForm"
method="POST"
th:action="@{/admin/password}">
<!-- CSRF Token -->
<input th:name="${_csrf.parameterName}"
th:value="${_csrf.token}"
type="hidden"/>
<!-- Password Field -->
<div class="mb-3">
<label class="form-label"
for="adminPassword">Admin Password:</label>
<input
class="form-control"
id="adminPassword"
name="password"
required
type="password"
>
</div>
<!-- Submit Button -->
<button class="btn btn-primary w-100"
type="submit">
Submit
</button>
</form>
<!-- Error Message -->
<div th:if="${error}">
<p class="text-danger mt-3"
th:text="${error}"></p>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+13 -18
View File
@@ -17,43 +17,38 @@
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand d-flex align-items-center"
href="/">
<img alt="Website Logo"
class="me-2"
height="40"
src="/images/favicon.png">
<a class="navbar-brand d-flex align-items-center" href="/">
<img alt="Website Logo" class="me-2" height="40" src="/images/favicon.png">
QuickDrop
</a>
<button
class="navbar-toggler"
type="button"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
class="navbar-toggler"
data-bs-target="#navbarNav"
data-bs-toggle="collapse"
type="button"
>
data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse"
id="navbarNav">
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link"
href="/file/list">View
Files</a>
<a class="nav-link" href="/file/list">View Files</a>
</li>
<li class="nav-item">
<a class="nav-link"
href="/file/upload">Upload
File</a>
<a class="nav-link" href="/file/upload">Upload File</a>
</li>
<li class="nav-item">
<!-- Admin Dashboard Button -->
<a class="nav-link" href="/admin/dashboard" onclick="requestAdminPassword()">Admin Dashboard</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container mt-5">
<h1 class="text-center mb-4">
+12 -15
View File
@@ -17,38 +17,35 @@
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand d-flex align-items-center"
href="/">
<img alt="Website Logo"
class="me-2"
height="40"
src="/images/favicon.png">
<a class="navbar-brand d-flex align-items-center" href="/">
<img alt="Website Logo" class="me-2" height="40" src="/images/favicon.png">
QuickDrop
</a>
<button
class="navbar-toggler"
type="button"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
class="navbar-toggler"
data-bs-target="#navbarNav"
data-bs-toggle="collapse"
type="button"
>
data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse"
id="navbarNav">
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link"
href="/file/upload">Upload
File</a>
<a class="nav-link" href="/file/upload">Upload File</a>
</li>
<li class="nav-item">
<!-- Admin Dashboard Button -->
<a class="nav-link" href="/admin/dashboard" onclick="requestAdminPassword()">Admin Dashboard</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container mt-5">
<h1 class="text-center mb-4">
+12 -15
View File
@@ -17,38 +17,35 @@
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand d-flex align-items-center"
href="/">
<img alt="Website Logo"
class="me-2"
height="40"
src="/images/favicon.png">
<a class="navbar-brand d-flex align-items-center" href="/">
<img alt="Website Logo" class="me-2" height="40" src="/images/favicon.png">
QuickDrop
</a>
<button
class="navbar-toggler"
type="button"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
class="navbar-toggler"
data-bs-target="#navbarNav"
data-bs-toggle="collapse"
type="button"
>
data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse"
id="navbarNav">
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link"
href="/file/list">View
Files</a>
<a class="nav-link" href="/file/list">View Files</a>
</li>
<li class="nav-item">
<!-- Admin Dashboard Button -->
<a class="nav-link" href="/admin/dashboard" onclick="requestAdminPassword()">Admin Dashboard</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container">
<h1 class="text-center mb-4">
+60
View File
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Welcome to QuickDrop</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f8f9fa;
}
.welcome-card {
max-width: 500px;
width: 100%;
}
</style>
</head>
<body>
<div class="card welcome-card shadow">
<div class="card-header bg-primary text-white text-center">
<h1>Welcome to QuickDrop</h1>
</div>
<div class="card-body">
<p class="text-center">
Thank you for setting up QuickDrop! Before you get started, please set an admin password for the dashboard.
</p>
<form id="setupForm" method="post" th:action="@{/admin/setup}">
<div class="mb-3">
<label class="form-label" for="adminPassword">Admin Password</label>
<input class="form-control" id="adminPassword" minlength="8" name="adminPassword" required
type="password">
</div>
<div class="mb-3">
<label class="form-label" for="confirmPassword">Confirm Password</label>
<input class="form-control" id="confirmPassword" minlength="8" name="confirmPassword" required
type="password">
</div>
<div class="text-danger mb-3" id="error-message" style="display: none;">Passwords do not match.</div>
<button class="btn btn-primary w-100" type="submit">Set Admin Password</button>
</form>
</div>
</div>
<script>
document.getElementById('setupForm').addEventListener('submit', function (e) {
const password = document.getElementById('adminPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (password !== confirmPassword) {
e.preventDefault();
document.getElementById('error-message').style.display = 'block';
}
});
</script>
</body>
</html>