login form okie

This commit is contained in:
2026-07-01 15:40:46 +07:00
parent e9909784c7
commit ba587a40cb
107 changed files with 5601 additions and 33 deletions
@@ -0,0 +1,44 @@
package com.sisvietnamvn.web.config;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import java.util.LinkedHashMap;
import java.util.Map;
public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final OAuth2AuthorizationRequestResolver defaultResolver;
public CustomOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, String authorizationRequestBaseUri) {
this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, authorizationRequestBaseUri);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
return customizeAuthorizationRequest(req);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
return customizeAuthorizationRequest(req);
}
private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest req) {
if (req == null) {
return null;
}
Map<String, Object> additionalParameters = new LinkedHashMap<>(req.getAdditionalParameters());
additionalParameters.put("prompt", "select_account");
return OAuth2AuthorizationRequest.from(req)
.additionalParameters(additionalParameters)
.build();
}
}
@@ -0,0 +1,40 @@
package com.sisvietnamvn.web.config;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.repository.AuthorityRepository;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class RoleSeeder implements CommandLineRunner {
private final AuthorityRepository authorityRepository;
public RoleSeeder(AuthorityRepository authorityRepository) {
this.authorityRepository = authorityRepository;
}
@Override
public void run(String... args) {
List<String> roles = Arrays.asList(
AuthoritiesConstants.EDITOR,
AuthoritiesConstants.AUTHOR,
AuthoritiesConstants.CONTRIBUTOR,
AuthoritiesConstants.SUBSCRIBER,
AuthoritiesConstants.PRE_AUTH_2FA
);
for (String roleName : roles) {
if (authorityRepository.findById(roleName).isEmpty()) {
Authority authority = new Authority();
authority.setName(roleName);
authorityRepository.save(authority);
System.out.println("Seeded role: " + roleName);
}
}
}
}
@@ -3,6 +3,7 @@ package com.sisvietnamvn.web.config;
import static org.springframework.security.config.Customizer.withDefaults;
import com.sisvietnamvn.web.security.*;
import com.sisvietnamvn.web.security.oauth2.CustomOidcUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@@ -19,6 +20,10 @@ import org.springframework.security.web.SecurityFilterChain;
import tech.jhipster.config.JHipsterConstants;
import tech.jhipster.config.JHipsterProperties;
import com.sisvietnamvn.web.security.CustomAuthenticationSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfiguration {
@@ -26,10 +31,14 @@ public class SecurityConfiguration {
private final Environment env;
private final JHipsterProperties jHipsterProperties;
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfiguration(Environment env, JHipsterProperties jHipsterProperties) {
public SecurityConfiguration(Environment env, JHipsterProperties jHipsterProperties, CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler, ClientRegistrationRepository clientRegistrationRepository) {
this.env = env;
this.jHipsterProperties = jHipsterProperties;
this.customAuthenticationSuccessHandler = customAuthenticationSuccessHandler;
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Bean
@@ -38,18 +47,18 @@ public class SecurityConfiguration {
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
public SecurityFilterChain filterChain(HttpSecurity http, CustomOidcUserService customOidcUserService) {
http.cors(withDefaults())
.csrf(csrf -> csrf.disable())
.csrf(withDefaults())
.authorizeHttpRequests(authz ->
// prettier-ignore
authz
.requestMatchers(HttpMethod.GET, "/", "/about", "/flex-finish", "/tin-tuc", "/tin-tuc/**", "/lien-he",
"/manage/**", "/css/**", "/images/**", "/js/**", "/UMass*/**", "/Undergraduate*/**",
"/manage/login", "/css/**", "/images/**", "/js/**", "/vendor/**", "/fonts/**", "/login-assets/**", "/UMass*/**", "/Undergraduate*/**",
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/api/manage/snippets/**", "/page/**", "/news/article/**", "/post/**", "/error",
"/about-us", "/specialty", "/doctor", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us")
.permitAll()
.requestMatchers(HttpMethod.POST, "/manage/**", "/api/manage/media/upload").permitAll()
.requestMatchers(HttpMethod.POST, "/api/manage/media/upload").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/authenticate").permitAll()
.requestMatchers(HttpMethod.GET, "/api/authenticate").permitAll()
@@ -63,11 +72,30 @@ public class SecurityConfiguration {
.requestMatchers("/management/health/**").permitAll()
.requestMatchers("/management/info").permitAll()
.requestMatchers("/management/prometheus").permitAll()
.requestMatchers("/2fa/**").hasAuthority(AuthoritiesConstants.PRE_AUTH_2FA)
.requestMatchers("/manage/**").hasAnyAuthority(AuthoritiesConstants.ADMIN, AuthoritiesConstants.EDITOR, AuthoritiesConstants.AUTHOR, AuthoritiesConstants.CONTRIBUTOR, AuthoritiesConstants.SUBSCRIBER, AuthoritiesConstants.USER)
.requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()))
.formLogin(form -> form
.loginPage("/manage/login")
.successHandler(customAuthenticationSuccessHandler)
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/manage/logout")
.logoutSuccessUrl("/manage/login?logout")
.permitAll()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/manage/login")
.successHandler(customAuthenticationSuccessHandler)
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(new CustomOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI))
)
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(customOidcUserService)
)
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) {
http.authorizeHttpRequests(authz -> authz.requestMatchers("/h2-console/**").permitAll())
@@ -7,10 +7,6 @@ import org.springframework.web.bind.annotation.GetMapping;
public class HomeController {
@GetMapping("/")
public String index() {
return "index";
}
// Trang Flex Finish (MỚI THÊM)
@GetMapping("/flex-finish")
@@ -46,6 +46,11 @@ public class PageController {
return renderPage(pageService.findBySlug(slug), model);
}
@GetMapping("/")
public String getHome(Model model) {
return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.HOME), model);
}
@GetMapping("/about-us")
public String getAboutUs(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.ABOUT_US), model); }
@@ -0,0 +1,176 @@
package com.sisvietnamvn.web.controller;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.repository.UserRepository;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.security.DomainUserDetailsService;
import dev.samstevens.totp.code.CodeGenerator;
import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import dev.samstevens.totp.code.DefaultCodeVerifier;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
import dev.samstevens.totp.secret.DefaultSecretGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import dev.samstevens.totp.time.TimeProvider;
import dev.samstevens.totp.util.Utils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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 org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Controller
@RequestMapping("/2fa")
public class TwoFactorController {
private final UserRepository userRepository;
public TwoFactorController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping
public String show2FaForm(Authentication authentication) {
if (authentication == null || !authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals(AuthoritiesConstants.PRE_AUTH_2FA))) {
return "redirect:/manage/login";
}
String username = authentication.getName();
Optional<User> userOpt = userRepository.findOneByLogin(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
if (!user.isUsing2FA()) {
return "redirect:/2fa/setup";
}
}
return "manage/2fa/verify";
}
@PostMapping
public String verify2Fa(@RequestParam("code") String code, Authentication authentication, RedirectAttributes redirectAttributes) {
String username = authentication.getName();
Optional<User> userOpt = userRepository.findOneWithAuthoritiesByLogin(username);
if (userOpt.isEmpty()) {
return "redirect:/manage/login";
}
User user = userOpt.get();
if (verifyCode(user.getTotpSecret(), code)) {
upgradeSecurityContext(user, authentication);
return "redirect:/manage";
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid 2FA code. Please try again.");
return "redirect:/2fa";
}
}
@GetMapping("/setup")
public String showSetupForm(Authentication authentication, Model model) {
String username = authentication.getName();
Optional<User> userOpt = userRepository.findOneByLogin(username);
if (userOpt.isEmpty()) {
return "redirect:/manage/login";
}
User user = userOpt.get();
if (user.isUsing2FA()) {
return "redirect:/2fa";
}
if (user.getTotpSecret() == null) {
SecretGenerator secretGenerator = new DefaultSecretGenerator();
user.setTotpSecret(secretGenerator.generate());
userRepository.save(user);
}
try {
QrData data = new QrData.Builder()
.label(user.getEmail() != null ? user.getEmail() : user.getLogin())
.secret(user.getTotpSecret())
.issuer("SIS Vietnam")
.algorithm(dev.samstevens.totp.code.HashingAlgorithm.SHA1)
.digits(6)
.period(30)
.build();
QrGenerator generator = new ZxingPngQrGenerator();
byte[] imageData = generator.generate(data);
String mimeType = generator.getImageMimeType();
String dataUri = Utils.getDataUriForImage(imageData, mimeType);
model.addAttribute("qrCode", dataUri);
model.addAttribute("secret", user.getTotpSecret());
} catch (QrGenerationException e) {
model.addAttribute("errorMessage", "Failed to generate QR Code.");
}
return "manage/2fa/setup";
}
@PostMapping("/setup")
public String verifySetup(@RequestParam("code") String code, Authentication authentication, RedirectAttributes redirectAttributes) {
String username = authentication.getName();
Optional<User> userOpt = userRepository.findOneWithAuthoritiesByLogin(username);
if (userOpt.isEmpty()) {
return "redirect:/manage/login";
}
User user = userOpt.get();
if (verifyCode(user.getTotpSecret(), code)) {
user.setUsing2FA(true);
userRepository.save(user);
upgradeSecurityContext(user, authentication);
return "redirect:/manage";
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid 2FA code. Please try again.");
return "redirect:/2fa/setup";
}
}
private boolean verifyCode(String secret, String code) {
TimeProvider timeProvider = new SystemTimeProvider();
CodeGenerator codeGenerator = new DefaultCodeGenerator();
CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
return verifier.isValidCode(secret, code);
}
private void upgradeSecurityContext(User user, Authentication originalAuthentication) {
List<GrantedAuthority> authorities = user.getAuthorities().stream()
.map(Authority::getName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
DomainUserDetailsService.UserWithId userWithId = new DomainUserDetailsService.UserWithId(
user.getLogin(),
user.getPassword(),
authorities,
user.getId()
);
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
userWithId, originalAuthentication.getCredentials(), authorities);
SecurityContextHolder.getContext().setAuthentication(newAuth);
}
}
@@ -12,6 +12,8 @@ import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* Controller for managing post Categories in the admin panel.
@@ -19,6 +21,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
*/
@Controller
@RequestMapping("/manage/posts/categories")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManageCategoryController {
private static final Logger LOG = LoggerFactory.getLogger(ManageCategoryController.class);
@@ -1,8 +1,10 @@
package com.sisvietnamvn.web.controller.manage;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* Controller for the admin dashboard index page.
@@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
*/
@Controller
@RequestMapping("/manage")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\", \"" + AuthoritiesConstants.SUBSCRIBER + "\", \"" + AuthoritiesConstants.USER + "\")")
public class ManageDashboardController {
@GetMapping({"", "/"})
@@ -0,0 +1,18 @@
package com.sisvietnamvn.web.controller.manage;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Controller for the admin login page.
*/
@Controller
@RequestMapping("/manage/login")
public class ManageLoginController {
@GetMapping
public String login() {
return "manage/login";
}
}
@@ -13,13 +13,15 @@ import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* Controller for managing CMS Pages in the admin panel.
* Provides CRUD operations for static pages.
*/
@Controller
@RequestMapping("/manage/pages")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManagePageController {
private static final Logger LOG = LoggerFactory.getLogger(ManagePageController.class);
@@ -62,6 +64,7 @@ public class ManagePageController {
model.addAttribute("page", page);
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("pageTypes", com.sisvietnamvn.web.domain.PageType.values());
model.addAttribute("layouts", com.sisvietnamvn.web.domain.PageLayout.values());
model.addAttribute("isNew", true);
return "manage/pages/form";
}
@@ -78,6 +81,7 @@ public class ManagePageController {
if (bindingResult.hasErrors()) {
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("pageTypes", com.sisvietnamvn.web.domain.PageType.values());
model.addAttribute("layouts", com.sisvietnamvn.web.domain.PageLayout.values());
model.addAttribute("isNew", true);
return "manage/pages/form";
}
@@ -100,6 +104,7 @@ public class ManagePageController {
model.addAttribute("page", pageOptional.get());
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("pageTypes", com.sisvietnamvn.web.domain.PageType.values());
model.addAttribute("layouts", com.sisvietnamvn.web.domain.PageLayout.values());
model.addAttribute("isNew", false);
return "manage/pages/form";
}
@@ -117,6 +122,7 @@ public class ManagePageController {
if (bindingResult.hasErrors()) {
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("pageTypes", com.sisvietnamvn.web.domain.PageType.values());
model.addAttribute("layouts", com.sisvietnamvn.web.domain.PageLayout.values());
model.addAttribute("isNew", false);
return "manage/pages/form";
}
@@ -18,6 +18,8 @@ import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* Controller for managing blog Posts in the admin panel.
@@ -25,6 +27,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
*/
@Controller
@RequestMapping("/manage/posts")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\")")
public class ManagePostController {
private static final Logger LOG = LoggerFactory.getLogger(ManagePostController.class);
@@ -0,0 +1,111 @@
package com.sisvietnamvn.web.controller.manage;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.service.UserService;
import com.sisvietnamvn.web.service.dto.AdminUserDTO;
import jakarta.validation.Valid;
/**
* Controller for managing the logged-in User's Profile in the admin panel.
*/
@Controller
@RequestMapping("/manage/profile")
@PreAuthorize("isAuthenticated()")
public class ManageProfileController {
private static final Logger LOG = LoggerFactory.getLogger(ManageProfileController.class);
private final UserService userService;
private final HookManager hookManager;
public ManageProfileController(UserService userService, HookManager hookManager) {
this.userService = userService;
this.hookManager = hookManager;
}
@ModelAttribute
public void adminInit() {
hookManager.doAction("admin_init");
}
/**
* GET /manage/profile : view the current user's profile form.
*/
@GetMapping
public String showProfileForm(Model model, RedirectAttributes redirectAttributes) {
LOG.debug("Request to show profile form");
Optional<User> userOptional = userService.getUserWithAuthorities();
if (userOptional.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "User session not found.");
return "redirect:/manage";
}
model.addAttribute("userDTO", new AdminUserDTO(userOptional.get()));
return "manage/users/profile";
}
/**
* POST /manage/profile/update : update basic information for the current user.
*/
@PostMapping("/update")
public String updateProfile(@Valid @ModelAttribute("userDTO") AdminUserDTO userDTO,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to update current user Profile : {}", userDTO);
if (bindingResult.hasErrors()) {
return "manage/users/profile";
}
try {
userService.updateUser(userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail(), userDTO.getLangKey(), userDTO.getImageUrl());
redirectAttributes.addFlashAttribute("successMessage", "Profile updated successfully!");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error updating profile: " + e.getMessage());
}
return "redirect:/manage/profile";
}
/**
* POST /manage/profile/password : change the current user's password.
*/
@PostMapping("/password")
public String changePassword(@RequestParam("currentPassword") String currentPassword,
@RequestParam("newPassword") String newPassword,
@RequestParam("confirmPassword") String confirmPassword,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to change password for current user");
if (!newPassword.equals(confirmPassword)) {
redirectAttributes.addFlashAttribute("errorPasswordMessage", "New password and confirm password do not match!");
return "redirect:/manage/profile";
}
try {
userService.changePassword(currentPassword, newPassword);
redirectAttributes.addFlashAttribute("successPasswordMessage", "Password changed successfully!");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorPasswordMessage", "Incorrect current password or error changing password.");
}
return "redirect:/manage/profile";
}
}
@@ -12,6 +12,8 @@ import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* Controller for managing post Tags in the admin panel.
@@ -19,6 +21,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
*/
@Controller
@RequestMapping("/manage/posts/tags")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\")")
public class ManageTagController {
private static final Logger LOG = LoggerFactory.getLogger(ManageTagController.class);
@@ -0,0 +1,179 @@
package com.sisvietnamvn.web.controller.manage;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.sisvietnamvn.web.config.Constants;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.UserService;
import com.sisvietnamvn.web.service.dto.AdminUserDTO;
import jakarta.validation.Valid;
/**
* Controller for managing Users in the admin panel.
*/
@Controller
@RequestMapping("/manage/users")
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
public class ManageUserController {
private static final Logger LOG = LoggerFactory.getLogger(ManageUserController.class);
private final UserService userService;
private final HookManager hookManager;
public ManageUserController(UserService userService, HookManager hookManager) {
this.userService = userService;
this.hookManager = hookManager;
}
@ModelAttribute
public void adminInit() {
hookManager.doAction("admin_init");
}
/**
* GET /manage/users : get all users.
*/
@GetMapping
public String listUsers(@PageableDefault(size = 20, sort = "login") Pageable pageable, Model model) {
LOG.debug("REST request to get all User for an admin");
Page<AdminUserDTO> page = userService.getAllManagedUsers(pageable);
model.addAttribute("users", page.getContent());
model.addAttribute("page", page);
return "manage/users/list";
}
/**
* GET /manage/users/new : show create form.
*/
@GetMapping("/new")
public String showCreateForm(Model model) {
LOG.debug("Request to show create user form");
AdminUserDTO userDTO = new AdminUserDTO();
userDTO.setLangKey(Constants.DEFAULT_LANGUAGE);
model.addAttribute("userDTO", userDTO);
model.addAttribute("authorities", userService.getAuthorities());
model.addAttribute("isNew", true);
return "manage/users/form";
}
/**
* POST /manage/users : create a new user.
*/
@PostMapping
public String createUser(@Valid @ModelAttribute("userDTO") AdminUserDTO userDTO,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to create User : {}", userDTO);
if (bindingResult.hasErrors()) {
model.addAttribute("authorities", userService.getAuthorities());
model.addAttribute("isNew", true);
return "manage/users/form";
}
if (userDTO.getId() != null) {
redirectAttributes.addFlashAttribute("errorMessage", "A new user cannot already have an ID");
return "redirect:/manage/users/new";
}
// Ensure email login constraint (optional depending on jhipster config, but good practice)
if (userDTO.getLogin() != null) {
userDTO.setLogin(userDTO.getLogin().toLowerCase());
}
try {
User newUser = userService.createUser(userDTO);
redirectAttributes.addFlashAttribute("successMessage", "User created successfully! Login: " + newUser.getLogin());
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error creating user: " + e.getMessage());
return "redirect:/manage/users/new";
}
return "redirect:/manage/users";
}
/**
* GET /manage/users/{login}/edit : show edit form.
*/
@GetMapping("/{login}/edit")
public String showEditForm(@PathVariable String login, Model model, RedirectAttributes redirectAttributes) {
LOG.debug("Request to show edit form for User : {}", login);
Optional<User> userOptional = userService.getUserWithAuthoritiesByLogin(login);
if (userOptional.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "User not found.");
return "redirect:/manage/users";
}
model.addAttribute("userDTO", new AdminUserDTO(userOptional.get()));
model.addAttribute("authorities", userService.getAuthorities());
model.addAttribute("isNew", false);
return "manage/users/form";
}
/**
* POST /manage/users/{login} : update a user.
*/
@PostMapping("/{login}")
public String updateUser(@PathVariable String login,
@Valid @ModelAttribute("userDTO") AdminUserDTO userDTO,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to update User : {}", userDTO);
if (bindingResult.hasErrors()) {
model.addAttribute("authorities", userService.getAuthorities());
model.addAttribute("isNew", false);
return "manage/users/form";
}
Optional<User> existingUser = userService.getUserWithAuthoritiesByLogin(login);
if (existingUser.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "User not found.");
return "redirect:/manage/users";
}
userDTO.setId(existingUser.get().getId());
try {
userService.updateUser(userDTO);
redirectAttributes.addFlashAttribute("successMessage", "User updated successfully!");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error updating user: " + e.getMessage());
return "redirect:/manage/users/" + login + "/edit";
}
return "redirect:/manage/users";
}
/**
* POST /manage/users/{login}/delete : delete a user.
*/
@PostMapping("/{login}/delete")
public String deleteUser(@PathVariable String login, RedirectAttributes redirectAttributes) {
LOG.debug("Request to delete User : {}", login);
userService.deleteUser(login);
redirectAttributes.addFlashAttribute("successMessage", "User deleted successfully!");
return "redirect:/manage/users";
}
}
@@ -15,9 +15,12 @@ import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
@RestController
@RequestMapping("/api/manage/media")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\")")
public class MediaController {
private final Logger log = LoggerFactory.getLogger(MediaController.class);
@@ -10,12 +10,15 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* REST controller for fetching predefined HTML snippets to be previewed in Editor.js.
*/
@RestController
@RequestMapping("/api/manage/snippets")
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
public class SnippetController {
private static final Logger LOG = LoggerFactory.getLogger(SnippetController.class);
@@ -53,6 +53,11 @@ public class Page extends AbstractAuditingEntity<Long> {
@Column(name = "display_order")
private Integer displayOrder = 0;
@NotNull
@Enumerated(EnumType.STRING)
@Column(name = "layout", length = 20, nullable = false)
private PageLayout layout = PageLayout.STANDARD;
// --- Getters and Setters ---
@Override
@@ -120,6 +125,14 @@ public class Page extends AbstractAuditingEntity<Long> {
this.displayOrder = displayOrder;
}
public PageLayout getLayout() {
return layout;
}
public void setLayout(PageLayout layout) {
this.layout = layout;
}
// --- equals, hashCode, toString ---
@Override
@@ -0,0 +1,10 @@
package com.sisvietnamvn.web.domain;
/**
* Enumeration for Page Layout options.
*/
public enum PageLayout {
STANDARD,
SIDEBAR,
FULL_WIDTH
}
@@ -85,6 +85,12 @@ public class User extends AbstractAuditingEntity<Long> implements Serializable {
@Column(name = "reset_date")
private Instant resetDate = null;
@Column(name = "using_2fa", nullable = false)
private boolean using2FA = false;
@Column(name = "totp_secret")
private String totpSecret;
@JsonIgnore
@ManyToMany
@JoinTable(
@@ -185,6 +191,22 @@ public class User extends AbstractAuditingEntity<Long> implements Serializable {
this.resetDate = resetDate;
}
public boolean isUsing2FA() {
return using2FA;
}
public void setUsing2FA(boolean using2FA) {
this.using2FA = using2FA;
}
public String getTotpSecret() {
return totpSecret;
}
public void setTotpSecret(String totpSecret) {
this.totpSecret = totpSecret;
}
public String getLangKey() {
return langKey;
}
@@ -7,9 +7,19 @@ public final class AuthoritiesConstants {
public static final String ADMIN = "ROLE_ADMIN";
public static final String EDITOR = "ROLE_EDITOR";
public static final String AUTHOR = "ROLE_AUTHOR";
public static final String CONTRIBUTOR = "ROLE_CONTRIBUTOR";
public static final String SUBSCRIBER = "ROLE_SUBSCRIBER";
public static final String USER = "ROLE_USER";
public static final String ANONYMOUS = "ROLE_ANONYMOUS";
public static final String PRE_AUTH_2FA = "ROLE_PRE_AUTH_2FA";
private AuthoritiesConstants() {}
}
@@ -0,0 +1,50 @@
package com.sisvietnamvn.web.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
boolean isPreAuth = authorities.stream()
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(AuthoritiesConstants.PRE_AUTH_2FA));
if (isPreAuth) {
redirectStrategy.sendRedirect(request, response, "/2fa");
return;
}
boolean isWordPressRole = authorities.stream()
.anyMatch(grantedAuthority -> {
String role = grantedAuthority.getAuthority();
return role.equals(AuthoritiesConstants.ADMIN) ||
role.equals(AuthoritiesConstants.EDITOR) ||
role.equals(AuthoritiesConstants.AUTHOR) ||
role.equals(AuthoritiesConstants.CONTRIBUTOR) ||
role.equals(AuthoritiesConstants.SUBSCRIBER) ||
role.equals(AuthoritiesConstants.USER);
});
if (isWordPressRole) {
redirectStrategy.sendRedirect(request, response, "/manage");
} else {
redirectStrategy.sendRedirect(request, response, "/");
}
}
}
@@ -79,10 +79,12 @@ public class DomainUserDetailsService implements UserDetailsService {
}
public static UserWithId fromUser(User user) {
List<GrantedAuthority> grantedAuthorities = Collections.singletonList(new SimpleGrantedAuthority(AuthoritiesConstants.PRE_AUTH_2FA));
return new UserWithId(
user.getLogin(),
user.getPassword(),
user.getAuthorities().stream().map(Authority::getName).map(SimpleGrantedAuthority::new).toList(),
grantedAuthorities,
user.getId()
);
}
@@ -0,0 +1,64 @@
package com.sisvietnamvn.web.security.oauth2;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class CustomOidcUserService extends OidcUserService {
private final UserRepository userRepository;
public CustomOidcUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
String email = oidcUser.getEmail();
System.out.println("===============================");
System.out.println("CustomOidcUserService is called!");
System.out.println("Google Email: " + email);
System.out.println("===============================");
if (email == null) {
throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token", "Email not found from OAuth2 provider", ""));
}
// Map the Google email to an existing User in the database (Option A)
Optional<User> userOptional = userRepository.findOneWithAuthoritiesByEmailIgnoreCase(email);
if (userOptional.isEmpty()) {
throw new OAuth2AuthenticationException(new OAuth2Error("user_not_found", "User not found in system. Please contact administrator.", ""));
}
User user = userOptional.get();
if (!user.isActivated()) {
throw new OAuth2AuthenticationException(new OAuth2Error("user_not_activated", "User is not activated.", ""));
}
List<GrantedAuthority> authorities = user.getAuthorities().stream()
.map(Authority::getName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
}
}