Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.openpodcastapi.opa.advice;

import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;

import java.security.Principal;

@Log4j2
@ControllerAdvice
public class GlobalModelAttributeAdvice {

@ModelAttribute
public void addAuthenticationFlag(Model model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAuthenticated = authentication != null && authentication.isAuthenticated()
&& !"anonymousUser".equals(authentication.getPrincipal());
model.addAttribute("isAuthenticated", isAuthenticated);
}

@ModelAttribute
public void addUserDetails(Principal principal, Model model) {
String username = principal != null ? principal.getName() : "Guest";
model.addAttribute("username", username);
}
}
20 changes: 14 additions & 6 deletions src/main/java/org/openpodcastapi/opa/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

Expand All @@ -19,13 +18,22 @@ public class SecurityConfig {
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/docs").permitAll()
.requestMatchers("/", "/login", "/logout-confirm", "/register", "/docs", "/css/**", "/js/**", "/images/**", "/favicon.ico").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/users/**").permitAll()
.anyRequest().authenticated())
.csrf(AbstractHttpConfigurer::disable)
.formLogin(Customizer.withDefaults())
.logout(Customizer.withDefaults());
.formLogin(login -> login
.loginPage("/login")
.defaultSuccessUrl("/home", true)
.failureUrl("/login?error=true")
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
)
.csrf(Customizer.withDefaults());
return http.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TemplateConfig {
@Bean
public LayoutDialect layoutDialect() {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/openpodcastapi/opa/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.openpodcastapi.opa.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.openpodcastapi.opa.ui.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.openpodcastapi.opa.user.dto.CreateUserDto;
import org.openpodcastapi.opa.user.service.UserService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@Log4j2
@RequiredArgsConstructor
public class AuthController {
private static final String USER_REQUEST_ATTRIBUTE = "createUserRequest";
private static final String REGISTER_TEMPLATE = "auth/register";
private final UserService userService;

// === Login page ===
@GetMapping("/login")
public String loginPage(@RequestParam(value = "error", required = false) String error,
Model model) {
if (error != null) {
model.addAttribute("loginError", true);
}
return "auth/login";
}

// === Logout confirmation page ===
@GetMapping("/logout-confirm")
public String logoutPage() {
return "auth/logout";
}

// === Registration page ===
@GetMapping("/register")
public String getRegister(Model model) {
model.addAttribute(USER_REQUEST_ATTRIBUTE, new CreateUserDto("", "", ""));
return REGISTER_TEMPLATE;
}

// === Registration POST handler ===
@PostMapping("/register")
public String processRegistration(
@Valid @ModelAttribute CreateUserDto createUserRequest,
BindingResult result,
Model model
) {
if (result.hasErrors()) {
model.addAttribute(USER_REQUEST_ATTRIBUTE, createUserRequest);
return REGISTER_TEMPLATE;
}

try {
userService.createAndPersistUser(createUserRequest);
} catch (DataIntegrityViolationException _) {
result.rejectValue("username", "", "Username or email already exists");
model.addAttribute(USER_REQUEST_ATTRIBUTE, createUserRequest);
return REGISTER_TEMPLATE;
}

return "redirect:/login?registered";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.openpodcastapi.opa.ui.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@RequiredArgsConstructor
@Log4j2
public class HomeController {

@GetMapping("/")
public String getLandingPage() {
return "landing";
}

@GetMapping("/home")
public String getHomePage(Authentication auth) {
if (auth != null && !auth.isAuthenticated()) {
return "redirect:/login";
}
return "home";
}
}
42 changes: 42 additions & 0 deletions src/main/resources/templates/auth/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!-- auth/login.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}">
<head>
<title layout:fragment="title">Log in - Open Podcast API</title>
</head>
<body>
<div layout:fragment="content" th:remove>
<h1 class="title">Log in</h1>
<div>
<div th:if="${loginError}" class="has-text-danger">
Invalid username or password.
</div>

<form th:action="@{/login}" method="post">
<div class="field">
<label for="username" class="label">Username</label>
<div class="control">
<input autocomplete="username" id="username" name="username" class="input" type="text"
placeholder="Username">
</div>
</div>
<div class="field">
<label for="password" class="label">Password</label>
<div class="control">
<input autocomplete="current-password" id="password" name="password" class="input" type="password"
placeholder="Password">
</div>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="field">
<div class="control">
<button type="submit" name="submit" class="button is-success">Log in</button>
</div>
</div>
</form>
</div>
</div>
</body>
</html>
22 changes: 22 additions & 0 deletions src/main/resources/templates/auth/logout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- auth/logout.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}">
<head>
<title layout:fragment="title">Log out - Open Podcast API</title>
</head>
<body>

<div layout:fragment="content" th:remove>
<h1>Are you sure you want to log out?</h1>
<div>
<form th:action="@{/logout}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button type="submit" class="button is-danger">Log out</button>
</form>
</div>
</div>

</body>
</html>
51 changes: 51 additions & 0 deletions src/main/resources/templates/auth/register.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!-- auth/register.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}">
<head>
<title layout:fragment="title">Sign up - Open Podcast API</title>
</head>
<body>
<div layout:fragment="content" th:remove>
<h1>Sign up</h1>
<form
th:object="${createUserRequest}"
th:action="@{/register}"
method="post">
<div class="field">
<label for="username" class="label">Username</label>
<div class="control">
<input id="username" autocomplete="username" name="username" th:field="*{username}" class="input"
type="text"
placeholder="Username">
<div th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></div>
</div>
</div>
<div class="field">
<label for="email" class="label">Email</label>
<div class="control">
<input id="email" autocomplete="email" name="email" th:field="*{email}" class="input" type="email"
placeholder="address@example.com">
<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div>
</div>
</div>
<div class="field">
<label for="password" class="label">Password</label>
<div class="control">
<input id="password" autocomplete="new-password" name="password" th:field="*{password}"
class="input" type="password"
placeholder="Password">
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>
</div>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="field">
<div class="control">
<input type="submit" name="submit" class="button is-success" value="Submit"/>
</div>
</div>
</form>
</div>
</body>
</html>
19 changes: 19 additions & 0 deletions src/main/resources/templates/home.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- home.html -->
<!--/*@thymesVar id="username" type="String"*/-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}">
<head>
<title layout:fragment="title">Home - Open Podcast API</title>
</head>
<body>

<div layout:fragment="content" th:remove>
<article class="content">
<h1>Welcome, <span th:text="${username}"></span>!</h1>
</article>
</div>

</body>
</html>
47 changes: 47 additions & 0 deletions src/main/resources/templates/landing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!-- home.html -->
<!--/*@thymesVar id="username" type="String"*/-->
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}">
<head>
<title layout:fragment="title">Open Podcast API</title>
</head>
<body>

<div layout:fragment="content">
<h1>Welcome to the Open Podcast API reference server</h1>
<p>The Open Podcast API is an initiative aiming to provide a feature-complete synchronization API specification for
podcast (web) apps and user-focused servers.</p>
<h2>Our goals</h2>
<div class="columns">
<div class="column">
<div class="card">
<div class="card-content">
<h3 class="title is-4">For users</h3>
<ul>
<li>Synchronize subscriptions, listening progress, favorites, queues, and more</li>
<li>Support multiple apps and online services</li>
<li>Enable users to easily switch between providers without losing any information</li>
</ul>
</div>
</div>
</div>
<div class="column">
<div class="card">
<div class="card-content">
<h3 class="title is-4">For developers</h3>
<ul>
<li>Write clear and comprehensive documentation for features and behaviors</li>
<li>Create reliable specifications that are decentralization-ready and easy to implement</li>
<li>Provide a full <a href="https://spec.openapis.org/oas/latest" rel="noreferrer"
target="blank">OpenAPI specification</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>

</body>
</html>
25 changes: 25 additions & 0 deletions src/main/resources/templates/layouts/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!-- layouts/main.html -->
<!DOCTYPE html>
<html class="no-js"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" lang="en">
<head>
<meta charset="UTF-8"/>
<title id="page-title" layout:fragment="title">Open Podcast API</title>
<th:block layout:fragment="head-resources"></th:block>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha1/dist/htmx.min.js"></script>
</head>
<body>
<div id="app" class="columns section" style="min-height: 100vh;">
<!-- Sidebar (hidden on mobile) -->
<aside th:replace="~{layouts/sidebar :: sidebar}"></aside>

<!-- Main content -->
<main class="container content" id="main-content">
<div layout:fragment="content"></div>
</main>
</div>
</body>
</html>
Loading