diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java index 12f85eda..6f1f33f7 100644 --- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java +++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java @@ -2,11 +2,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; -import org.bugzkit.api.auth.jwt.util.JwtUtil; -import org.bugzkit.api.auth.payload.dto.AuthTokensDTO; import org.bugzkit.api.auth.payload.request.AuthTokensRequest; import org.bugzkit.api.auth.payload.request.ForgotPasswordRequest; -import org.bugzkit.api.auth.payload.request.RefreshAuthTokensRequest; import org.bugzkit.api.auth.payload.request.RegisterUserRequest; import org.bugzkit.api.auth.payload.request.ResetPasswordRequest; import org.bugzkit.api.auth.payload.request.VerificationEmailRequest; @@ -15,6 +12,8 @@ import org.bugzkit.api.auth.util.AuthUtil; import org.bugzkit.api.shared.constants.Path; import org.bugzkit.api.user.payload.dto.UserDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -28,6 +27,12 @@ public class AuthController { private final AuthService authService; + @Value("${jwt.access-token.duration}") + private int accessTokenDuration; + + @Value("${jwt.refresh-token.duration}") + private int refreshTokenDuration; + public AuthController(AuthService authService) { this.authService = authService; } @@ -39,33 +44,57 @@ public ResponseEntity register( } @PostMapping("/tokens") - public ResponseEntity authenticate( + public ResponseEntity authenticate( @Valid @RequestBody AuthTokensRequest authTokensRequest, HttpServletRequest request) { final var ipAddress = AuthUtil.getUserIpAddress(request); - return ResponseEntity.ok(authService.authenticate(authTokensRequest, ipAddress)); + final var authTokensDTO = authService.authenticate(authTokensRequest, ipAddress); + final var accessTokenCookie = + AuthUtil.createCookie("accessToken", authTokensDTO.accessToken(), accessTokenDuration); + final var refreshTokenCookie = + AuthUtil.createCookie("refreshToken", authTokensDTO.refreshToken(), refreshTokenDuration); + return ResponseEntity.noContent() + .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) + .build(); } @DeleteMapping("/tokens") public ResponseEntity deleteTokens(HttpServletRequest request) { - final var accessToken = JwtUtil.removeBearer(AuthUtil.getAccessTokenFromRequest(request)); + final var accessToken = AuthUtil.getValueFromCookie("accessToken", request); final var ipAddress = AuthUtil.getUserIpAddress(request); authService.deleteTokens(accessToken, ipAddress); - return ResponseEntity.noContent().build(); + final var accessTokenCookie = AuthUtil.createCookie("accessToken", "", 0); + final var refreshTokenCookie = AuthUtil.createCookie("refreshToken", "", 0); + return ResponseEntity.noContent() + .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) + .build(); } @DeleteMapping("/tokens/devices") public ResponseEntity deleteTokensOnAllDevices() { authService.deleteTokensOnAllDevices(); - return ResponseEntity.noContent().build(); + final var accessTokenCookie = AuthUtil.createCookie("accessToken", "", 0); + final var refreshTokenCookie = AuthUtil.createCookie("refreshToken", "", 0); + return ResponseEntity.noContent() + .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) + .build(); } @PostMapping("/tokens/refresh") - public ResponseEntity refreshTokens( - @Valid @RequestBody RefreshAuthTokensRequest refreshAuthTokensRequest, - HttpServletRequest request) { - final var refreshToken = JwtUtil.removeBearer(refreshAuthTokensRequest.refreshToken()); + public ResponseEntity refreshTokens(HttpServletRequest request) { + final var refreshToken = AuthUtil.getValueFromCookie("refreshToken", request); final var ipAddress = AuthUtil.getUserIpAddress(request); - return ResponseEntity.ok(authService.refreshTokens(refreshToken, ipAddress)); + final var authTokensDTO = authService.refreshTokens(refreshToken, ipAddress); + final var accessTokenCookie = + AuthUtil.createCookie("accessToken", authTokensDTO.accessToken(), accessTokenDuration); + final var refreshTokenCookie = + AuthUtil.createCookie("refreshToken", authTokensDTO.refreshToken(), refreshTokenDuration); + return ResponseEntity.noContent() + .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) + .build(); } @PostMapping("/password/forgot") diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/util/JwtUtil.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/util/JwtUtil.java index e3d0c535..cf91c1eb 100644 --- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/util/JwtUtil.java +++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/util/JwtUtil.java @@ -8,8 +8,6 @@ import org.bugzkit.api.user.payload.dto.RoleDTO; public class JwtUtil { - private static final String BEARER = "Bearer "; - private JwtUtil() {} public static void verify(String token, String secret, JwtPurpose purpose) { @@ -23,14 +21,6 @@ public static Algorithm getAlgorithm(String secret) { return Algorithm.HMAC512(secret.getBytes()); } - public static String removeBearer(String token) { - return token.replace(BEARER, ""); - } - - public static boolean isBearer(String token) { - return token.startsWith(BEARER); - } - public static Long getUserId(String token) { return Long.parseLong(JWT.decode(token).getIssuer()); } diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/RefreshAuthTokensRequest.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/RefreshAuthTokensRequest.java deleted file mode 100644 index 4b0dc82e..00000000 --- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/RefreshAuthTokensRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.bugzkit.api.auth.payload.request; - -import jakarta.validation.constraints.NotBlank; - -public record RefreshAuthTokensRequest( - @NotBlank(message = "{auth.tokenRequired}") String refreshToken) {} diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/JWTFilter.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/JWTFilter.java index 99b664fa..3817aa6e 100644 --- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/JWTFilter.java +++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/JWTFilter.java @@ -31,8 +31,8 @@ protected void doFilterInternal( @Nonnull HttpServletResponse response, @Nonnull FilterChain chain) throws IOException, ServletException { - final var accessToken = AuthUtil.getAccessTokenFromRequest(request); - if (accessToken == null || !JwtUtil.isBearer(accessToken)) { + final var accessToken = AuthUtil.getValueFromCookie("accessToken", request); + if (accessToken == null) { chain.doFilter(request, response); return; } @@ -45,9 +45,8 @@ protected void doFilterInternal( } private UsernamePasswordAuthenticationToken getAuth(String accessToken) { - final var token = JwtUtil.removeBearer(accessToken); - accessTokenService.check(token); - final var userId = JwtUtil.getUserId(token); + accessTokenService.check(accessToken); + final var userId = JwtUtil.getUserId(accessToken); final var userPrincipal = (UserPrincipal) userDetailsService.loadUserByUserId(userId); return new UsernamePasswordAuthenticationToken( userPrincipal, null, userPrincipal.getAuthorities()); diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/util/AuthUtil.java b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/util/AuthUtil.java index ce80fcdb..13380f1b 100644 --- a/backend/spring-boot/src/main/java/org/bugzkit/api/auth/util/AuthUtil.java +++ b/backend/spring-boot/src/main/java/org/bugzkit/api/auth/util/AuthUtil.java @@ -1,9 +1,12 @@ package org.bugzkit.api.auth.util; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; import org.bugzkit.api.auth.security.UserPrincipal; import org.bugzkit.api.user.model.Role.RoleName; -import org.springframework.http.HttpHeaders; +import org.springframework.boot.web.server.Cookie.SameSite; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.context.SecurityContextHolder; public class AuthUtil { @@ -38,7 +41,23 @@ public static String getUserIpAddress(HttpServletRequest request) { return ipAddress; } - public static String getAccessTokenFromRequest(HttpServletRequest request) { - return request.getHeader(HttpHeaders.AUTHORIZATION); + public static String getValueFromCookie(String name, HttpServletRequest request) { + final var cookies = request.getCookies(); + if (cookies == null) return null; + return Arrays.stream(cookies) + .filter(cookie -> name.equals(cookie.getName())) + .findFirst() + .map(Cookie::getValue) + .orElse(null); + } + + public static ResponseCookie createCookie(String name, String value, int maxAge) { + return ResponseCookie.from(name, value) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(maxAge) + .sameSite(SameSite.STRICT.attributeValue()) + .build(); } } diff --git a/backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/WebMvcConfig.java b/backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/WebMvcConfig.java index 6a179740..9bd77abf 100644 --- a/backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/WebMvcConfig.java +++ b/backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/WebMvcConfig.java @@ -1,6 +1,7 @@ package org.bugzkit.api.shared.config; import org.bugzkit.api.shared.interceptor.RequestInterceptor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -8,12 +9,16 @@ @Configuration public class WebMvcConfig implements WebMvcConfigurer { + @Value("${ui.url}") + private String uiUrl; + @Override public void addCorsMappings(CorsRegistry registry) { registry .addMapping("/**") - .allowedOrigins("*") + .allowedOrigins(uiUrl) .allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE") + .allowCredentials(true) .maxAge(3600); } diff --git a/backend/spring-boot/src/main/resources/static/openapi.yml b/backend/spring-boot/src/main/resources/static/openapi.yml index 6c5fac41..e74d50f0 100644 --- a/backend/spring-boot/src/main/resources/static/openapi.yml +++ b/backend/spring-boot/src/main/resources/static/openapi.yml @@ -1,10 +1,10 @@ openapi: 3.0.3 info: title: bugzkit - description: Description + description: A production-ready web application template termsOfService: https://swagger.io/terms/ contact: - email: dejan.zdravkovic@protonmail.com + email: office@bugzkit.com license: name: MIT License url: https://choosealicense.com/licenses/mit/ @@ -55,15 +55,17 @@ paths: requestBody: $ref: "#/components/requestBodies/AuthTokenRequest" responses: - 200: + 204: description: Auth tokens created successfully - content: - application/json: + headers: + Set-Cookie: schema: - $ref: "#/components/schemas/AuthTokensDTO" - example: - accessToken: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0yMFQxNDozMzo0NC43MDcyOTMzMDlaIiwiZXhwIjoxNjM0NzQxMzI0LCJ1c2VySWQiOjJ9.uXOVA1q-o2DtHmwBAzEfqEm8GLpAhXrYo0rlZ_6NFbBGILhkV74x-Iu9W2uSfSlwp1IfKPCHlR6zWVPvAbhWVw - refreshToken: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0xOVQxMzowMDowMy45MDYzNDQyMDJaIiwiZXhwIjoxNjM1MjUzMjAzLCJ1c2VySWQiOjJ9.RHzh6qyGJEKYdvCuCF7wPoUGBSrDGeoY8dSTBhuv21Fzw_CPEa5KeI3MOYgSN3zA1o_ZlKwjHgpSsPM3xAO_DQ + type: array + items: + type: string + example: + - "accessToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0yMFQxNDozMzo0NC43MDcyOTMzMDlaIiwiZXhwIjoxNjM0NzQxMzI0LCJ1c2VySWQiOjJ9.uXOVA1q-o2DtHmwBAzEfqEm8GLpAhXrYo0rlZ_6NFbBGILhkV74x-Iu9W2uSfSlwp1IfKPCHlR6zWVPvAbhWVw; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=9000" + - "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0xOVQxMzowMDowMy45MDYzNDQyMDJaIiwiZXhwIjoxNjM1MjUzMjAzLCJ1c2VySWQiOjJ9.RHzh6qyGJEKYdvCuCF7wPoUGBSrDGeoY8dSTBhuv21Fzw_CPEa5KeI3MOYgSN3zA1o_ZlKwjHgpSsPM3xAO_DQ; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800" 400: $ref: "#/components/responses/BadRequest" 401: @@ -75,37 +77,61 @@ paths: - auth summary: Delete access and refresh tokens security: - - BearerAuth: [] + - cookieAuth: [ ] responses: 204: description: Access and refresh tokens deleted successfully + headers: + Set-Cookie: + schema: + type: array + items: + type: string + example: + - "accessToken=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0" + - "refreshToken=; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=0" /auth/tokens/devices: delete: tags: - auth summary: Delete access and refresh tokens on all devices security: - - BearerAuth: [] + - cookieAuth: [ ] responses: 204: description: Access and refresh tokens deleted successfully on all devices + headers: + Set-Cookie: + schema: + type: array + items: + type: string + example: + - "accessToken=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0" + - "refreshToken=; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=0" /auth/tokens/refresh: post: tags: - auth summary: Refresh auth tokens - requestBody: - $ref: "#/components/requestBodies/RefreshAuthTokensRequest" + parameters: + - in: cookie + name: refreshToken + schema: + type: string + example: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0xOVQxMzowMDowMy45MDYzNDQyMDJaIiwiZXhwIjoxNjM1MjUzMjAzLCJ1c2VySWQiOjJ9.RHzh6qyGJEKYdvCuCF7wPoUGBSrDGeoY8dSTBhuv21Fzw_CPEa5KeI3MOYgSN3zA1o_ZlKwjHgpSsPM3xAO_DQ" responses: - 200: + 204: description: Auth tokens refreshed successfully - content: - application/json: + headers: + Set-Cookie: schema: - $ref: "#/components/schemas/AuthTokensDTO" - example: - accessToken: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0yMFQxNDozMzo0NC43MDcyOTMzMDlaIiwiZXhwIjoxNjM0NzQxMzI0LCJ1c2VySWQiOjJ9.uXOVA1q-o2DtHmwBAzEfqEm8GLpAhXrYo0rlZ_6NFbBGILhkV74x-Iu9W2uSfSlwp1IfKPCHlR6zWVPvAbhWVw - refreshToken: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0xOVQxMzowMDowMy45MDYzNDQyMDJaIiwiZXhwIjoxNjM1MjUzMjAzLCJ1c2VySWQiOjJ9.RHzh6qyGJEKYdvCuCF7wPoUGBSrDGeoY8dSTBhuv21Fzw_CPEa5KeI3MOYgSN3zA1o_ZlKwjHgpSsPM3xAO_DQ + type: array + items: + type: string + example: + - "accessToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0yMFQxNDozMzo0NC43MDcyOTMzMDlaIiwiZXhwIjoxNjM0NzQxMzI0LCJ1c2VySWQiOjJ9.uXOVA1q-o2DtHmwBAzEfqEm8GLpAhXrYo0rlZ_6NFbBGILhkV74x-Iu9W2uSfSlwp1IfKPCHlR6zWVPvAbhWVw; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=9000" + - "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJVU0VSIl0sImlzc3VlZEF0IjoiMjAyMS0xMC0xOVQxMzowMDowMy45MDYzNDQyMDJaIiwiZXhwIjoxNjM1MjUzMjAzLCJ1c2VySWQiOjJ9.RHzh6qyGJEKYdvCuCF7wPoUGBSrDGeoY8dSTBhuv21Fzw_CPEa5KeI3MOYgSN3zA1o_ZlKwjHgpSsPM3xAO_DQ; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800" 400: $ref: "#/components/responses/BadRequest" /auth/password/forgot: @@ -170,7 +196,7 @@ paths: - profile summary: Get profile security: - - BearerAuth: [] + - cookieAuth: [ ] responses: 200: description: Profile retrieved successfully @@ -190,7 +216,7 @@ paths: - profile summary: Patch profile security: - - BearerAuth: [] + - cookieAuth: [ ] requestBody: $ref: "#/components/requestBodies/PatchProfileRequest" responses: @@ -216,7 +242,7 @@ paths: - profile summary: Delete profile security: - - BearerAuth: [] + - cookieAuth: [ ] responses: 204: description: Profile deleted successfully @@ -228,7 +254,7 @@ paths: - profile summary: Change profile password security: - - BearerAuth: [] + - cookieAuth: [ ] requestBody: $ref: "#/components/requestBodies/ChangePasswordRequest" responses: @@ -363,7 +389,7 @@ paths: - roles summary: Get roles security: - - BearerAuth: [] + - cookieAuth: [ ] responses: 200: description: Roles retrieved successfully @@ -386,7 +412,7 @@ paths: - admin summary: Create user security: - - BearerAuth: [] + - cookieAuth: [ ] requestBody: $ref: "#/components/requestBodies/UserRequest" responses: @@ -418,7 +444,7 @@ paths: - admin summary: Get users with pagination security: - - BearerAuth: [] + - cookieAuth: [ ] parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/size" @@ -478,7 +504,7 @@ paths: - admin summary: Get user by id security: - - BearerAuth: [] + - cookieAuth: [ ] parameters: - $ref: "#/components/parameters/id" responses: @@ -508,7 +534,7 @@ paths: - admin summary: Update user by id security: - - BearerAuth: [] + - cookieAuth: [ ] parameters: - $ref: "#/components/parameters/id" requestBody: @@ -543,7 +569,7 @@ paths: - admin summary: Patch user by id security: - - BearerAuth: [] + - cookieAuth: [ ] parameters: - $ref: "#/components/parameters/id" requestBody: @@ -580,7 +606,7 @@ paths: - admin summary: Delete user by id security: - - BearerAuth: [] + - cookieAuth: [ ] parameters: - $ref: "#/components/parameters/id" responses: @@ -973,28 +999,6 @@ components: example: email: user@localhost schemas: - AuthTokensDTO: - type: object - required: - - accessToken - - refreshToken - properties: - accessToken: - type: string - format: JWT - refreshToken: - type: string - format: JWT - RoleDTO: - type: object - required: - - name - properties: - name: - type: string - enum: - - USER - - ADMIN UserDTO: type: object required: @@ -1023,6 +1027,16 @@ components: type: array items: $ref: "#/components/schemas/RoleDTO" + RoleDTO: + type: object + required: + - name + properties: + name: + type: string + enum: + - USER + - ADMIN AvailabilityDTO: type: object required: @@ -1051,7 +1065,7 @@ components: items: type: string securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT + cookieAuth: + type: apiKey + in: cookie + name: accessToken diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/admin/integration/UserControllerIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/admin/integration/UserControllerIT.java index 8c4e553f..66a2dc2c 100644 --- a/backend/spring-boot/src/test/java/org/bugzkit/api/admin/integration/UserControllerIT.java +++ b/backend/spring-boot/src/test/java/org/bugzkit/api/admin/integration/UserControllerIT.java @@ -12,6 +12,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.Cookie; import java.time.LocalDateTime; import java.util.Set; import org.bugzkit.api.admin.payload.request.PatchUserRequest; @@ -63,7 +64,7 @@ void createUser() throws Exception { .perform( post(Path.ADMIN_USERS) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isCreated()); } @@ -84,7 +85,7 @@ void createUser_throwBadRequest_invalidParameters() throws Exception { .perform( post(Path.ADMIN_USERS) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("API_ERROR_USER_USERNAME_INVALID"))) @@ -108,7 +109,7 @@ void createUser_throwConflict_usernameExists() throws Exception { .perform( post(Path.ADMIN_USERS) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isConflict()) .andExpect(content().string(containsString("API_ERROR_USER_USERNAME_EXISTS"))); @@ -130,7 +131,7 @@ void createUser_throwConflict_emailExists() throws Exception { .perform( post(Path.ADMIN_USERS) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isConflict()) .andExpect(content().string(containsString("API_ERROR_USER_EMAIL_EXISTS"))); @@ -142,7 +143,7 @@ void findAllUsers() throws Exception { .perform( get(Path.ADMIN_USERS) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken))) + .cookie(new Cookie("accessToken", accessToken))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.length()").value(10)) .andExpect(jsonPath("$.total").value(11)); @@ -165,7 +166,7 @@ void findUserById() throws Exception { .perform( get(Path.ADMIN_USERS + "/{id}", 2L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken))) + .cookie(new Cookie("accessToken", accessToken))) .andExpect(status().isOk()) .andReturn() .getResponse() @@ -180,7 +181,7 @@ void findUserById_throwResourceNotFound() throws Exception { .perform( get(Path.ADMIN_USERS + "/{id}", 100L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken))) + .cookie(new Cookie("accessToken", accessToken))) .andExpect(status().isNotFound()) .andExpect(content().string(containsString("API_ERROR_USER_NOT_FOUND"))); } @@ -201,7 +202,7 @@ void updateUser() throws Exception { .perform( put(Path.ADMIN_USERS + "/{id}", 5L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isOk()); } @@ -222,7 +223,7 @@ void updateUser_throwBadRequest_invalidParameters() throws Exception { .perform( put(Path.ADMIN_USERS + "/{id}", 5L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("API_ERROR_USER_USERNAME_INVALID"))) @@ -246,7 +247,7 @@ void updateUser_throwConflict_usernameExists() throws Exception { .perform( put(Path.ADMIN_USERS + "/{id}", 5L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isConflict()) .andExpect(content().string(containsString("API_ERROR_USER_USERNAME_EXISTS"))); @@ -268,7 +269,7 @@ void updateUser_throwConflict_emailExists() throws Exception { .perform( put(Path.ADMIN_USERS + "/{id}", 5L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isConflict()) .andExpect(content().string(containsString("API_ERROR_USER_EMAIL_EXISTS"))); @@ -290,7 +291,7 @@ void patchUser() throws Exception { .perform( patch(Path.ADMIN_USERS + "/{id}", 5L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(patchUserRequest))) .andExpect(status().isOk()); } @@ -311,7 +312,7 @@ void patchUser_throwBadRequest_invalidParameters() throws Exception { .perform( patch(Path.ADMIN_USERS + "/{id}", 5L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(patchUserRequest))) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("API_ERROR_USER_USERNAME_INVALID"))) @@ -335,7 +336,7 @@ void patchUser_throwConflict_usernameExists() throws Exception { .perform( patch(Path.ADMIN_USERS + "/{id}", 5L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(patchUserRequest))) .andExpect(status().isConflict()) .andExpect(content().string(containsString("API_ERROR_USER_USERNAME_EXISTS"))); @@ -357,7 +358,7 @@ void patchUser_throwConflict_emailExists() throws Exception { .perform( patch(Path.ADMIN_USERS + "/{id}", 5L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken)) + .cookie(new Cookie("accessToken", accessToken)) .content(objectMapper.writeValueAsString(patchUserRequest))) .andExpect(status().isConflict()) .andExpect(content().string(containsString("API_ERROR_USER_EMAIL_EXISTS"))); @@ -369,7 +370,7 @@ void deleteUser() throws Exception { .perform( delete(Path.ADMIN_USERS + "/{id}", 10L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken))) + .cookie(new Cookie("accessToken", accessToken))) .andExpect(status().isNoContent()); } } diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/auth/integration/AuthControllerIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/auth/integration/AuthControllerIT.java index 258f937b..74b6e91e 100644 --- a/backend/spring-boot/src/test/java/org/bugzkit/api/auth/integration/AuthControllerIT.java +++ b/backend/spring-boot/src/test/java/org/bugzkit/api/auth/integration/AuthControllerIT.java @@ -8,15 +8,16 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.Cookie; import org.bugzkit.api.auth.jwt.service.impl.ResetPasswordTokenServiceImpl; import org.bugzkit.api.auth.jwt.service.impl.VerificationTokenServiceImpl; import org.bugzkit.api.auth.payload.request.AuthTokensRequest; import org.bugzkit.api.auth.payload.request.ForgotPasswordRequest; -import org.bugzkit.api.auth.payload.request.RefreshAuthTokensRequest; import org.bugzkit.api.auth.payload.request.RegisterUserRequest; import org.bugzkit.api.auth.payload.request.ResetPasswordRequest; import org.bugzkit.api.auth.payload.request.VerificationEmailRequest; @@ -33,6 +34,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; @@ -134,9 +136,8 @@ void authenticate() throws Exception { post(Path.AUTH + "/tokens") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(authTokensRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").isString()) - .andExpect(jsonPath("$.refreshToken").isString()); + .andExpect(status().isNoContent()) + .andExpect(header().exists(HttpHeaders.SET_COOKIE)); } @Test @@ -175,7 +176,7 @@ void deleteTokens() throws Exception { .perform( delete(Path.AUTH + "/tokens") .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) + .cookie(new Cookie("accessToken", authTokens.accessToken()))) .andExpect(status().isNoContent()); invalidAccessToken(authTokens.accessToken()); invalidRefreshToken(authTokens.refreshToken()); @@ -189,8 +190,9 @@ void deleteTokensOnAllDevices() throws Exception { .perform( delete(Path.AUTH + "/tokens/devices") .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) - .andExpect(status().isNoContent()); + .cookie(new Cookie("accessToken", authTokens.accessToken()))) + .andExpect(status().isNoContent()) + .andExpect(header().exists(HttpHeaders.SET_COOKIE)); invalidAccessToken(authTokens.accessToken()); invalidRefreshToken(authTokens.refreshToken()); } @@ -200,18 +202,17 @@ private void invalidAccessToken(String accessToken) throws Exception { .perform( get(Path.PROFILE) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(accessToken))) + .cookie(new Cookie("accessToken", accessToken))) .andExpect(status().isUnauthorized()) .andExpect(content().string(containsString("API_ERROR_AUTH_UNAUTHORIZED"))); } private void invalidRefreshToken(String refreshToken) throws Exception { - final var refreshTokenRequest = new RefreshAuthTokensRequest(refreshToken); mockMvc .perform( post(Path.AUTH + "/tokens/refresh") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(refreshTokenRequest))) + .cookie(new Cookie("refreshToken", refreshToken))) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("API_ERROR_AUTH_TOKEN_INVALID"))); } @@ -219,13 +220,13 @@ private void invalidRefreshToken(String refreshToken) throws Exception { @Test void refreshToken() throws Exception { final var authTokens = IntegrationTestUtil.authTokens(mockMvc, objectMapper, "update1"); - final var refreshTokenRequest = new RefreshAuthTokensRequest(authTokens.refreshToken()); mockMvc .perform( post(Path.AUTH + "/tokens/refresh") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(refreshTokenRequest))) - .andExpect(status().isOk()); + .cookie(new Cookie("refreshToken", authTokens.refreshToken()))) + .andExpect(status().isNoContent()) + .andExpect(header().exists(HttpHeaders.SET_COOKIE)); } @Test @@ -270,7 +271,7 @@ void resetPassword() throws Exception { post(Path.AUTH + "/tokens") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(authTokensRequest))) - .andExpect(status().isOk()); + .andExpect(status().isNoContent()); } @Test diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/shared/integration/AccessingResourcesIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/shared/integration/AccessingResourcesIT.java index ffc6a249..bed76bfc 100644 --- a/backend/spring-boot/src/test/java/org/bugzkit/api/shared/integration/AccessingResourcesIT.java +++ b/backend/spring-boot/src/test/java/org/bugzkit/api/shared/integration/AccessingResourcesIT.java @@ -10,6 +10,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.Cookie; import java.util.Set; import org.bugzkit.api.admin.payload.request.PatchUserRequest; import org.bugzkit.api.admin.payload.request.UserRequest; @@ -31,11 +32,10 @@ @ActiveProfiles("test") @SpringBootTest class AccessingResourcesIT extends DatabaseContainers { - @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; - private final String unauthorized = "API_ERROR_AUTH_UNAUTHORIZED"; private final String forbidden = "API_ERROR_AUTH_FORBIDDEN"; + @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; @Test void getProfile_throwUnauthorized_userNotSignedIn() throws Exception { @@ -94,7 +94,7 @@ void createUser_throwForbidden_userNotAdmin() throws Exception { .perform( post(Path.ADMIN_USERS) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isForbidden()) .andExpect(content().string(containsString(forbidden))); @@ -115,7 +115,7 @@ void getUsers_throwForbidden_userNotAdmin() throws Exception { .perform( get(Path.ADMIN_USERS) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) + .cookie(new Cookie("accessToken", authTokens.accessToken()))) .andExpect(status().isForbidden()) .andExpect(content().string(containsString(forbidden))); } @@ -135,7 +135,7 @@ void getUserById_throwForbidden_userNotAdmin() throws Exception { .perform( get(Path.ADMIN_USERS + "/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) + .cookie(new Cookie("accessToken", authTokens.accessToken()))) .andExpect(status().isForbidden()) .andExpect(content().string(containsString(forbidden))); } @@ -165,7 +165,7 @@ void updateUserById_throwForbidden_userNotAdmin() throws Exception { .perform( put(Path.ADMIN_USERS + "/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(userRequest))) .andExpect(status().isForbidden()) .andExpect(content().string(containsString(forbidden))); @@ -187,7 +187,7 @@ void patchUserById_throwForbidden_userNotAdmin() throws Exception { .perform( patch(Path.ADMIN_USERS + "/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(patchUserRequest))) .andExpect(status().isForbidden()) .andExpect(content().string(containsString(forbidden))); @@ -208,7 +208,7 @@ void deleteUserById_throwForbidden_userNotAdmin() throws Exception { .perform( delete(Path.ADMIN_USERS + "/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) + .cookie(new Cookie("accessToken", authTokens.accessToken()))) .andExpect(status().isForbidden()) .andExpect(content().string(containsString(forbidden))); } @@ -228,7 +228,7 @@ void getRoles_throwForbidden_userNotAdmin() throws Exception { .perform( get(Path.ROLES) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) + .cookie(new Cookie("accessToken", authTokens.accessToken()))) .andExpect(status().isForbidden()) .andExpect(content().string(containsString(forbidden))); } diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/shared/util/IntegrationTestUtil.java b/backend/spring-boot/src/test/java/org/bugzkit/api/shared/util/IntegrationTestUtil.java index 8d075d30..6e1c2d26 100644 --- a/backend/spring-boot/src/test/java/org/bugzkit/api/shared/util/IntegrationTestUtil.java +++ b/backend/spring-boot/src/test/java/org/bugzkit/api/shared/util/IntegrationTestUtil.java @@ -1,6 +1,5 @@ package org.bugzkit.api.shared.util; -import static org.springframework.security.web.http.SecurityHeaders.bearerToken; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -8,19 +7,12 @@ import org.bugzkit.api.auth.payload.dto.AuthTokensDTO; import org.bugzkit.api.auth.payload.request.AuthTokensRequest; import org.bugzkit.api.shared.constants.Path; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; public class IntegrationTestUtil { private IntegrationTestUtil() {} - public static HttpHeaders authHeader(String accessToken) { - final var headers = new HttpHeaders(); - bearerToken(accessToken).accept(headers); - return headers; - } - public static AuthTokensDTO authTokens( MockMvc mockMvc, ObjectMapper objectMapper, String username) throws Exception { final var authTokensRequest = new AuthTokensRequest(username, "qwerty123"); @@ -30,10 +22,11 @@ public static AuthTokensDTO authTokens( post(Path.AUTH + "/tokens") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(authTokensRequest))) - .andExpect(status().isOk()) + .andExpect(status().isNoContent()) .andReturn() - .getResponse() - .getContentAsString(); - return objectMapper.readValue(response, AuthTokensDTO.class); + .getResponse(); + return new AuthTokensDTO( + response.getCookie("accessToken").getValue(), + response.getCookie("refreshToken").getValue()); } } diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/ProfileControllerIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/ProfileControllerIT.java index 3e3094fc..530c47da 100644 --- a/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/ProfileControllerIT.java +++ b/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/ProfileControllerIT.java @@ -12,6 +12,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.Cookie; import org.bugzkit.api.shared.config.DatabaseContainers; import org.bugzkit.api.shared.constants.Path; import org.bugzkit.api.shared.email.service.EmailService; @@ -45,7 +46,7 @@ void findProfile() throws Exception { .perform( get(Path.PROFILE) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) + .cookie(new Cookie("accessToken", authTokens.accessToken()))) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("user")) .andExpect(jsonPath("$.email").value("user@localhost")); @@ -59,7 +60,7 @@ void patchProfile_newUsernameAndEmail() throws Exception { .perform( patch(Path.PROFILE) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(patchProfileRequest))) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("updated1")) @@ -76,7 +77,7 @@ void patchProfile_sameUsernameAndEmail() throws Exception { .perform( patch(Path.PROFILE) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(patchProfileRequest))) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("update2")) @@ -91,7 +92,7 @@ void patchProfile_throwConflict_usernameExists() throws Exception { .perform( patch(Path.PROFILE) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(patchProfileRequest))) .andExpect(status().isConflict()) .andExpect(content().string(containsString("API_ERROR_USER_USERNAME_EXISTS"))); @@ -105,7 +106,7 @@ void patchProfile_throwConflict_emailExists() throws Exception { .perform( patch(Path.PROFILE) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(patchProfileRequest))) .andExpect(status().isConflict()) .andExpect(content().string(containsString("API_ERROR_USER_EMAIL_EXISTS"))); @@ -119,7 +120,7 @@ void patchProfile_throwBadRequest_invalidParameters() throws Exception { .perform( patch(Path.PROFILE) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(patchProfileRequest))) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("API_ERROR_USER_EMAIL_INVALID"))) @@ -133,7 +134,7 @@ void deleteProfile() throws Exception { .perform( delete(Path.PROFILE) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) + .cookie(new Cookie("accessToken", authTokens.accessToken()))) .andExpect(status().isNoContent()); } @@ -146,7 +147,7 @@ void changePassword() throws Exception { .perform( patch(Path.PROFILE + "/password") .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(changePasswordRequest))) .andExpect(status().isNoContent()); } @@ -160,7 +161,7 @@ void changePassword_throwBadRequest_wrongCurrentPassword() throws Exception { .perform( patch(Path.PROFILE + "/password") .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(changePasswordRequest))) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("API_ERROR_USER_CURRENT_PASSWORD_WRONG"))); @@ -175,7 +176,7 @@ void changePassword_throwBadRequest_passwordsDoNotMatch() throws Exception { .perform( patch(Path.PROFILE + "/password") .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(changePasswordRequest))) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("API_ERROR_USER_PASSWORDS_DO_NOT_MATCH"))); @@ -189,7 +190,7 @@ void changePassword_throwBadRequest_invalidParameters() throws Exception { .perform( patch(Path.PROFILE + "/password") .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken())) + .cookie(new Cookie("accessToken", authTokens.accessToken())) .content(objectMapper.writeValueAsString(changePasswordRequest))) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("API_ERROR_USER_PASSWORD_INVALID"))); diff --git a/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/RoleControllerIT.java b/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/RoleControllerIT.java index 46d9053f..98aa8390 100644 --- a/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/RoleControllerIT.java +++ b/backend/spring-boot/src/test/java/org/bugzkit/api/user/integration/RoleControllerIT.java @@ -5,6 +5,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.Cookie; import org.bugzkit.api.shared.config.DatabaseContainers; import org.bugzkit.api.shared.constants.Path; import org.bugzkit.api.shared.util.IntegrationTestUtil; @@ -32,7 +33,7 @@ void findAllRoles() throws Exception { .perform( get(Path.ROLES) .contentType(MediaType.APPLICATION_JSON) - .headers(IntegrationTestUtil.authHeader(authTokens.accessToken()))) + .cookie(new Cookie("accessToken", authTokens.accessToken()))) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(2)); } diff --git a/frontend/svelte-kit/package.json b/frontend/svelte-kit/package.json index 14c6db12..2ea45b8d 100644 --- a/frontend/svelte-kit/package.json +++ b/frontend/svelte-kit/package.json @@ -27,6 +27,7 @@ "@sveltejs/vite-plugin-svelte": "5.0.3", "@types/eslint": "9.6.1", "@types/jsonwebtoken": "9.0.7", + "@types/set-cookie-parser": "2.4.10", "autoprefixer": "10.4.20", "bits-ui": "1.0.0-next.78", "clsx": "2.1.1", @@ -42,6 +43,7 @@ "prettier": "3.4.2", "prettier-plugin-svelte": "3.3.3", "prettier-plugin-tailwindcss": "0.6.10", + "set-cookie-parser": "2.7.1", "svelte": "5.19.1", "svelte-check": "4.1.4", "svelte-sonner": "0.3.28", diff --git a/frontend/svelte-kit/pnpm-lock.yaml b/frontend/svelte-kit/pnpm-lock.yaml index e9ad23ab..edac4ea7 100644 --- a/frontend/svelte-kit/pnpm-lock.yaml +++ b/frontend/svelte-kit/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@types/jsonwebtoken': specifier: 9.0.7 version: 9.0.7 + '@types/set-cookie-parser': + specifier: 2.4.10 + version: 2.4.10 autoprefixer: specifier: 10.4.20 version: 10.4.20(postcss@8.5.1) @@ -80,6 +83,9 @@ importers: prettier-plugin-tailwindcss: specifier: 0.6.10 version: 0.6.10(prettier-plugin-svelte@3.3.3(prettier@3.4.2)(svelte@5.19.1))(prettier@3.4.2) + set-cookie-parser: + specifier: 2.7.1 + version: 2.7.1 svelte: specifier: 5.19.1 version: 5.19.1 @@ -827,6 +833,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/validator@13.12.2': resolution: {integrity: sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==} @@ -3276,6 +3285,10 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 22.10.7 + '@types/validator@13.12.2': optional: true diff --git a/frontend/svelte-kit/src/hooks.server.ts b/frontend/svelte-kit/src/hooks.server.ts index 603e5cc2..46f73619 100644 --- a/frontend/svelte-kit/src/hooks.server.ts +++ b/frontend/svelte-kit/src/hooks.server.ts @@ -1,16 +1,10 @@ import { env } from '$env/dynamic/private'; import { i18n } from '$lib/i18n'; -import type { AuthTokens } from '$lib/models/auth/auth-tokens'; import type { JwtPayload } from '$lib/models/auth/jwt-payload'; import { RoleName } from '$lib/models/user/role'; import { languageTag } from '$lib/paraglide/runtime'; import { makeRequest } from '$lib/server/apis/api'; -import { - HttpRequest, - removeAuth, - setAccessTokenCookie, - setRefreshTokenCookie, -} from '$lib/server/utils/util'; +import { HttpRequest, removeAuth } from '$lib/server/utils/util'; import { redirect, type Cookies, type Handle } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import jwt from 'jsonwebtoken'; @@ -30,18 +24,18 @@ async function tryToRefreshToken(cookies: Cookies, locals: App.Locals): Promise< try { const refreshToken = cookies.get('refreshToken') ?? ''; jwt.verify(refreshToken, env.JWT_SECRET); - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/auth/tokens/refresh', - body: JSON.stringify({ refreshToken }), - }); + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/auth/tokens/refresh', + }, + cookies, + ); if ('error' in response) { removeAuth(cookies, locals); } else { - const { accessToken, refreshToken } = response as AuthTokens; - setAccessTokenCookie(cookies, accessToken); - setRefreshTokenCookie(cookies, refreshToken); + const accessToken = cookies.get('accessToken') ?? ''; const { iss } = jwt.decode(accessToken) as JwtPayload; locals.userId = iss; } diff --git a/frontend/svelte-kit/src/lib/server/apis/api.ts b/frontend/svelte-kit/src/lib/server/apis/api.ts index d902900d..b5824b31 100644 --- a/frontend/svelte-kit/src/lib/server/apis/api.ts +++ b/frontend/svelte-kit/src/lib/server/apis/api.ts @@ -1,33 +1,43 @@ import { PUBLIC_API_URL } from '$env/static/public'; import { ErrorCode, type ErrorMessage } from '$lib/models/shared/error-message'; import * as m from '$lib/paraglide/messages.js'; -import type { HttpRequest } from '$lib/server/utils/util'; -import { fail } from '@sveltejs/kit'; -import { type SuperValidated, setError } from 'sveltekit-superforms'; +import { setCookieFromString, type HttpRequest } from '$lib/server/utils/util'; +import { fail, type Cookies } from '@sveltejs/kit'; +import { setError, type SuperValidated } from 'sveltekit-superforms'; interface RequestParams { method: HttpRequest; path: string; body?: string; - auth?: string; } -export async function makeRequest(params: RequestParams): Promise { +export async function makeRequest( + params: RequestParams, + cookies: Cookies, +): Promise { const opts: RequestInit = {}; - let headers: HeadersInit = {}; + const headers = new Headers(); + + const accessToken = cookies.get('accessToken'); + const refreshToken = cookies.get('refreshToken'); + if (accessToken) headers.append('Cookie', `accessToken=${accessToken}`); + if (refreshToken) headers.append('Cookie', `refreshToken=${refreshToken}`); if (params.body) { - headers = { 'Content-Type': 'application/json' }; + headers.append('Content-Type', 'application/json'); opts.body = params.body; } - if (params.auth) headers = { ...headers, Authorization: `Bearer ${params.auth}` }; - opts.method = params.method; opts.headers = headers; const response = await fetch(`${PUBLIC_API_URL}${params.path}`, opts); + const setCookies = response.headers.getSetCookie(); + setCookies.forEach((cookie: string) => { + setCookieFromString(cookie, cookies); + }); + if (!response.ok) return (await response.json()) as ErrorMessage; const text = await response.text(); diff --git a/frontend/svelte-kit/src/lib/server/utils/util.ts b/frontend/svelte-kit/src/lib/server/utils/util.ts index ef1d15df..98bf072d 100644 --- a/frontend/svelte-kit/src/lib/server/utils/util.ts +++ b/frontend/svelte-kit/src/lib/server/utils/util.ts @@ -2,6 +2,7 @@ import type { JwtPayload } from '$lib/models/auth/jwt-payload'; import { RoleName } from '$lib/models/user/role'; import { type Cookies } from '@sveltejs/kit'; import jwt from 'jsonwebtoken'; +import * as setCookieParser from 'set-cookie-parser'; export enum HttpRequest { GET = 'GET', @@ -11,25 +12,31 @@ export enum HttpRequest { DELETE = 'DELETE', } -export function setAccessTokenCookie(cookies: Cookies, accessToken: string): void { - const { exp } = jwt.decode(accessToken) as JwtPayload; - cookies.set('accessToken', accessToken, { - httpOnly: true, - path: '/', - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - expires: new Date(exp * 1000), - }); -} +export function setCookieFromString(cookie: string, cookies: Cookies) { + if (cookie === '') return; + + const parsed = setCookieParser.parseString(cookie); + const { name, value, path, sameSite, secure, httpOnly, ...opts } = parsed; + + if (name === undefined || value === undefined || path === undefined) return; + + const normalizedSameSite = (() => { + if (sameSite === undefined || typeof sameSite === 'boolean') { + return sameSite; + } + const lower = sameSite.toLowerCase(); + if (lower === 'lax' || lower === 'strict' || lower === 'none') { + return lower; + } + return undefined; + })(); -export function setRefreshTokenCookie(cookies: Cookies, refreshToken: string): void { - const { exp } = jwt.decode(refreshToken) as JwtPayload; - cookies.set('refreshToken', refreshToken, { - httpOnly: true, - path: '/', - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - expires: new Date(exp * 1000), + cookies.set(name, value, { + ...opts, + path, + sameSite: normalizedSameSite, + secure: secure ?? false, + httpOnly: httpOnly ?? false, }); } diff --git a/frontend/svelte-kit/src/routes/+layout.server.ts b/frontend/svelte-kit/src/routes/+layout.server.ts index 3f41905e..990b5af3 100644 --- a/frontend/svelte-kit/src/routes/+layout.server.ts +++ b/frontend/svelte-kit/src/routes/+layout.server.ts @@ -7,14 +7,15 @@ import type { LayoutServerLoad } from './$types'; export const load = (async ({ locals, cookies }) => { if (!locals.userId) return { profile: null }; - const accessToken = cookies.get('accessToken'); - const response = await makeRequest({ - method: HttpRequest.GET, - path: `/profile`, - auth: accessToken, - }); + const response = await makeRequest( + { + method: HttpRequest.GET, + path: `/profile`, + }, + cookies, + ); if ('error' in response) error(response.status, { message: response.error }); - return { profile: response as Profile, isAdmin: isAdmin(accessToken) }; + return { profile: response as Profile, isAdmin: isAdmin(cookies.get('accessToken')) }; }) satisfies LayoutServerLoad; diff --git a/frontend/svelte-kit/src/routes/admin/user/+page.server.ts b/frontend/svelte-kit/src/routes/admin/user/+page.server.ts index 69c31e11..c23ea395 100644 --- a/frontend/svelte-kit/src/routes/admin/user/+page.server.ts +++ b/frontend/svelte-kit/src/routes/admin/user/+page.server.ts @@ -17,19 +17,23 @@ export const load = (async ({ cookies, url }) => { if (page < 1) page = 1; if (size < 1) size = 10; - const usersResponse = await makeRequest({ - method: HttpRequest.GET, - path: `/admin/users?page=${page}&size=${size}`, - auth: cookies.get('accessToken'), - }); + const usersResponse = await makeRequest( + { + method: HttpRequest.GET, + path: `/admin/users?page=${page}&size=${size}`, + }, + cookies, + ); if ('error' in usersResponse) error(usersResponse.status, { message: usersResponse.error }); - const rolesResponse = await makeRequest({ - method: HttpRequest.GET, - path: `/roles`, - auth: cookies.get('accessToken'), - }); + const rolesResponse = await makeRequest( + { + method: HttpRequest.GET, + path: `/roles`, + }, + cookies, + ); if ('error' in rolesResponse) error(rolesResponse.status, { message: rolesResponse.error }); @@ -56,12 +60,14 @@ export const actions = { const form = await superValidate(request, zod(createSchema)); if (!form.valid) return fail(400, { form }); - const response = await makeRequest({ - method: HttpRequest.POST, - path: `/admin/users`, - auth: cookies.get('accessToken'), - body: JSON.stringify(form.data), - }); + const response = await makeRequest( + { + method: HttpRequest.POST, + path: `/admin/users`, + body: JSON.stringify(form.data), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); @@ -70,12 +76,14 @@ export const actions = { activate: async ({ request, cookies }) => { const form = await superValidate(request, zod(actionSchema)); - const response = await makeRequest({ - method: HttpRequest.PATCH, - path: `/admin/users/${form.data.id}`, - auth: cookies.get('accessToken'), - body: JSON.stringify({ active: true }), - }); + const response = await makeRequest( + { + method: HttpRequest.PATCH, + path: `/admin/users/${form.data.id}`, + body: JSON.stringify({ active: true }), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); @@ -84,12 +92,14 @@ export const actions = { deactivate: async ({ request, cookies }) => { const form = await superValidate(request, zod(actionSchema)); - const response = await makeRequest({ - method: HttpRequest.PATCH, - path: `/admin/users/${form.data.id}`, - auth: cookies.get('accessToken'), - body: JSON.stringify({ active: false }), - }); + const response = await makeRequest( + { + method: HttpRequest.PATCH, + path: `/admin/users/${form.data.id}`, + body: JSON.stringify({ active: false }), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); @@ -98,12 +108,14 @@ export const actions = { unlock: async ({ request, cookies }) => { const form = await superValidate(request, zod(actionSchema)); - const response = await makeRequest({ - method: HttpRequest.PATCH, - path: `/admin/users/${form.data.id}`, - auth: cookies.get('accessToken'), - body: JSON.stringify({ lock: false }), - }); + const response = await makeRequest( + { + method: HttpRequest.PATCH, + path: `/admin/users/${form.data.id}`, + body: JSON.stringify({ lock: false }), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); @@ -112,12 +124,14 @@ export const actions = { lock: async ({ request, cookies }) => { const form = await superValidate(request, zod(actionSchema)); - const response = await makeRequest({ - method: HttpRequest.PATCH, - path: `/admin/users/${form.data.id}`, - auth: cookies.get('accessToken'), - body: JSON.stringify({ lock: true }), - }); + const response = await makeRequest( + { + method: HttpRequest.PATCH, + path: `/admin/users/${form.data.id}`, + body: JSON.stringify({ lock: true }), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); @@ -126,11 +140,13 @@ export const actions = { delete: async ({ request, cookies }) => { const form = await superValidate(request, zod(actionSchema)); - const response = await makeRequest({ - method: HttpRequest.DELETE, - path: `/admin/users/${form.data.id}`, - auth: cookies.get('accessToken'), - }); + const response = await makeRequest( + { + method: HttpRequest.DELETE, + path: `/admin/users/${form.data.id}`, + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); @@ -139,12 +155,14 @@ export const actions = { changeRoles: async ({ request, cookies }) => { const form = await superValidate(request, zod(changeRolesSchema)); - const response = await makeRequest({ - method: HttpRequest.PATCH, - path: `/admin/users/${form.data.id}`, - auth: cookies.get('accessToken'), - body: JSON.stringify({ roleNames: form.data.roleNames }), - }); + const response = await makeRequest( + { + method: HttpRequest.PATCH, + path: `/admin/users/${form.data.id}`, + body: JSON.stringify({ roleNames: form.data.roleNames }), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); diff --git a/frontend/svelte-kit/src/routes/auth/forgot-password/+page.server.ts b/frontend/svelte-kit/src/routes/auth/forgot-password/+page.server.ts index 22be4255..75665b21 100644 --- a/frontend/svelte-kit/src/routes/auth/forgot-password/+page.server.ts +++ b/frontend/svelte-kit/src/routes/auth/forgot-password/+page.server.ts @@ -16,15 +16,18 @@ export const load = (async ({ locals }) => { }) satisfies PageServerLoad; export const actions = { - forgotPassword: async ({ request }) => { + forgotPassword: async ({ request, cookies }) => { const form = await superValidate(request, zod(forgotPasswordSchema)); if (!form.valid) return fail(400, { form }); - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/auth/password/forgot', - body: JSON.stringify(form.data), - }); + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/auth/password/forgot', + body: JSON.stringify(form.data), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); diff --git a/frontend/svelte-kit/src/routes/auth/resend-confirmation-email/+server.ts b/frontend/svelte-kit/src/routes/auth/resend-confirmation-email/+server.ts index 305e1176..00327cbd 100644 --- a/frontend/svelte-kit/src/routes/auth/resend-confirmation-email/+server.ts +++ b/frontend/svelte-kit/src/routes/auth/resend-confirmation-email/+server.ts @@ -3,15 +3,18 @@ import { HttpRequest } from '$lib/server/utils/util'; import { error, redirect } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -export const GET = (async ({ locals, url }) => { +export const GET = (async ({ locals, url, cookies }) => { if (locals.userId) redirect(302, '/'); const usernameOrEmail = url.searchParams.get('usernameOrEmail'); - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/auth/verification-email', - body: JSON.stringify({ usernameOrEmail }), - }); + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/auth/verification-email', + body: JSON.stringify({ usernameOrEmail }), + }, + cookies, + ); if ('error' in response) error(response.status, { message: response.error }); diff --git a/frontend/svelte-kit/src/routes/auth/reset-password/+page.server.ts b/frontend/svelte-kit/src/routes/auth/reset-password/+page.server.ts index fda950ac..34e359c1 100644 --- a/frontend/svelte-kit/src/routes/auth/reset-password/+page.server.ts +++ b/frontend/svelte-kit/src/routes/auth/reset-password/+page.server.ts @@ -18,7 +18,7 @@ export const load = (async ({ locals, url }) => { }) satisfies PageServerLoad; export const actions = { - resetPassword: async ({ request }) => { + resetPassword: async ({ request, cookies }) => { const form = await superValidate(request, zod(resetPasswordSchema)); if (!form.valid) return fail(400, { form }); @@ -28,11 +28,14 @@ export const actions = { return setError(form, m.auth_tokenInvalid()); } - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/auth/password/reset', - body: JSON.stringify(form.data), - }); + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/auth/password/reset', + body: JSON.stringify(form.data), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); diff --git a/frontend/svelte-kit/src/routes/auth/sign-in/+page.server.ts b/frontend/svelte-kit/src/routes/auth/sign-in/+page.server.ts index 6803f9f1..82c67224 100644 --- a/frontend/svelte-kit/src/routes/auth/sign-in/+page.server.ts +++ b/frontend/svelte-kit/src/routes/auth/sign-in/+page.server.ts @@ -1,6 +1,5 @@ -import type { AuthTokens } from '$lib/models/auth/auth-tokens'; import { apiErrors, makeRequest } from '$lib/server/apis/api'; -import { HttpRequest, setAccessTokenCookie, setRefreshTokenCookie } from '$lib/server/utils/util'; +import { HttpRequest } from '$lib/server/utils/util'; import { fail, redirect } from '@sveltejs/kit'; import { superValidate } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; @@ -20,18 +19,17 @@ export const actions = { const form = await superValidate(request, zod(signInSchema)); if (!form.valid) return fail(400, { form }); - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/auth/tokens', - body: JSON.stringify(form.data), - }); + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/auth/tokens', + body: JSON.stringify(form.data), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); - const { accessToken, refreshToken } = response as AuthTokens; - setAccessTokenCookie(cookies, accessToken); - setRefreshTokenCookie(cookies, refreshToken); - redirect(302, '/'); }, } satisfies Actions; diff --git a/frontend/svelte-kit/src/routes/auth/sign-out-from-all-devices/+server.ts b/frontend/svelte-kit/src/routes/auth/sign-out-from-all-devices/+server.ts index a2812907..e7c7a4a6 100644 --- a/frontend/svelte-kit/src/routes/auth/sign-out-from-all-devices/+server.ts +++ b/frontend/svelte-kit/src/routes/auth/sign-out-from-all-devices/+server.ts @@ -4,11 +4,13 @@ import { error, redirect } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; export const GET = (async ({ locals, cookies }) => { - const response = await makeRequest({ - method: HttpRequest.DELETE, - path: '/auth/tokens/devices', - auth: cookies.get('accessToken'), - }); + const response = await makeRequest( + { + method: HttpRequest.DELETE, + path: '/auth/tokens/devices', + }, + cookies, + ); if ('error' in response) error(response.status, { message: response.error }); diff --git a/frontend/svelte-kit/src/routes/auth/sign-out/+server.ts b/frontend/svelte-kit/src/routes/auth/sign-out/+server.ts index 1c9b2025..6d2bb186 100644 --- a/frontend/svelte-kit/src/routes/auth/sign-out/+server.ts +++ b/frontend/svelte-kit/src/routes/auth/sign-out/+server.ts @@ -4,11 +4,13 @@ import { error, redirect } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; export const GET = (async ({ locals, cookies }) => { - const response = await makeRequest({ - method: HttpRequest.DELETE, - path: '/auth/tokens', - auth: cookies.get('accessToken'), - }); + const response = await makeRequest( + { + method: HttpRequest.DELETE, + path: '/auth/tokens', + }, + cookies, + ); if ('error' in response) error(response.status, { message: response.error }); diff --git a/frontend/svelte-kit/src/routes/auth/sign-up/+page.server.ts b/frontend/svelte-kit/src/routes/auth/sign-up/+page.server.ts index c8c92204..fb8a7858 100644 --- a/frontend/svelte-kit/src/routes/auth/sign-up/+page.server.ts +++ b/frontend/svelte-kit/src/routes/auth/sign-up/+page.server.ts @@ -2,7 +2,7 @@ import type { Availability } from '$lib/models/shared/availability'; import * as m from '$lib/paraglide/messages.js'; import { apiErrors, makeRequest } from '$lib/server/apis/api'; import { HttpRequest } from '$lib/server/utils/util'; -import { fail, redirect } from '@sveltejs/kit'; +import { fail, redirect, type Cookies } from '@sveltejs/kit'; import { setError, superValidate } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; import type { Actions, PageServerLoad } from './$types'; @@ -17,25 +17,28 @@ export const load = (async ({ locals }) => { }) satisfies PageServerLoad; export const actions = { - signUp: async ({ request }) => { + signUp: async ({ request, cookies }) => { const form = await superValidate(request, zod(signUpSchema)); if (!form.valid) return fail(400, { form }); - if (!(await usernameAvailability(form.data.username))) { + if (!(await usernameAvailability(form.data.username, cookies))) { setError(form, 'username', m.auth_usernameExists()); form.valid = false; } - if (!(await emailAvailability(form.data.email))) { + if (!(await emailAvailability(form.data.email, cookies))) { setError(form, 'email', m.auth_emailExists()); form.valid = false; } if (!form.valid) return fail(409, { form }); - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/auth/register', - body: JSON.stringify(form.data), - }); + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/auth/register', + body: JSON.stringify(form.data), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); @@ -43,20 +46,26 @@ export const actions = { }, } satisfies Actions; -const usernameAvailability = async (username: string) => { - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/users/username/availability', - body: JSON.stringify({ username }), - }); +const usernameAvailability = async (username: string, cookies: Cookies) => { + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/users/username/availability', + body: JSON.stringify({ username }), + }, + cookies, + ); return (response as Availability).available; }; -const emailAvailability = async (email: string) => { - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/users/email/availability', - body: JSON.stringify({ email }), - }); +const emailAvailability = async (email: string, cookies: Cookies) => { + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/users/email/availability', + body: JSON.stringify({ email }), + }, + cookies, + ); return (response as Availability).available; }; diff --git a/frontend/svelte-kit/src/routes/auth/verify-email/+server.ts b/frontend/svelte-kit/src/routes/auth/verify-email/+server.ts index 7bbc4899..13362640 100644 --- a/frontend/svelte-kit/src/routes/auth/verify-email/+server.ts +++ b/frontend/svelte-kit/src/routes/auth/verify-email/+server.ts @@ -3,15 +3,18 @@ import { HttpRequest } from '$lib/server/utils/util'; import { error, redirect } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -export const GET = (async ({ locals, url }) => { +export const GET = (async ({ locals, url, cookies }) => { if (locals.userId) redirect(302, '/'); const token = url.searchParams.get('token'); - const response = await makeRequest({ - method: HttpRequest.POST, - path: '/auth/verify-email', - body: JSON.stringify({ token }), - }); + const response = await makeRequest( + { + method: HttpRequest.POST, + path: '/auth/verify-email', + body: JSON.stringify({ token }), + }, + cookies, + ); if ('error' in response) error(response.status, { message: response.error }); diff --git a/frontend/svelte-kit/src/routes/profile/settings/+page.server.ts b/frontend/svelte-kit/src/routes/profile/settings/+page.server.ts index 2a7532b6..623fea9a 100644 --- a/frontend/svelte-kit/src/routes/profile/settings/+page.server.ts +++ b/frontend/svelte-kit/src/routes/profile/settings/+page.server.ts @@ -1,12 +1,6 @@ -import type { AuthTokens } from '$lib/models/auth/auth-tokens'; import * as m from '$lib/paraglide/messages.js'; import { apiErrors, makeRequest } from '$lib/server/apis/api'; -import { - HttpRequest, - removeAuth, - setAccessTokenCookie, - setRefreshTokenCookie, -} from '$lib/server/utils/util'; +import { HttpRequest, removeAuth } from '$lib/server/utils/util'; import { redirect } from '@sveltejs/kit'; import { fail, message, superValidate } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; @@ -27,12 +21,14 @@ export const actions = { const form = await superValidate(request, zod(updateProfileSchema)); if (!form.valid) return fail(400, { form }); - const response = await makeRequest({ - method: HttpRequest.PATCH, - path: '/profile', - body: JSON.stringify(form.data), - auth: cookies.get('accessToken'), - }); + const response = await makeRequest( + { + method: HttpRequest.PATCH, + path: '/profile', + body: JSON.stringify(form.data), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); @@ -42,40 +38,43 @@ export const actions = { const form = await superValidate(request, zod(changePasswordSchema)); if (!form.valid) return fail(400, { form }); - const response = await makeRequest({ - method: HttpRequest.PATCH, - path: '/profile/password', - body: JSON.stringify(form.data), - auth: cookies.get('accessToken'), - }); + const response = await makeRequest( + { + method: HttpRequest.PATCH, + path: '/profile/password', + body: JSON.stringify(form.data), + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); - const signInResponse = await makeRequest({ - method: HttpRequest.POST, - path: '/auth/tokens', - body: JSON.stringify({ - usernameOrEmail: url.searchParams.get('username'), - password: form.data.newPassword, - }), - }); + const signInResponse = await makeRequest( + { + method: HttpRequest.POST, + path: '/auth/tokens', + body: JSON.stringify({ + usernameOrEmail: url.searchParams.get('username'), + password: form.data.newPassword, + }), + }, + cookies, + ); if ('error' in signInResponse) return apiErrors(signInResponse, form); - const { accessToken, refreshToken } = signInResponse as AuthTokens; - setAccessTokenCookie(cookies, accessToken); - setRefreshTokenCookie(cookies, refreshToken); - return message(form, m.profile_changePasswordSuccess()); }, delete: async ({ request, cookies, locals }) => { const form = await superValidate(request, zod(deleteSchema)); - const response = await makeRequest({ - method: HttpRequest.DELETE, - path: '/profile', - auth: cookies.get('accessToken'), - }); + const response = await makeRequest( + { + method: HttpRequest.DELETE, + path: '/profile', + }, + cookies, + ); if ('error' in response) return apiErrors(response, form); diff --git a/frontend/svelte-kit/src/routes/user/[name]/+page.server.ts b/frontend/svelte-kit/src/routes/user/[name]/+page.server.ts index d8087060..a6200663 100644 --- a/frontend/svelte-kit/src/routes/user/[name]/+page.server.ts +++ b/frontend/svelte-kit/src/routes/user/[name]/+page.server.ts @@ -4,11 +4,14 @@ import { HttpRequest } from '$lib/server/utils/util'; import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -export const load = (async ({ params }) => { - const response = await makeRequest({ - method: HttpRequest.GET, - path: `/users/username/${params.name}`, - }); +export const load = (async ({ params, cookies }) => { + const response = await makeRequest( + { + method: HttpRequest.GET, + path: `/users/username/${params.name}`, + }, + cookies, + ); if ('error' in response) error(response.status, { message: response.error });