Skip to content
Open
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
16 changes: 8 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
backend/src/main/resources/application.properties
frontend/.env
# Antigravity-kit
/.agent
# Document
doc/
backend/src/main/resources/application.properties
frontend/.env

# Antigravity-kit
/.agent

# Document
docs/
5 changes: 4 additions & 1 deletion backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- JJWT - Parse & verify Supabase JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
Expand Down
85 changes: 64 additions & 21 deletions backend/src/main/java/com/ticketrush/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand All @@ -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();
}
Expand All @@ -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"));
Expand All @@ -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<String, Object> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/api/events")
Expand All @@ -19,4 +20,9 @@ public class EventController {
public List<EventResponse> getEvents() {
return eventService.getAllEvents();
}

@GetMapping("/{id}")
public EventResponse getEventById(@PathVariable UUID id) {
return eventService.getEventById(id);
}
}
Original file line number Diff line number Diff line change
@@ -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<UserProfileDto> 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<String, Object> meta = jwt.getClaimAsMap("user_metadata");
if (meta.containsKey("full_name")) profile.setFullName((String) meta.get("full_name"));
}
}

return ResponseEntity.ok(profile);
}

@PutMapping
public ResponseEntity<UserProfileDto> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.Data;
import java.util.List;
import java.util.UUID;
import java.util.List;

@Data
public class EventResponse {
Expand All @@ -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<ShowtimeResponse> showtimes;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 14 additions & 0 deletions backend/src/main/java/com/ticketrush/model/dto/UserProfileDto.java
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 2 additions & 1 deletion backend/src/main/java/com/ticketrush/model/entity/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.UUID;
import java.util.List;
import jakarta.persistence.*;
import lombok.Data;

Expand All @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/java/com/ticketrush/model/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Order> orders;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ public interface UserRepository extends JpaRepository<User, UUID> {

// 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<User> findByEmail(String email);
}
}
Loading