diff options
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/authentication/api/controller')
7 files changed, 367 insertions, 0 deletions
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/AuthenticationController.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/AuthenticationController.java new file mode 100644 index 0000000..f580969 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/AuthenticationController.java @@ -0,0 +1,251 @@ +package org.psesquared.server.authentication.api.controller; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.psesquared.server.authentication.api.service.AuthenticationService; +import org.psesquared.server.config.EmailConfigProperties; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * This is a controller class for the Authentication API that handles the + * requests from the client concerning login/logout and user account management. + */ +@RequestMapping("/api/2") +@RestController +@RequiredArgsConstructor +public class AuthenticationController { + + /** + * The name of the HTTP location header. + */ + private static final String LOCATION_HEADER = "Location"; + + /** + * The service class that this controller calls to further process requests. + */ + private final AuthenticationService authenticationService; + + /** + * The properties class that is used to return some externally stored URLs. + */ + private final EmailConfigProperties emailConfigProperties; + + /** + * The API-endpoint for registering a new + * {@link org.psesquared.server.model.User} with a username, email address and + * password. In order for the account to be used, the registration process + * must be concluded with the verification of the email address. For this an + * email with a link for verification is sent to {@code userInfo.email()}. + * + * @param userInfo The request-wrapper containing username, email and + * password. + * @return {@link HttpStatus#OK} on success, <br> + * {@link HttpStatus#BAD_REQUEST} for invalid user information + * @see AuthenticationService#registerUser(UserInfoRequest) + */ + @PostMapping("/auth/register.json") + public ResponseEntity<String> registerUser( + @RequestBody final UserInfoRequest userInfo) { + return new ResponseEntity<>(authenticationService.registerUser(userInfo)); + } + + /** + * The API-endpoint for verifying a newly created + * {@link org.psesquared.server.model.User}. This method is invoked via the + * link in the verification email that is sent in + * {@link #registerUser(UserInfoRequest)}. + * On success, it transfers the user to the dashboard and on failure it sets + * the following status codes: + * {@link HttpStatus#OK} on success, <br> + * {@link HttpStatus#BAD_REQUEST} user exists and is already verified, + * <br> + * {@link HttpStatus#UNAUTHORIZED} invalid token, <br> + * {@link HttpStatus#NOT_FOUND} user not found + * + * @param username The username of the user that needs to be verified + * @param token The JWT that indicates the authority of the request + * @param response The {@link HttpServletResponse} for setting up a + * redirection to the frontend + * @see AuthenticationService#verifyRegistration(String, String) + */ + @GetMapping("/auth/{username}/verify.json") + public void verifyRegistration( + @PathVariable final String username, + @RequestParam("token") final String token, + @NonNull final HttpServletResponse response) { + HttpStatus status + = authenticationService.verifyRegistration(username, token); + if (status.equals(HttpStatus.OK)) { + response.setHeader(LOCATION_HEADER, + emailConfigProperties.dashboardBaseUrl()); + response.setStatus(HttpStatus.FOUND.value()); + } else { + response.setStatus(status.value()); + } + } + + /** + * The API-endpoint for setting a JWT access token with a lifespan of one hour + * as the "sessionid" cookie for authorization with further requests. <br> + * (This is a secured endpoint requiring authorization via HTTP basic or JWT.) + * + * @param username The username of the user who wants to log in + * @param response The {@link HttpServletResponse} for setting the "sessionid" + * cookie + * @return {@link HttpStatus#OK} on success, <br> + * {@link HttpStatus#NOT_FOUND} user not found + * @see AuthenticationService#login(String, HttpServletResponse) + */ + @PostMapping("/auth/{username}/login.json") + public ResponseEntity<String> login( + @PathVariable final String username, + @NonNull final HttpServletResponse response) { + return new ResponseEntity<>( + authenticationService.login(username, response)); + } + + /** + * The API-endpoint for invalidating the "sessionid" cookie containing a JWT + * access token. Following authorized requests require HTTP basic + * authentication or a new login. <br> + * (This is a secured endpoint requiring authorization via HTTP basic or JWT.) + * + * @param username The username of the user who wants to log out + * @param response The {@link HttpServletResponse} for invalidating the + * "sessionid" cookie + * @return {@link HttpStatus#OK} on success, <br> + * {@link HttpStatus#NOT_FOUND} user not found + * @see AuthenticationService#logout(String, HttpServletResponse) + */ + @PostMapping("/auth/{username}/logout.json") + public ResponseEntity<String> logout( + @PathVariable final String username, + @NonNull final HttpServletResponse response) { + return new ResponseEntity<>( + authenticationService.logout(username, response)); + } + + /** + * The API-endpoint for sending an email to the given address + * ({@link ForgotPasswordRequest#email()} with an url to reset the password of + * the user with that email address. + * + * @param email The email address of the user who wants to reset their + * password + * @return {@link HttpStatus#OK} on success, <br> + * {@link HttpStatus#NOT_FOUND} user not found + * @see AuthenticationService#forgotPassword(String) + */ + @PostMapping("/auth/{email}/forgot.json") + public ResponseEntity<String> forgotPassword( + @PathVariable final String email) { + return new ResponseEntity<>(authenticationService.forgotPassword(email)); + } + + /** + * The API-endpoint for resetting the password of a + * {@link org.psesquared.server.model.User}. This method is invoked via the + * link in the verification email that is sent in + * {@link #forgotPassword(String)}. + * + * @param username The username of the user who wants to reset their + * password + * @param token The JWT that indicates the authority of the request + * @param requestBody The request-wrapper containing the new password + * @return {@link HttpStatus#OK} on success, <br> + * {@link HttpStatus#BAD_REQUEST} password doesn't meet requirements, + * <br> + * {@link HttpStatus#UNAUTHORIZED} invalid token, <br> + * {@link HttpStatus#NOT_FOUND} user not found + * @see AuthenticationService#resetPassword(String, String, PasswordRequest) + */ + @PutMapping("/auth/{username}/resetpassword.json") + public ResponseEntity<String> resetPassword( + @PathVariable final String username, + @RequestParam("token") final String token, + @RequestBody final PasswordRequest requestBody) { + return new ResponseEntity<>( + authenticationService.resetPassword(username, token, requestBody)); + } + + /** + * The API-endpoint for changing the password of a + * {@link org.psesquared.server.model.User}, who is logged-in in the + * dashboard. + * (This is a secured endpoint requiring authorization via HTTP basic or JWT.) + * + * @param username The username of the user who wants to change their + * password + * @param requestBody The request-wrapper containing old and new password + * @return {@link HttpStatus#OK} on success, <br> + * {@link HttpStatus#BAD_REQUEST} old password is wrong, <br> + * {@link HttpStatus#NOT_FOUND} user not found + * @see AuthenticationService#changePassword(String, ChangePasswordRequest) + */ + @PutMapping("/auth/{username}/changepassword.json") + public ResponseEntity<String> changePassword( + @PathVariable final String username, + @RequestBody final ChangePasswordRequest requestBody) { + return new ResponseEntity<>( + authenticationService.changePassword(username, requestBody)); + } + + /** + * The API-endpoint for deleting a {@link org.psesquared.server.model.User}. + * This action is performed by a logged-in user from the dashboard. + * The user must enter their password ({@link PasswordRequest#password()}) + * and if correct, the user along with all associated data is deleted. + * (This is a secured endpoint requiring authorization via HTTP basic or JWT.) + * + * @param username The username of the user who wants to delete their + * account + * @param requestBody The request-wrapper containing the user's password + * @return {@link HttpStatus#OK} on success, <br> + * {@link HttpStatus#BAD_REQUEST} wrong password, <br> + * {@link HttpStatus#NOT_FOUND} user not found + * @see AuthenticationService#deleteUser(String, PasswordRequest) + */ + @DeleteMapping("/auth/{username}/delete.json") + public ResponseEntity<String> deleteUser( + @PathVariable final String username, + @RequestBody final PasswordRequest requestBody) { + return new ResponseEntity<>( + authenticationService.deleteUser(username, requestBody)); + } + + /** + * This API-endpoint exists for compatibility with podcatchers, especially + * AntennaPod and Kasts, which initially call this endpoint instead of + * {@link #login(String, HttpServletResponse)}. + * Accordingly, a call to this endpoint is internally treated as a login. + * In particular, devices remain unsupported. + * + * @param username The username of the user to be synchronized + * @param response The {@link HttpServletResponse} for setting the "sessionid" + * cookie + * @return A dummy response with a single dummy device for the given user + * @see AuthenticationService#login(String, HttpServletResponse) + */ + @GetMapping("/devices/{username}.json") + public ResponseEntity<List<DeviceWrapper>> getDeviceList( + @PathVariable final String username, + @NonNull final HttpServletResponse response) { + DeviceWrapper dummyDevice = new DeviceWrapper(); + return new ResponseEntity<>( + List.of(dummyDevice), + authenticationService.login(username, response)); + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ChangePasswordRequest.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ChangePasswordRequest.java new file mode 100644 index 0000000..d8b2357 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ChangePasswordRequest.java @@ -0,0 +1,15 @@ +package org.psesquared.server.authentication.api.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A request for changing the password containing the old, i.e. current, and new + * password. + * + * @param oldPassword The user's current password + * @param newPassword The new password + */ +public record ChangePasswordRequest( + @JsonProperty(value = "password", required = true) String oldPassword, + @JsonProperty(value = "new_password", required = true) String newPassword) { +} diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/DeviceWrapper.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/DeviceWrapper.java new file mode 100644 index 0000000..35dae3d --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/DeviceWrapper.java @@ -0,0 +1,46 @@ +package org.psesquared.server.authentication.api.controller; + +/** + * This record wraps a dummy device that is required to be returned by <br> + * {@link AuthenticationController#getDeviceList(String, + * jakarta.servlet.http.HttpServletResponse)}. + * + * @param id The device id + * @param caption The caption, i.e. name, of the device + * @param type The device type + * @param subscriptions The number of subscriptions of the device + */ +public record DeviceWrapper( + String id, + String caption, + String type, + int subscriptions) { + + /** + * The id of the dummy device. + */ + private static final String DUMMY_ID = "dummy"; + + /** + * The name of the dummy device. + */ + private static final String DUMMY_DEVICE = "device"; + + /** + * The type of the dummy device. + */ + private static final String DUMMY_TYPE = "other"; + + /** + * The number of subscriptions of the dummy device. + */ + private static final int DUMMY_SUBSCRIPTIONS = 0; + + /** + * The no-args-constructor for a device-wrapper containing a dummy device. + */ + public DeviceWrapper() { + this(DUMMY_ID, DUMMY_DEVICE, DUMMY_TYPE, DUMMY_SUBSCRIPTIONS); + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ForgotPasswordRequest.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ForgotPasswordRequest.java new file mode 100644 index 0000000..700fb08 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ForgotPasswordRequest.java @@ -0,0 +1,13 @@ +package org.psesquared.server.authentication.api.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A request for sending an email with a link for resetting a user's password to + * the user's {@link #email} address. + * + * @param email The email address + */ +public record ForgotPasswordRequest( + @JsonProperty(required = true) String email) { +} diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/PasswordRequest.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/PasswordRequest.java new file mode 100644 index 0000000..f772c7b --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/PasswordRequest.java @@ -0,0 +1,13 @@ +package org.psesquared.server.authentication.api.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A request that contains a {@link #password}, which is either set as a new + * password or used for confirming the deletion of an account. + * + * @param password The password + */ +public record PasswordRequest( + @JsonProperty(required = true) String password) { +} diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/UserInfoRequest.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/UserInfoRequest.java new file mode 100644 index 0000000..9c112b1 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/UserInfoRequest.java @@ -0,0 +1,16 @@ +package org.psesquared.server.authentication.api.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A request that contains the {@link #username}, {@link #email} address and + * {@link #password} for registering a new user. + * + * @param username The username + * @param email The email + * @param password The password + */ +public record UserInfoRequest(@JsonProperty(required = true) String username, + @JsonProperty(required = true) String email, + @JsonProperty(required = true) String password) { +} diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/package-info.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/package-info.java new file mode 100644 index 0000000..79ae33d --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/package-info.java @@ -0,0 +1,13 @@ +/** + * This package represents the highest logical layer of the authentication API + * ({@link org.psesquared.server.authentication.api}) - the controller layer. + * <br> + * It contains the + * {@link + * org.psesquared.server.authentication.api.controller.AuthenticationController} + * along with a series of wrapper classes for JSON request and response bodies. + * + * @author PSE-Squared Team + * @version 1.0 + */ +package org.psesquared.server.authentication.api.controller; |