Skip to content

Commit 2fef06c

Browse files
iuliaaa20driedpampasbmbiancaoliviaa28irinnaa
authored
Backend User Authentication & Registration (#133)
* Add initial README for P2P-shopping project * Add contribution guidelines to CONTRIBUTING.md * Add contribution guidelines reference to README * Add contribution guidelines and project setup files * Add SonarQube configuration for code analysis * Add Spring Data JPA and MongoDB dependencies to build configuration * config(db): add PostgreSQL and MongoDB database configuration * Add Spring Web, OpenAPI setup and mock RoutingController Added dependencies for REST APIs and Swagger. Created a mock endpoint at /api/routing/calculate returning fake route data for frontend integration. * fix: refactor DTOs, add unit test, and disable DB autoconfig - Extrase clasele DTO (RoutingRequest, RoutingResponse, RoutePoint) în fișiere separate și adăugați getteri/setteri pentru a rezolva problemele de Maintainability din SonarQube. - Adăugat RoutingControllerTest.java pentru a crește Code Coverage-ul conform cerințelor Quality Gate-ului. - Adăugat excepție în application.properties pentru a ignora temporar auto-configurarea bazei de date (DataSource/Hibernate). Baza de date PostGIS va fi configurată și integrată ulterior de BE 1 în task-ul dedicat. * (telemetry): add TelemetryPingDTO, TelemetryRecord and TelemetryRepository * (telemetry): add TelemetryPingDTO, TelemetryRecord and TelemetryRepository * (telemetry): move MongoDB URI to environment variable * Revert "(telemetry): move MongoDB URI to environment variable" This reverts commit 3d9ef59. * Update application configuration and add JaCoCo for coverage (#115) * fix(config): update MongoDB URI property in application configuration * feat(jacoco): add JaCoCo configuration for test coverage reporting * chore: update springdoc-openapi dependency version in build configuration * Setup complete: Spring Boot, PostgreSQL, GlobalExceptionHandler * Refactor exception handler for security * Added unit test for GlobalExceptionHandler to increase coverage * final * sters chestii inutile(sper) * Update init-scripts/users.sql Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * plangea iepurele * curatenie * added tests * added login * added login * added login * test * test * test * final hopefully * final hopefully * fix: make qube happy by switching to authorization header + chore: misc config changes * fix: disable csrf cause i forgor * chore: readd init scripts * chore: sql scripts part 2 --------- Co-authored-by: supernova <matei.gurzu@gmail.com> Co-authored-by: bmbianca <barbubiancamariana@yahoo.com> Co-authored-by: oliviaa28 <156539318+oliviaa28@users.noreply.github.com> Co-authored-by: irinnaa <irinagrigorciuc@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent fbd2314 commit 2fef06c

23 files changed

Lines changed: 917 additions & 24 deletions

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
POSTGRES_DB=p2p_shopping
22
POSTGRES_USER=postgres
3-
POSTGRES_PASSWORD=postgres
3+
POSTGRES_PASSWORD=postgres
4+
JWT_SECRET=your-secret-key-here-at-least-32-characters-long

build.gradle.kts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ repositories {
2121
}
2222

2323
val springdocVersion = "3.0.2"
24-
24+
val jjwtVersion = "0.13.0"
2525
dependencies {
2626
annotationProcessor("org.projectlombok:lombok")
2727

@@ -32,11 +32,23 @@ dependencies {
3232
implementation("org.projectlombok:lombok")
3333
implementation("org.springframework.boot:spring-boot-starter-web")
3434
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:$springdocVersion")
35+
implementation("org.springframework.boot:spring-boot-starter-security")
36+
implementation("io.jsonwebtoken:jjwt-api:${jjwtVersion}")
37+
38+
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
39+
testImplementation("org.springframework.boot:spring-boot-starter-test")
40+
testImplementation("org.springframework.security:spring-security-test")
41+
42+
testImplementation("com.h2database:h2")
3543

44+
runtimeOnly("io.jsonwebtoken:jjwt-impl:${jjwtVersion}")
45+
runtimeOnly("io.jsonwebtoken:jjwt-jackson:${jjwtVersion}")
3646
runtimeOnly("org.postgresql:postgresql")
3747

3848
testImplementation("org.springframework.boot:spring-boot-starter-test")
3949
testImplementation("org.junit.platform:junit-platform-suite-api")
50+
testImplementation("org.testcontainers:testcontainers:1.19.0")
51+
testImplementation("org.testcontainers:postgresql:1.19.0")
4052
testRuntimeOnly("org.junit.platform:junit-platform-suite-engine")
4153
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
4254
}

docker-compose.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: '3.8'
2-
31
services:
42
db:
53
image: postgis/postgis:16-3.4
@@ -10,7 +8,7 @@ services:
108
POSTGRES_USER: ${POSTGRES_USER}
119
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
1210
ports:
13-
- "5432:5432"
11+
- "5433:5432"
1412
volumes:
1513
- pgdata:/var/lib/postgresql/data
1614
- ./init-scripts:/docker-entrypoint-initdb.d

init-scripts/users.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE TABLE IF NOT EXISTS users (
2+
id SERIAL PRIMARY KEY,
3+
first_name VARCHAR(255) NOT NULL,
4+
last_name VARCHAR(255) NOT NULL,
5+
email VARCHAR(255) NOT NULL UNIQUE,
6+
password VARCHAR(255) NOT NULL,
7+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
8+
);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.p2ps.auth.controller;
2+
3+
import com.p2ps.auth.security.dto.LoginRequest;
4+
import com.p2ps.auth.dto.RegisterRequest;
5+
import com.p2ps.auth.security.JwtUtil;
6+
import com.p2ps.auth.service.UserService;
7+
import jakarta.validation.Valid;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.security.authentication.AuthenticationManager;
10+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
11+
import org.springframework.security.core.Authentication;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
import java.util.Collections;
15+
import java.util.Map;
16+
17+
@RestController
18+
@RequestMapping("/api/auth")
19+
public class AuthController { // Acolada clasei deschisă aici
20+
21+
private final UserService userService;
22+
private final AuthenticationManager authenticationManager;
23+
private final JwtUtil jwtUtil;
24+
25+
public AuthController(UserService userService, AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
26+
this.userService = userService;
27+
this.authenticationManager = authenticationManager;
28+
this.jwtUtil = jwtUtil;
29+
}
30+
31+
@PostMapping("/register")
32+
public ResponseEntity<Map<String, String>> register(@Valid @RequestBody RegisterRequest request) {
33+
userService.registerUser(
34+
request.getEmail(),
35+
request.getPassword(),
36+
request.getFirstName(),
37+
request.getLastName()
38+
);
39+
return ResponseEntity.ok(Map.of("message", "User registered successfully!"));
40+
}
41+
42+
@PostMapping("/login")
43+
public ResponseEntity<Map<String, String>> login(@Valid @RequestBody LoginRequest request) {
44+
45+
Authentication authentication = authenticationManager.authenticate(
46+
new UsernamePasswordAuthenticationToken(
47+
request.getEmail(),
48+
request.getPassword()
49+
)
50+
);
51+
52+
53+
String token = jwtUtil.generateToken(request.getEmail());
54+
55+
return ResponseEntity.ok(Collections.singletonMap("token", token));
56+
}
57+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.p2ps.auth.dto;
2+
3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Pattern;
6+
import jakarta.validation.constraints.Size;
7+
8+
9+
10+
import lombok.*;
11+
12+
@Getter
13+
@Setter
14+
public class RegisterRequest {
15+
16+
@NotBlank(message = "First name is required")
17+
@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
18+
@Pattern(regexp = "^[a-zA-Z\\s-]+$", message = "First name can only contain letters, spaces, or hyphens")
19+
private String firstName;
20+
21+
@NotBlank(message = "Last name is required")
22+
@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
23+
@Pattern(regexp = "^[a-zA-Z\\s-]+$", message = "Last name can only contain letters, spaces, or hyphens")
24+
private String lastName;
25+
26+
@NotBlank(message = "Email is required")
27+
@Email(message = "Invalid email format")
28+
@Size(max = 255)
29+
private String email;
30+
31+
@NotBlank(message = "Password is required")
32+
@Size(min = 8, max = 100)
33+
@Pattern(regexp = "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$",
34+
message = "Password must contain at least one digit, one lowercase letter, and one uppercase letter")
35+
private String password;
36+
37+
38+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.p2ps.auth.model;
2+
3+
import jakarta.persistence.*;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
7+
@Getter
8+
@Setter
9+
@Entity
10+
@Table(name = "users")
11+
public class Users {
12+
13+
@Id
14+
@GeneratedValue(strategy = GenerationType.IDENTITY)
15+
private Integer id;
16+
17+
@Column(nullable = false, unique = true)
18+
private String email;
19+
20+
@Column(nullable = false)
21+
private String password;
22+
23+
24+
public Users() {}
25+
@Column(name = "first_name", nullable = false)
26+
private String firstName;
27+
28+
@Column(name = "last_name", nullable = false)
29+
private String lastName;
30+
31+
public Users(String email, String password, String firstName, String lastName) {
32+
this.email = email;
33+
this.password = password;
34+
this.firstName = firstName;
35+
this.lastName = lastName;
36+
}
37+
38+
39+
40+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.p2ps.auth.repository;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import com.p2ps.auth.model.Users;
5+
import java.util.Optional;
6+
7+
public interface UserRepository extends JpaRepository<Users, Integer> {
8+
Optional<Users> findByEmail(String email);
9+
boolean existsByEmail(String email);
10+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.p2ps.auth.security;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
8+
import org.springframework.security.core.context.SecurityContextHolder;
9+
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.web.filter.OncePerRequestFilter;
12+
13+
import java.io.IOException;
14+
import java.util.ArrayList;
15+
16+
@Component
17+
public class JwtAuthFilter extends OncePerRequestFilter {
18+
19+
private final JwtUtil jwtUtil;
20+
21+
public JwtAuthFilter(JwtUtil jwtUtil) {
22+
this.jwtUtil = jwtUtil;
23+
}
24+
25+
@Override
26+
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
27+
throws ServletException, IOException {
28+
29+
String token = null;
30+
String userEmail = null;
31+
32+
// Extract token from Authorization header
33+
String authorizationHeader = request.getHeader("Authorization");
34+
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
35+
token = authorizationHeader.substring(7);
36+
}
37+
38+
try {
39+
if (token != null) {
40+
userEmail = jwtUtil.extractEmail(token);
41+
}
42+
43+
// Authenticate if user is not already in the SecurityContext
44+
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null && !jwtUtil.isTokenExpired(token)){
45+
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
46+
userEmail, null, new ArrayList<>()
47+
);
48+
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
49+
50+
// Set user as authenticated
51+
SecurityContextHolder.getContext().setAuthentication(authToken);
52+
}
53+
54+
} catch (Exception _) {
55+
// Clear context if token is expired, malformed, or invalid
56+
SecurityContextHolder.clearContext();
57+
}
58+
59+
// Continue the filter chain
60+
filterChain.doFilter(request, response);
61+
}
62+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.p2ps.auth.security;
2+
3+
import io.jsonwebtoken.Claims;
4+
import io.jsonwebtoken.Jwts;
5+
import io.jsonwebtoken.security.Keys;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.stereotype.Component;
8+
import jakarta.annotation.PostConstruct;
9+
10+
import javax.crypto.SecretKey;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.Date;
13+
14+
@Component
15+
public class JwtUtil {
16+
17+
@Value("${jwt.secret}")
18+
private String secretKeyString;
19+
20+
private SecretKey secretKey;
21+
22+
private static final long EXPIRATION_TIME = 1000L * 60 * 60 * 24; // 24h
23+
24+
@PostConstruct
25+
public void init() {
26+
27+
byte[] keyBytes = secretKeyString == null
28+
? new byte[0]
29+
: secretKeyString.getBytes(StandardCharsets.UTF_8);
30+
31+
if (keyBytes.length < 32) {
32+
throw new IllegalStateException("JWT secret must be at least 32 bytes for HS256");
33+
}
34+
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
35+
}
36+
37+
public String generateToken(String email) {
38+
39+
return Jwts.builder()
40+
.subject(email)
41+
.issuedAt(new Date(System.currentTimeMillis()))
42+
.expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
43+
.signWith(secretKey)
44+
.compact();
45+
}
46+
47+
public String extractEmail(String token) {
48+
return extractAllClaims(token).getSubject();
49+
}
50+
51+
public boolean isTokenExpired(String token) {
52+
return extractAllClaims(token).getExpiration().before(new Date());
53+
}
54+
55+
public boolean isTokenValid(String token, String userEmailFromDatabase) {
56+
final String email = extractEmail(token);
57+
return (email.equals(userEmailFromDatabase) && !isTokenExpired(token));
58+
}
59+
60+
private Claims extractAllClaims(String token) {
61+
return Jwts.parser()
62+
.verifyWith(secretKey)
63+
.build()
64+
.parseSignedClaims(token)
65+
.getPayload();
66+
}
67+
}

0 commit comments

Comments
 (0)