Skip to content

Commit 2c12219

Browse files
committed
enhance API documentation
1 parent c76660c commit 2c12219

20 files changed

+442
-40
lines changed

backend/spring-boot/src/main/java/org/bugzkit/api/admin/controller/UserController.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package org.bugzkit.api.admin.controller;
22

3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.media.Content;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
7+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
38
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
49
import io.swagger.v3.oas.annotations.tags.Tag;
510
import jakarta.validation.Valid;
611
import org.bugzkit.api.admin.payload.request.PatchUserRequest;
712
import org.bugzkit.api.admin.payload.request.UserRequest;
813
import org.bugzkit.api.admin.service.UserService;
914
import org.bugzkit.api.shared.constants.Path;
15+
import org.bugzkit.api.shared.error.ErrorMessage;
1016
import org.bugzkit.api.shared.generic.crud.CrudController;
1117
import org.bugzkit.api.user.payload.dto.UserDTO;
1218
import org.springframework.http.ResponseEntity;
@@ -28,6 +34,30 @@ public UserController(UserService userService) {
2834
this.userService = userService;
2935
}
3036

37+
@Operation(summary = "Partially update a user by ID")
38+
@ApiResponses({
39+
@ApiResponse(responseCode = "200", description = "OK"),
40+
@ApiResponse(
41+
responseCode = "400",
42+
description = "Validation error",
43+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
44+
@ApiResponse(
45+
responseCode = "401",
46+
description = "Unauthorized",
47+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
48+
@ApiResponse(
49+
responseCode = "403",
50+
description = "Forbidden",
51+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
52+
@ApiResponse(
53+
responseCode = "404",
54+
description = "Not found",
55+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
56+
@ApiResponse(
57+
responseCode = "409",
58+
description = "Conflict — username or email already exists",
59+
content = @Content(schema = @Schema(implementation = ErrorMessage.class)))
60+
})
3161
@PatchMapping("/{id}")
3262
public ResponseEntity<UserDTO> patch(
3363
@PathVariable Long id, @Valid @RequestBody PatchUserRequest patchUserRequest) {
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.bugzkit.api.admin.payload.request;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
34
import jakarta.validation.constraints.Email;
45
import jakarta.validation.constraints.Pattern;
56
import java.util.Set;
@@ -11,10 +12,18 @@
1112
@Builder
1213
@FieldMatch(first = "password", second = "confirmPassword", message = "{user.passwordsDoNotMatch}")
1314
public record PatchUserRequest(
14-
@Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}") String username,
15-
@Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL) String email,
16-
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") String password,
17-
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") String confirmPassword,
18-
Boolean active,
19-
Boolean lock,
15+
@Schema(example = "john_doe")
16+
@Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}")
17+
String username,
18+
@Schema(example = "john@example.com")
19+
@Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL)
20+
String email,
21+
@Schema(example = "Secret123!")
22+
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
23+
String password,
24+
@Schema(example = "Secret123!")
25+
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
26+
String confirmPassword,
27+
@Schema(example = "true") Boolean active,
28+
@Schema(example = "false") Boolean lock,
2029
Set<RoleName> roleNames) {}

backend/spring-boot/src/main/java/org/bugzkit/api/admin/payload/request/UserRequest.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.bugzkit.api.admin.payload.request;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
34
import jakarta.validation.constraints.Email;
45
import jakarta.validation.constraints.NotBlank;
56
import jakarta.validation.constraints.NotEmpty;
@@ -14,18 +15,22 @@
1415
@Builder
1516
@FieldMatch(first = "password", second = "confirmPassword", message = "{user.passwordsDoNotMatch}")
1617
public record UserRequest(
17-
@NotBlank(message = "{user.usernameRequired}")
18+
@Schema(example = "john_doe")
19+
@NotBlank(message = "{user.usernameRequired}")
1820
@Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}")
1921
String username,
20-
@NotBlank(message = "{user.emailRequired}")
22+
@Schema(example = "john@example.com")
23+
@NotBlank(message = "{user.emailRequired}")
2124
@Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL)
2225
String email,
23-
@NotBlank(message = "{user.passwordRequired}")
26+
@Schema(example = "Secret123!")
27+
@NotBlank(message = "{user.passwordRequired}")
2428
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
2529
String password,
26-
@NotBlank(message = "{user.passwordRequired}")
30+
@Schema(example = "Secret123!")
31+
@NotBlank(message = "{user.passwordRequired}")
2732
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
2833
String confirmPassword,
29-
@NotNull(message = "{user.activeRequired}") Boolean active,
30-
@NotNull(message = "{user.lockRequired}") Boolean lock,
34+
@Schema(example = "true") @NotNull(message = "{user.activeRequired}") Boolean active,
35+
@Schema(example = "false") @NotNull(message = "{user.lockRequired}") Boolean lock,
3136
@NotEmpty(message = "{user.rolesEmpty}") Set<RoleName> roleNames) {}

backend/spring-boot/src/main/java/org/bugzkit/api/auth/controller/AuthController.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package org.bugzkit.api.auth.controller;
22

3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.media.Content;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
7+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
38
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
49
import io.swagger.v3.oas.annotations.tags.Tag;
510
import jakarta.servlet.http.HttpServletRequest;
@@ -18,6 +23,7 @@
1823
import org.bugzkit.api.auth.util.AuthUtil;
1924
import org.bugzkit.api.auth.util.JwtUtil;
2025
import org.bugzkit.api.shared.constants.Path;
26+
import org.bugzkit.api.shared.error.ErrorMessage;
2127
import org.bugzkit.api.shared.interceptor.RateLimit;
2228
import org.bugzkit.api.user.payload.dto.UserDTO;
2329
import org.springframework.beans.factory.annotation.Value;
@@ -53,13 +59,39 @@ public AuthController(AuthService authService, DeviceService deviceService) {
5359
this.deviceService = deviceService;
5460
}
5561

62+
@Operation(summary = "Register a new user account")
63+
@ApiResponses({
64+
@ApiResponse(responseCode = "201", description = "Registered"),
65+
@ApiResponse(
66+
responseCode = "400",
67+
description = "Validation error",
68+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
69+
@ApiResponse(
70+
responseCode = "409",
71+
description = "Username or email already exists",
72+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
73+
@ApiResponse(responseCode = "429", description = "Too many requests", content = @Content)
74+
})
5675
@RateLimit(requests = 5, duration = 60)
5776
@PostMapping("/register")
5877
public ResponseEntity<UserDTO> register(
5978
@Valid @RequestBody RegisterUserRequest registerUserRequest) {
6079
return new ResponseEntity<>(authService.register(registerUserRequest), HttpStatus.CREATED);
6180
}
6281

82+
@Operation(summary = "Sign in and receive access and refresh token cookies")
83+
@ApiResponses({
84+
@ApiResponse(responseCode = "204", description = "Authenticated — cookies set"),
85+
@ApiResponse(
86+
responseCode = "400",
87+
description = "Validation error",
88+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
89+
@ApiResponse(
90+
responseCode = "401",
91+
description = "Invalid credentials or account inactive/locked",
92+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
93+
@ApiResponse(responseCode = "429", description = "Too many requests", content = @Content)
94+
})
6395
@RateLimit(requests = 10, duration = 60)
6496
@PostMapping("/tokens")
6597
public ResponseEntity<Void> authenticate(
@@ -70,13 +102,23 @@ public ResponseEntity<Void> authenticate(
70102
return setAuthCookies(authTokens);
71103
}
72104

105+
@Operation(summary = "Sign out and clear auth cookies for the current device")
106+
@ApiResponse(responseCode = "204", description = "Signed out — cookies cleared")
73107
@DeleteMapping("/tokens")
74108
public ResponseEntity<Void> deleteTokens(HttpServletRequest request) {
75109
final var accessToken = AuthUtil.getValueFromCookie("accessToken", request);
76110
authService.deleteTokens(accessToken);
77111
return removeAuthCookies();
78112
}
79113

114+
@Operation(summary = "List all active devices for the current user")
115+
@ApiResponses({
116+
@ApiResponse(responseCode = "200", description = "OK"),
117+
@ApiResponse(
118+
responseCode = "401",
119+
description = "Unauthorized",
120+
content = @Content(schema = @Schema(implementation = ErrorMessage.class)))
121+
})
80122
@SecurityRequirement(name = "cookieAuth")
81123
@GetMapping("/tokens/devices")
82124
public ResponseEntity<List<DeviceDTO>> findAllDevices(HttpServletRequest request) {
@@ -85,19 +127,38 @@ public ResponseEntity<List<DeviceDTO>> findAllDevices(HttpServletRequest request
85127
return ResponseEntity.ok(deviceService.findAll(deviceId));
86128
}
87129

130+
@Operation(summary = "Sign out from all devices and clear auth cookies")
131+
@ApiResponse(responseCode = "204", description = "Signed out from all devices")
88132
@DeleteMapping("/tokens/devices")
89133
public ResponseEntity<Void> deleteTokensOnAllDevices() {
90134
authService.deleteTokensOnAllDevices();
91135
return removeAuthCookies();
92136
}
93137

138+
@Operation(summary = "Revoke a specific device and invalidate all access tokens")
139+
@ApiResponses({
140+
@ApiResponse(responseCode = "204", description = "Device revoked"),
141+
@ApiResponse(
142+
responseCode = "401",
143+
description = "Unauthorized",
144+
content = @Content(schema = @Schema(implementation = ErrorMessage.class)))
145+
})
94146
@SecurityRequirement(name = "cookieAuth")
95147
@DeleteMapping("/tokens/devices/{deviceId}")
96148
public ResponseEntity<Void> revokeDevice(@PathVariable String deviceId) {
97149
deviceService.revoke(deviceId);
98150
return ResponseEntity.noContent().build();
99151
}
100152

153+
@Operation(summary = "Refresh access and refresh token cookies using the refresh token")
154+
@ApiResponses({
155+
@ApiResponse(responseCode = "204", description = "Tokens refreshed — cookies updated"),
156+
@ApiResponse(
157+
responseCode = "400",
158+
description = "Invalid or expired refresh token",
159+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
160+
@ApiResponse(responseCode = "429", description = "Too many requests", content = @Content)
161+
})
101162
@RateLimit(requests = 10, duration = 60)
102163
@PostMapping("/tokens/refresh")
103164
public ResponseEntity<Void> refreshTokens(HttpServletRequest request) {
@@ -107,6 +168,15 @@ public ResponseEntity<Void> refreshTokens(HttpServletRequest request) {
107168
return setAuthCookies(authTokens);
108169
}
109170

171+
@Operation(summary = "Send a password reset email")
172+
@ApiResponses({
173+
@ApiResponse(responseCode = "204", description = "Email sent if account exists"),
174+
@ApiResponse(
175+
responseCode = "400",
176+
description = "Validation error",
177+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
178+
@ApiResponse(responseCode = "429", description = "Too many requests", content = @Content)
179+
})
110180
@RateLimit(requests = 3, duration = 60)
111181
@PostMapping("/password/forgot")
112182
public ResponseEntity<Void> forgotPassword(
@@ -115,6 +185,15 @@ public ResponseEntity<Void> forgotPassword(
115185
return ResponseEntity.noContent().build();
116186
}
117187

188+
@Operation(summary = "Reset password using a token from the reset email")
189+
@ApiResponses({
190+
@ApiResponse(responseCode = "204", description = "Password reset"),
191+
@ApiResponse(
192+
responseCode = "400",
193+
description = "Invalid or expired token, or validation error",
194+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
195+
@ApiResponse(responseCode = "429", description = "Too many requests", content = @Content)
196+
})
118197
@RateLimit(requests = 5, duration = 60)
119198
@PostMapping("/password/reset")
120199
public ResponseEntity<Void> resetPassword(
@@ -123,6 +202,17 @@ public ResponseEntity<Void> resetPassword(
123202
return ResponseEntity.noContent().build();
124203
}
125204

205+
@Operation(summary = "Resend the email verification link")
206+
@ApiResponses({
207+
@ApiResponse(
208+
responseCode = "204",
209+
description = "Email sent if account exists and is inactive"),
210+
@ApiResponse(
211+
responseCode = "400",
212+
description = "Validation error",
213+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
214+
@ApiResponse(responseCode = "429", description = "Too many requests", content = @Content)
215+
})
126216
@RateLimit(requests = 3, duration = 60)
127217
@PostMapping("/verification-email")
128218
public ResponseEntity<Void> sendVerificationMail(
@@ -131,6 +221,15 @@ public ResponseEntity<Void> sendVerificationMail(
131221
return ResponseEntity.noContent().build();
132222
}
133223

224+
@Operation(summary = "Verify email address using a token from the verification email")
225+
@ApiResponses({
226+
@ApiResponse(responseCode = "204", description = "Email verified"),
227+
@ApiResponse(
228+
responseCode = "400",
229+
description = "Invalid or expired token",
230+
content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
231+
@ApiResponse(responseCode = "429", description = "Too many requests", content = @Content)
232+
})
134233
@RateLimit(requests = 5, duration = 60)
135234
@PostMapping("/verify-email")
136235
public ResponseEntity<Void> verifyEmail(
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package org.bugzkit.api.auth.payload.request;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
34
import jakarta.validation.constraints.NotBlank;
45
import jakarta.validation.constraints.Pattern;
56
import org.bugzkit.api.shared.constants.Regex;
67
import org.bugzkit.api.shared.validator.UsernameOrEmail;
78

89
public record AuthTokensRequest(
9-
@NotBlank(message = "{user.usernameOrEmailRequired}")
10+
@Schema(example = "john_doe")
11+
@NotBlank(message = "{user.usernameOrEmailRequired}")
1012
@UsernameOrEmail(message = "{user.usernameOrEmailInvalid}")
1113
String usernameOrEmail,
12-
@NotBlank(message = "{user.passwordRequired}")
14+
@Schema(example = "Secret123!")
15+
@NotBlank(message = "{user.passwordRequired}")
1316
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
1417
String password) {}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package org.bugzkit.api.auth.payload.request;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
34
import jakarta.validation.constraints.Email;
45
import jakarta.validation.constraints.NotBlank;
56
import org.bugzkit.api.shared.constants.Regex;
67

78
public record ForgotPasswordRequest(
8-
@NotBlank(message = "{user.emailRequired}")
9+
@Schema(example = "john@example.com")
10+
@NotBlank(message = "{user.emailRequired}")
911
@Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL)
1012
String email) {}
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.bugzkit.api.auth.payload.request;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
34
import jakarta.validation.constraints.Email;
45
import jakarta.validation.constraints.NotBlank;
56
import jakarta.validation.constraints.Pattern;
@@ -10,15 +11,19 @@
1011
@Builder
1112
@FieldMatch(first = "password", second = "confirmPassword", message = "{user.passwordsDoNotMatch}")
1213
public record RegisterUserRequest(
13-
@NotBlank(message = "{user.usernameRequired}")
14+
@Schema(example = "john_doe")
15+
@NotBlank(message = "{user.usernameRequired}")
1416
@Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}")
1517
String username,
16-
@NotBlank(message = "{user.emailRequired}")
18+
@Schema(example = "john@example.com")
19+
@NotBlank(message = "{user.emailRequired}")
1720
@Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL)
1821
String email,
19-
@NotBlank(message = "{user.passwordRequired}")
22+
@Schema(example = "Secret123!")
23+
@NotBlank(message = "{user.passwordRequired}")
2024
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
2125
String password,
22-
@NotBlank(message = "{user.passwordRequired}")
26+
@Schema(example = "Secret123!")
27+
@NotBlank(message = "{user.passwordRequired}")
2328
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
2429
String confirmPassword) {}
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package org.bugzkit.api.auth.payload.request;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
34
import jakarta.validation.constraints.NotBlank;
45
import jakarta.validation.constraints.Pattern;
56
import org.bugzkit.api.shared.constants.Regex;
67
import org.bugzkit.api.shared.validator.FieldMatch;
78

89
@FieldMatch(first = "password", second = "confirmPassword", message = "{user.passwordsDoNotMatch}")
910
public record ResetPasswordRequest(
10-
@NotBlank(message = "{auth.tokenRequired}") String token,
11-
@NotBlank(message = "{user.passwordRequired}")
11+
@Schema(example = "550e8400-e29b-41d4-a716-446655440000")
12+
@NotBlank(message = "{auth.tokenRequired}")
13+
String token,
14+
@Schema(example = "NewSecret123!")
15+
@NotBlank(message = "{user.passwordRequired}")
1216
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
1317
String password,
14-
@NotBlank(message = "{user.passwordRequired}")
18+
@Schema(example = "NewSecret123!")
19+
@NotBlank(message = "{user.passwordRequired}")
1520
@Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}")
1621
String confirmPassword) {}

0 commit comments

Comments
 (0)