diff --git a/.gitignore b/.gitignore index c5c909d..459af63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -backend/src/main/resources/application.properties -frontend/.env - -# Antigravity-kit -/.agent - -# Document -doc/ \ No newline at end of file +backend/src/main/resources/application.properties +frontend/.env + +# Antigravity-kit +/.agent + +# Document +docs/ diff --git a/backend/pom.xml b/backend/pom.xml index 729a692..f4dc7a8 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -64,7 +64,10 @@ org.springframework.boot spring-boot-starter-security - + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + io.jsonwebtoken diff --git a/backend/src/main/java/com/ticketrush/config/SecurityConfig.java b/backend/src/main/java/com/ticketrush/config/SecurityConfig.java index 9ab6b53..be9e6b3 100644 --- a/backend/src/main/java/com/ticketrush/config/SecurityConfig.java +++ b/backend/src/main/java/com/ticketrush/config/SecurityConfig.java @@ -1,19 +1,26 @@ package com.ticketrush.config; -import com.ticketrush.util.JwtAuthFilter; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +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.config.http.SessionCreationPolicy; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import jakarta.servlet.http.HttpServletResponse; import java.util.List; /** @@ -30,37 +37,40 @@ @RequiredArgsConstructor public class SecurityConfig { - private final JwtAuthFilter jwtAuthFilter; + @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") + private String jwkSetUri; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - // 1. Tắt CSRF (Stateless API không cần) - .csrf(AbstractHttpConfigurer::disable) + // 1. Tắt CSRF (Stateless API không cần) + .csrf(AbstractHttpConfigurer::disable) - // 2. Cấu hình CORS - .cors(cors -> cors.configurationSource(corsConfigurationSource())) + // 2. Cấu hình CORS + .cors(cors -> cors.configurationSource(corsConfigurationSource())) - // 3. Session policy: STATELESS — không tạo session phía server - .sessionManagement(session -> - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 3. Session policy: STATELESS — không tạo session phía server + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 4. Phân quyền route .authorizeHttpRequests(auth -> auth // Public routes — không cần đăng nhập - .requestMatchers("/", "/api/status", "/api/events/**", "/actuator/**").permitAll() + .requestMatchers("/", "/api/status", "/api/events/**", "/api/events", "/actuator/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") - // Chỉ ADMIN mới vào được các route /api/admin/** - // (Role được lấy từ JWT claim "app_metadata.role") - // .requestMatchers("/api/admin/**").hasRole("ADMIN") + // Chỉ ADMIN mới vào được các route /api/admin/** + // (Role được lấy từ JWT claim "app_metadata.role") + // .requestMatchers("/api/admin/**").hasRole("ADMIN") - // Mọi route còn lại đều yêu cầu JWT hợp lệ - .anyRequest().authenticated() - ) + // Mọi route còn lại đều yêu cầu JWT hợp lệ + .anyRequest().authenticated()) - // 5. Gắn JWT Filter vào trước filter mặc định của Spring - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + // 5. Sử dụng OAuth2 Resource Server để tự động verify JWT + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) + .authenticationEntryPoint((request, response, authException) -> { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); + })); return http.build(); } @@ -74,8 +84,8 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( - "http://localhost:5173", // Vite dev server - "http://localhost:3000" // Dự phòng nếu đổi port + "http://localhost:5173", // Vite dev server + "http://localhost:3000" // Dự phòng nếu đổi port )); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); @@ -86,4 +96,37 @@ public CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", config); return source; } + + /** + * Converter để trích xuất role từ JWT của Supabase. + * Supabase lưu role ở dạng: "app_metadata": { "role": "ADMIN" } + */ + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(jwt -> { + try { + java.util.Map appMetadata = jwt.getClaimAsMap("app_metadata"); + if (appMetadata != null && appMetadata.containsKey("role")) { + String role = (String) appMetadata.get("role"); + return List.of(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())); + } + } catch (Exception e) { + // Ignore if app_metadata is missing or malformed + } + return List.of(); + }); + return converter; + } + + /** + * Tùy chỉnh JwtDecoder để Spring Security sử dụng thuật toán ES256 + * (Mặc định Spring Security dùng RS256, nên sẽ bị lỗi khi Supabase dùng ES256) + */ + @Bean + public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) + .jwsAlgorithm(SignatureAlgorithm.ES256) + .build(); + } } diff --git a/backend/src/main/java/com/ticketrush/controller/EventController.java b/backend/src/main/java/com/ticketrush/controller/EventController.java index 762d462..0f5f615 100644 --- a/backend/src/main/java/com/ticketrush/controller/EventController.java +++ b/backend/src/main/java/com/ticketrush/controller/EventController.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.UUID; @RestController @RequestMapping("/api/events") @@ -19,4 +20,9 @@ public class EventController { public List getEvents() { return eventService.getAllEvents(); } + + @GetMapping("/{id}") + public EventResponse getEventById(@PathVariable UUID id) { + return eventService.getEventById(id); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/ticketrush/controller/UserController.java b/backend/src/main/java/com/ticketrush/controller/UserController.java new file mode 100644 index 0000000..2dccff6 --- /dev/null +++ b/backend/src/main/java/com/ticketrush/controller/UserController.java @@ -0,0 +1,48 @@ +package com.ticketrush.controller; + +import com.ticketrush.model.dto.UserProfileDto; +import com.ticketrush.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/profile") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @GetMapping + public ResponseEntity getProfile(@AuthenticationPrincipal Jwt jwt) { + UUID userId = UUID.fromString(jwt.getSubject()); + + UserProfileDto profile = userService.getProfile(userId); + + // If DB doesn't have it, fill basic info from JWT + if (profile.getEmail() == null) { + profile.setEmail(jwt.getClaimAsString("email")); + + // Supabase stores user_metadata in the token sometimes + if (jwt.getClaims().containsKey("user_metadata")) { + java.util.Map meta = jwt.getClaimAsMap("user_metadata"); + if (meta.containsKey("full_name")) profile.setFullName((String) meta.get("full_name")); + } + } + + return ResponseEntity.ok(profile); + } + + @PutMapping + public ResponseEntity updateProfile(@AuthenticationPrincipal Jwt jwt, @RequestBody UserProfileDto dto) { + UUID userId = UUID.fromString(jwt.getSubject()); + String email = jwt.getClaimAsString("email"); + + UserProfileDto updatedProfile = userService.updateProfile(userId, email, dto); + return ResponseEntity.ok(updatedProfile); + } +} diff --git a/backend/src/main/java/com/ticketrush/model/dto/EventResponse.java b/backend/src/main/java/com/ticketrush/model/dto/EventResponse.java index f2d335b..1bb65a9 100644 --- a/backend/src/main/java/com/ticketrush/model/dto/EventResponse.java +++ b/backend/src/main/java/com/ticketrush/model/dto/EventResponse.java @@ -3,6 +3,7 @@ import lombok.Data; import java.util.List; import java.util.UUID; +import java.util.List; @Data public class EventResponse { @@ -13,6 +14,7 @@ public class EventResponse { private String coverImage; private String venueName; private String venueAddress; + private Integer maxTicketsPerOrder; // THÊM DÒNG NÀY: Để trả về danh sách suất diễn kèm theo private List showtimes; diff --git a/backend/src/main/java/com/ticketrush/model/dto/ShowtimeResponse.java b/backend/src/main/java/com/ticketrush/model/dto/ShowtimeResponse.java new file mode 100644 index 0000000..2bf146c --- /dev/null +++ b/backend/src/main/java/com/ticketrush/model/dto/ShowtimeResponse.java @@ -0,0 +1,13 @@ +package com.ticketrush.model.dto; + +import lombok.Data; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +public class ShowtimeResponse { + private UUID id; + private String name; + private LocalDateTime startTime; + private LocalDateTime endTime; +} diff --git a/backend/src/main/java/com/ticketrush/model/dto/UserProfileDto.java b/backend/src/main/java/com/ticketrush/model/dto/UserProfileDto.java new file mode 100644 index 0000000..05fbe40 --- /dev/null +++ b/backend/src/main/java/com/ticketrush/model/dto/UserProfileDto.java @@ -0,0 +1,14 @@ +package com.ticketrush.model.dto; + +import lombok.Data; +import java.time.LocalDate; + +@Data +public class UserProfileDto { + private String email; + private String fullName; + private String phoneNumber; + private String gender; + private LocalDate dateOfBirth; + private String avatarUrl; +} diff --git a/backend/src/main/java/com/ticketrush/model/entity/Event.java b/backend/src/main/java/com/ticketrush/model/entity/Event.java index 3eb14cc..dfccde9 100644 --- a/backend/src/main/java/com/ticketrush/model/entity/Event.java +++ b/backend/src/main/java/com/ticketrush/model/entity/Event.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.UUID; +import java.util.List; import jakarta.persistence.*; import lombok.Data; @@ -22,8 +23,8 @@ public class Event { private String coverImage; private String venueName; private String venueAddress; - private Integer maxTicketsPerOrder; private String category; + private Integer maxTicketsPerOrder; @ManyToOne @JoinColumn(name = "created_by") diff --git a/backend/src/main/java/com/ticketrush/model/entity/User.java b/backend/src/main/java/com/ticketrush/model/entity/User.java index 133720b..d63bcd7 100644 --- a/backend/src/main/java/com/ticketrush/model/entity/User.java +++ b/backend/src/main/java/com/ticketrush/model/entity/User.java @@ -20,6 +20,8 @@ public class User { private String role; private String gender; private LocalDate dateOfBirth; + private String avatarUrl; + private String phoneNumber; @OneToMany(mappedBy = "user") private List orders; diff --git a/backend/src/main/java/com/ticketrush/repository/UserRepository.java b/backend/src/main/java/com/ticketrush/repository/UserRepository.java index fbd4047..2f4e238 100644 --- a/backend/src/main/java/com/ticketrush/repository/UserRepository.java +++ b/backend/src/main/java/com/ticketrush/repository/UserRepository.java @@ -11,4 +11,4 @@ public interface UserRepository extends JpaRepository { // Tìm kiếm User theo Email (hữu ích cho việc kiểm tra trùng lặp hoặc đăng nhập) Optional findByEmail(String email); -} \ No newline at end of file +} diff --git a/backend/src/main/java/com/ticketrush/service/EventService.java b/backend/src/main/java/com/ticketrush/service/EventService.java index 43c39e3..1c9b9f1 100644 --- a/backend/src/main/java/com/ticketrush/service/EventService.java +++ b/backend/src/main/java/com/ticketrush/service/EventService.java @@ -1,6 +1,7 @@ package com.ticketrush.service; import com.ticketrush.model.dto.EventResponse; +import com.ticketrush.model.dto.ShowtimeResponse; import com.ticketrush.repository.EventRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -10,9 +11,11 @@ import com.ticketrush.model.entity.User; import com.ticketrush.repository.ShowtimeRepository; import com.ticketrush.repository.UserRepository; // Giả định đã tạo -import com.ticketrush.util.JwtAuthFilter; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; import java.util.ArrayList; import java.util.List; @@ -29,40 +32,67 @@ public class EventService { private final UserRepository userRepository; public List getAllEvents() { - return eventRepository.findAll().stream().map(event -> { - EventResponse dto = new EventResponse(); - dto.setId(event.getId()); - dto.setEventName(event.getEventName()); - dto.setDescription(event.getDescription()); - dto.setCategory(event.getCategory()); - dto.setCoverImage(event.getCoverImage()); - dto.setVenueName(event.getVenueName()); - dto.setVenueAddress(event.getVenueAddress()); + // Sử dụng method reference để code ngắn gọn và chuyên nghiệp + return eventRepository.findAll().stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + } + + // Hàm này team bạn có thể dùng chung cho cả Detail API + public EventResponse getEventById(java.util.UUID id) { + return eventRepository.findById(id) + .map(this::mapToResponse) + .orElseThrow(() -> new RuntimeException("Event not found")); + } + + // Hàm dùng chung để ánh xạ dữ liệu + private EventResponse mapToResponse(com.ticketrush.model.entity.Event event) { + EventResponse dto = new EventResponse(); + dto.setId(event.getId()); + dto.setEventName(event.getEventName()); + dto.setDescription(event.getDescription()); + dto.setCategory(event.getCategory()); + dto.setCoverImage(event.getCoverImage()); + dto.setVenueName(event.getVenueName()); + dto.setVenueAddress(event.getVenueAddress()); + + // LƯU Ý: Trong đoạn code Trung vừa gửi bên dưới đang THIẾU dòng này. + // Bạn cần giữ nó để "Số vé tối đa" hiện được ở Frontend + dto.setMaxTicketsPerOrder(event.getMaxTicketsPerOrder()); + + if (event.getShowtimes() != null) { dto.setShowtimes(event.getShowtimes().stream().map(showtime -> { - EventResponse.ShowtimeResponse showtimeDto = new EventResponse.ShowtimeResponse(); - showtimeDto.setId(showtime.getId()); - showtimeDto.setName(showtime.getName()); - showtimeDto.setStartTime(showtime.getStartTime()); - showtimeDto.setEndTime(showtime.getEndTime()); - return showtimeDto; + // Sử dụng class ShowtimeResponse mà team đã thống nhất + // Nếu team dùng Inner Class thì để là EventResponse.ShowtimeResponse + EventResponse.ShowtimeResponse sDto = new EventResponse.ShowtimeResponse(); + sDto.setId(showtime.getId()); + sDto.setName(showtime.getName()); + sDto.setStartTime(showtime.getStartTime()); + sDto.setEndTime(showtime.getEndTime()); + return sDto; }).collect(Collectors.toList())); - return dto; - }).collect(Collectors.toList()); + } + + return dto; } - @Transactional // Đảm bảo nếu lưu Showtime lỗi thì Event cũng sẽ bị hủy (Rollback) - public EventResponse createEvent(EventCreateRequest request) { // 1. Lấy ID của Admin từ Token thông qua hàm tiện ích bạn đã có - String adminId = JwtAuthFilter.getCurrentUserId(); - if (adminId == null) { - throw new RuntimeException("Bạn phải đăng nhập với quyền Admin để thực hiện hành động này!"); + // ── TẠO SỰ KIỆN MỚI (Dùng OAuth2 Resource Server) ── + @Transactional + public EventResponse createEvent(EventCreateRequest request) { + // 1. Lấy JWT từ SecurityContext (Thay thế cho JwtAuthFilter đã xóa) + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !(auth.getPrincipal() instanceof Jwt)) { + throw new RuntimeException("Xác thực không hợp lệ!"); } - UUID adminUuid = UUID.fromString(adminId); + Jwt jwt = (Jwt) auth.getPrincipal(); + // 2. Lấy User ID từ claim 'sub' của Supabase + String adminId = jwt.getSubject(); + + User admin = userRepository.findById(UUID.fromString(adminId)) + .orElseThrow(() -> new RuntimeException("Tài khoản Admin không tồn tại!")); - User admin = userRepository.findById(adminUuid) - .orElseThrow(() -> new RuntimeException("Tài khoản Admin không tồn tại trong hệ thống!")); - - // 3. Chuyển đổi DTO sang Entity Event và lưu vào DB + // 3. Lưu Event Event event = new Event(); event.setEventName(request.getEventName()); event.setDescription(request.getDescription()); @@ -71,25 +101,26 @@ public EventResponse createEvent(EventCreateRequest request) { // 1. Lấ event.setVenueName(request.getVenueName()); event.setVenueAddress(request.getVenueAddress()); event.setMaxTicketsPerOrder(request.getMaxTicketsPerOrder()); - event.setCreator(admin); // Gắn người tạo là Admin hiện tại + event.setCreator(admin); Event savedEvent = eventRepository.save(event); + // 4. Lưu Showtimes List savedShowtimes = new ArrayList<>(); if (request.getShowtimes() != null) { - for (var stRequest : request.getShowtimes()) { + for (var stReq : request.getShowtimes()) { Showtime showtime = new Showtime(); - showtime.setName(stRequest.getName()); - showtime.setStartTime(stRequest.getStartTime()); - showtime.setEndTime(stRequest.getEndTime()); + showtime.setName(stReq.getName()); + showtime.setStartTime(stReq.getStartTime()); + showtime.setEndTime(stReq.getEndTime()); showtime.setEvent(savedEvent); - savedShowtimes.add(showtimeRepository.save(showtime)); // Lưu xong lấy luôn ID + savedShowtimes.add(showtimeRepository.save(showtime)); } } - // Đóng gói dữ liệu trả về cho Frontend + + // 5. Trả về Response kèm ID vừa tạo EventResponse response = new EventResponse(); response.setId(savedEvent.getId()); - // Trả về danh sách showtimes kèm ID vừa tạo response.setShowtimes(savedShowtimes.stream().map(st -> { EventResponse.ShowtimeResponse sr = new EventResponse.ShowtimeResponse(); sr.setId(st.getId()); diff --git a/backend/src/main/java/com/ticketrush/service/UserService.java b/backend/src/main/java/com/ticketrush/service/UserService.java new file mode 100644 index 0000000..9897999 --- /dev/null +++ b/backend/src/main/java/com/ticketrush/service/UserService.java @@ -0,0 +1,61 @@ +package com.ticketrush.service; + +import com.ticketrush.model.dto.UserProfileDto; +import com.ticketrush.model.entity.User; +import com.ticketrush.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public UserProfileDto getProfile(UUID userId) { + User user = userRepository.findById(userId).orElse(null); + + if (user == null) { + // Return empty DTO if user doesn't exist in local DB yet + return new UserProfileDto(); + } + + UserProfileDto dto = new UserProfileDto(); + dto.setEmail(user.getEmail()); + dto.setFullName(user.getFullName()); + dto.setPhoneNumber(user.getPhoneNumber()); + dto.setGender(user.getGender()); + dto.setDateOfBirth(user.getDateOfBirth()); + dto.setAvatarUrl(user.getAvatarUrl()); + return dto; + } + + @Transactional + public UserProfileDto updateProfile(UUID userId, String email, UserProfileDto dto) { + User user = userRepository.findById(userId).orElseGet(() -> { + // Create user if not exists + User newUser = new User(); + newUser.setId(userId); + newUser.setEmail(email); + newUser.setRole("authenticated"); + return newUser; + }); + + user.setFullName(dto.getFullName()); + user.setPhoneNumber(dto.getPhoneNumber()); + user.setGender(dto.getGender()); + user.setDateOfBirth(dto.getDateOfBirth()); + + if (dto.getAvatarUrl() != null && !dto.getAvatarUrl().isEmpty()) { + user.setAvatarUrl(dto.getAvatarUrl()); + } + + userRepository.save(user); + + return getProfile(userId); + } +} diff --git a/backend/src/main/java/com/ticketrush/util/JwtAuthFilter.java b/backend/src/main/java/com/ticketrush/util/JwtAuthFilter.java deleted file mode 100644 index 89ef6d1..0000000 --- a/backend/src/main/java/com/ticketrush/util/JwtAuthFilter.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.ticketrush.util; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; - -import java.util.Base64; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * JwtAuthFilter — Filter chạy một lần mỗi request, verify JWT từ Supabase. - * - * Flow: - * 1. Đọc header "Authorization: Bearer " - * 2. Verify chữ ký bằng Supabase JWT Secret - * 3. Parse subject (user_id) và role từ Claims - * 4. Gắn Authentication vào SecurityContext để Spring biết user là ai - */ -@Component -public class JwtAuthFilter extends OncePerRequestFilter { - - // Lấy từ application.properties: supabase.jwt.secret - @Value("${supabase.jwt.secret}") - private String jwtSecret; - - @Value("${supabase.jwt.jwk-set-uri}") - private String jwkSetUri; - - @Override - protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain - ) throws ServletException, IOException { - - System.out.println(">>> Filter đã nhận được request: " + request.getRequestURI()); - - String authHeader = request.getHeader("Authorization"); - System.out.println(">>> Header Authorization: " + authHeader); - - // Không có header hoặc không bắt đầu bằng Bearer → bỏ qua, tiếp tục chain - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - String token = authHeader.substring(7); // Cắt bỏ "Bearer " - - try { - // 1. Tạo bộ giải mã và ÉP nó sử dụng thuật toán ES256 - var jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri) - .jwsAlgorithm(SignatureAlgorithm.ES256) // Cực kỳ quan trọng ở đây! - .build(); - - // 2. Tiến hành giải mã - Jwt decodedJwt = jwtDecoder.decode(token); - - // 3. Lấy thông tin user (subject) và role (app_metadata) - String userId = decodedJwt.getSubject(); - Map appMetadata = decodedJwt.getClaimAsMap("app_metadata"); - String role = (appMetadata != null) ? (String) appMetadata.get("role") : null; - - System.out.println(">>> Backend đã verify ES256 thành công! Role: " + role); - - // 4. Thiết lập quyền hạn (Authorities) cho Spring Security - List authorities = role != null - ? List.of(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) - : Collections.emptyList(); - - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(userId, null, authorities); - - SecurityContextHolder.getContext().setAuthentication(authentication); - - } catch (JwtException e) { - // Token lỗi hoặc hết hạn -> Không đặt Authentication, - // để Spring Security tự xử lý ở bước Filter sau (nếu route là private). - System.out.println(">>> LỖI XÁC THỰC TOKEN: " + e.getMessage()); // Xem nó có báo sai chữ ký không - } - - filterChain.doFilter(request, response); - } - - /** - * Parse role từ JWT claims của Supabase. - * Supabase đặt role trong: app_metadata.role - * - * @param claims JWT claims đã được verified - * @return role string hoặc null nếu không tìm thấy - */ - @SuppressWarnings("unchecked") - private String extractRole(Claims claims) { - try { - Map appMetadata = (Map) claims.get("app_metadata"); - if (appMetadata != null) { - return (String) appMetadata.get("role"); - } - } catch (ClassCastException ignored) { - // Bỏ qua nếu cấu trúc claims không đúng - } - return null; - } - - /** - * Helper: lấy userId từ SecurityContext trong các Controller/Service. - * - * Dùng trong Controller: - * String userId = JwtAuthFilter.getCurrentUserId(); - * - * @return user UUID string hoặc null - */ - public static String getCurrentUserId() { - var auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.getPrincipal() instanceof String userId) { - return userId; - } - return null; - } -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d484cc1..92b678e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,11 @@ -import { Routes, Route } from "react-router-dom"; +import { Routes, Route, Outlet } from "react-router-dom"; // Thêm Outlet ở đây import Home from "./pages/Home"; import Navbar from "./components/layout/Navbar"; import Footer from "./components/layout/Footer"; import AuthModal from "./components/auth/AuthModal"; +import Profile from "./pages/Profile"; +import MyTickets from "./pages/MyTickets"; +import EventDetail from "./pages/EventDetail"; import { AuthProvider } from "./context/AuthContext"; import AdminDashboard from "./pages/AdminDashboard"; import AdminAddEvent from "./pages/AdminAddEvent"; @@ -10,11 +13,12 @@ import AdminLayout from "./components/layout/AdminLayout"; import AdminEventsList from "./pages/AdminEventsList"; import AdminConfigureSeats from "./pages/AdminConfigureSeats"; -// Tạo một component phụ để bọc giao diện người dùng thường -const UserView = ({ children }) => ( +const UserLayout = () => ( <> -
{children}
+
+ {/* Các component như Home, Profile sẽ nhảy vào đây */} +