-
Notifications
You must be signed in to change notification settings - Fork 0
Backend User Authentication & Registration #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
17514a3
ad79263
2c80a8c
cbcda2b
7167c8b
11e2453
df5872a
0b6a077
b02fdbf
3c56aee
b9c9081
3d9ef59
99fc210
3d3ad0f
9c1454d
d3ac83b
f4ee687
a70010b
2b7ba0c
9af5b45
83fa7a1
7cd062b
675404a
c638e74
7877dae
083495d
0f4e2b2
f1ec012
9276f6f
1acbc0e
869041e
86087e3
6a6a8f7
8c308c4
b2d34f3
fc6c373
c9a20f7
ada55dd
3eea4b2
c797bfa
3a522e6
0be7f5a
35ff5b4
bfa6b56
cc7fdc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| POSTGRES_DB=p2p_shopping | ||
| POSTGRES_USER=postgres | ||
| POSTGRES_PASSWORD=postgres | ||
| POSTGRES_PASSWORD=postgres | ||
| JWT_SECRET=your-secret-key-here-at-least-32-characters-long |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| CREATE TABLE IF NOT EXISTS users ( | ||
| id SERIAL PRIMARY KEY, | ||
| first_name VARCHAR(255) NOT NULL, | ||
| last_name VARCHAR(255) NOT NULL, | ||
| email VARCHAR(255) NOT NULL UNIQUE, | ||
| password VARCHAR(255) NOT NULL, | ||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package com.p2ps.auth.controller; | ||
|
|
||
| import com.p2ps.auth.security.dto.LoginRequest; | ||
| import com.p2ps.auth.dto.RegisterRequest; | ||
| import com.p2ps.auth.security.JwtUtil; | ||
| import com.p2ps.auth.service.UserService; | ||
| import jakarta.validation.Valid; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.authentication.AuthenticationManager; | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.Map; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/auth") | ||
| public class AuthController { // Acolada clasei deschisă aici | ||
|
|
||
| private final UserService userService; | ||
| private final AuthenticationManager authenticationManager; | ||
| private final JwtUtil jwtUtil; | ||
|
|
||
| public AuthController(UserService userService, AuthenticationManager authenticationManager, JwtUtil jwtUtil) { | ||
| this.userService = userService; | ||
| this.authenticationManager = authenticationManager; | ||
| this.jwtUtil = jwtUtil; | ||
| } | ||
|
|
||
| @PostMapping("/register") | ||
| public ResponseEntity<Map<String, String>> register(@Valid @RequestBody RegisterRequest request) { | ||
| userService.registerUser( | ||
| request.getEmail(), | ||
| request.getPassword(), | ||
| request.getFirstName(), | ||
| request.getLastName() | ||
| ); | ||
| return ResponseEntity.ok(Map.of("message", "User registered successfully!")); | ||
| } | ||
|
|
||
| @PostMapping("/login") | ||
| public ResponseEntity<Map<String, String>> login(@Valid @RequestBody LoginRequest request) { | ||
|
|
||
| Authentication authentication = authenticationManager.authenticate( | ||
|
Check warning on line 45 in src/main/java/com/p2ps/auth/controller/AuthController.java
|
||
| new UsernamePasswordAuthenticationToken( | ||
| request.getEmail(), | ||
| request.getPassword() | ||
| ) | ||
| ); | ||
|
Check warning on line 50 in src/main/java/com/p2ps/auth/controller/AuthController.java
|
||
|
|
||
|
|
||
| String token = jwtUtil.generateToken(request.getEmail()); | ||
|
|
||
| return ResponseEntity.ok(Collections.singletonMap("token", token)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.p2ps.auth.dto; | ||
|
|
||
| import jakarta.validation.constraints.Email; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Pattern; | ||
| import jakarta.validation.constraints.Size; | ||
|
|
||
|
|
||
|
|
||
| import lombok.*; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| public class RegisterRequest { | ||
|
|
||
| @NotBlank(message = "First name is required") | ||
| @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters") | ||
| @Pattern(regexp = "^[a-zA-Z\\s-]+$", message = "First name can only contain letters, spaces, or hyphens") | ||
| private String firstName; | ||
|
|
||
| @NotBlank(message = "Last name is required") | ||
| @Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters") | ||
| @Pattern(regexp = "^[a-zA-Z\\s-]+$", message = "Last name can only contain letters, spaces, or hyphens") | ||
| private String lastName; | ||
|
|
||
| @NotBlank(message = "Email is required") | ||
| @Email(message = "Invalid email format") | ||
| @Size(max = 255) | ||
| private String email; | ||
|
|
||
| @NotBlank(message = "Password is required") | ||
| @Size(min = 8, max = 100) | ||
| @Pattern(regexp = "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$", | ||
| message = "Password must contain at least one digit, one lowercase letter, and one uppercase letter") | ||
| private String password; | ||
|
|
||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.p2ps.auth.model; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @Entity | ||
| @Table(name = "users") | ||
| public class Users { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Integer id; | ||
|
|
||
| @Column(nullable = false, unique = true) | ||
| private String email; | ||
|
|
||
| @Column(nullable = false) | ||
| private String password; | ||
|
|
||
|
|
||
| public Users() {} | ||
| @Column(name = "first_name", nullable = false) | ||
| private String firstName; | ||
|
|
||
| @Column(name = "last_name", nullable = false) | ||
| private String lastName; | ||
|
|
||
| public Users(String email, String password, String firstName, String lastName) { | ||
| this.email = email; | ||
| this.password = password; | ||
| this.firstName = firstName; | ||
| this.lastName = lastName; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.p2ps.auth.repository; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import com.p2ps.auth.model.Users; | ||
| import java.util.Optional; | ||
|
|
||
| public interface UserRepository extends JpaRepository<Users, Integer> { | ||
| Optional<Users> findByEmail(String email); | ||
| boolean existsByEmail(String email); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package com.p2ps.auth.security; | ||
|
|
||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
|
|
||
| @Component | ||
| public class JwtAuthFilter extends OncePerRequestFilter { | ||
|
|
||
| private final JwtUtil jwtUtil; | ||
|
|
||
| public JwtAuthFilter(JwtUtil jwtUtil) { | ||
| this.jwtUtil = jwtUtil; | ||
| } | ||
|
|
||
| @Override | ||
| public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||
| throws ServletException, IOException { | ||
|
|
||
| String token = null; | ||
| String userEmail = null; | ||
|
|
||
| // Extract token from Authorization header | ||
| String authorizationHeader = request.getHeader("Authorization"); | ||
| if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { | ||
| token = authorizationHeader.substring(7); | ||
| } | ||
|
|
||
| try { | ||
| if (token != null) { | ||
| userEmail = jwtUtil.extractEmail(token); | ||
| } | ||
|
|
||
| // Authenticate if user is not already in the SecurityContext | ||
| if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null && !jwtUtil.isTokenExpired(token)){ | ||
| UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( | ||
| userEmail, null, new ArrayList<>() | ||
| ); | ||
| authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
|
|
||
| // Set user as authenticated | ||
| SecurityContextHolder.getContext().setAuthentication(authToken); | ||
| } | ||
|
|
||
| } catch (Exception _) { | ||
| // Clear context if token is expired, malformed, or invalid | ||
| SecurityContextHolder.clearContext(); | ||
| } | ||
|
|
||
| // Continue the filter chain | ||
| filterChain.doFilter(request, response); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package com.p2ps.auth.security; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
| import io.jsonwebtoken.Jwts; | ||
| import io.jsonwebtoken.security.Keys; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Component; | ||
| import jakarta.annotation.PostConstruct; | ||
|
|
||
| import javax.crypto.SecretKey; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Date; | ||
|
|
||
| @Component | ||
| public class JwtUtil { | ||
|
|
||
| @Value("${jwt.secret}") | ||
| private String secretKeyString; | ||
|
|
||
| private SecretKey secretKey; | ||
|
|
||
| private static final long EXPIRATION_TIME = 1000L * 60 * 60 * 24; // 24h | ||
|
|
||
| @PostConstruct | ||
| public void init() { | ||
|
|
||
| byte[] keyBytes = secretKeyString == null | ||
| ? new byte[0] | ||
| : secretKeyString.getBytes(StandardCharsets.UTF_8); | ||
|
|
||
| if (keyBytes.length < 32) { | ||
| throw new IllegalStateException("JWT secret must be at least 32 bytes for HS256"); | ||
| } | ||
| this.secretKey = Keys.hmacShaKeyFor(keyBytes); | ||
| } | ||
|
|
||
| public String generateToken(String email) { | ||
|
|
||
| return Jwts.builder() | ||
| .subject(email) | ||
| .issuedAt(new Date(System.currentTimeMillis())) | ||
| .expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) | ||
| .signWith(secretKey) | ||
| .compact(); | ||
| } | ||
|
|
||
| public String extractEmail(String token) { | ||
| return extractAllClaims(token).getSubject(); | ||
| } | ||
|
|
||
| public boolean isTokenExpired(String token) { | ||
| return extractAllClaims(token).getExpiration().before(new Date()); | ||
| } | ||
|
|
||
| public boolean isTokenValid(String token, String userEmailFromDatabase) { | ||
| final String email = extractEmail(token); | ||
| return (email.equals(userEmailFromDatabase) && !isTokenExpired(token)); | ||
| } | ||
|
|
||
| private Claims extractAllClaims(String token) { | ||
| return Jwts.parser() | ||
| .verifyWith(secretKey) | ||
| .build() | ||
| .parseSignedClaims(token) | ||
| .getPayload(); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.