diff --git a/src/main/java/org/openpodcastapi/opa/advice/GlobalModelAttributeAdvice.java b/src/main/java/org/openpodcastapi/opa/advice/GlobalModelAttributeAdvice.java new file mode 100644 index 0000000..bc8b8ec --- /dev/null +++ b/src/main/java/org/openpodcastapi/opa/advice/GlobalModelAttributeAdvice.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/org/openpodcastapi/opa/config/SecurityConfig.java b/src/main/java/org/openpodcastapi/opa/config/SecurityConfig.java index f13485e..e4363f1 100644 --- a/src/main/java/org/openpodcastapi/opa/config/SecurityConfig.java +++ b/src/main/java/org/openpodcastapi/opa/config/SecurityConfig.java @@ -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; @@ -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(); } diff --git a/src/main/java/org/openpodcastapi/opa/config/TemplateConfig.java b/src/main/java/org/openpodcastapi/opa/config/TemplateConfig.java index 1a5a6ba..6e442e4 100644 --- a/src/main/java/org/openpodcastapi/opa/config/TemplateConfig.java +++ b/src/main/java/org/openpodcastapi/opa/config/TemplateConfig.java @@ -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() { diff --git a/src/main/java/org/openpodcastapi/opa/config/WebConfig.java b/src/main/java/org/openpodcastapi/opa/config/WebConfig.java index 227128b..e98c3f3 100644 --- a/src/main/java/org/openpodcastapi/opa/config/WebConfig.java +++ b/src/main/java/org/openpodcastapi/opa/config/WebConfig.java @@ -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) { diff --git a/src/main/java/org/openpodcastapi/opa/ui/controller/AuthController.java b/src/main/java/org/openpodcastapi/opa/ui/controller/AuthController.java new file mode 100644 index 0000000..90cde9a --- /dev/null +++ b/src/main/java/org/openpodcastapi/opa/ui/controller/AuthController.java @@ -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"; + } +} diff --git a/src/main/java/org/openpodcastapi/opa/ui/controller/HomeController.java b/src/main/java/org/openpodcastapi/opa/ui/controller/HomeController.java new file mode 100644 index 0000000..23c9f94 --- /dev/null +++ b/src/main/java/org/openpodcastapi/opa/ui/controller/HomeController.java @@ -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"; + } +} diff --git a/src/main/resources/templates/auth/login.html b/src/main/resources/templates/auth/login.html new file mode 100644 index 0000000..398474d --- /dev/null +++ b/src/main/resources/templates/auth/login.html @@ -0,0 +1,42 @@ + + + +
+The Open Podcast API is an initiative aiming to provide a feature-complete synchronization API specification for + podcast (web) apps and user-focused servers.
+