feat: add authentication service and endpoints
This commit is contained in:
1 parent
a593e090e4
commit
567219134a
6 files changed
+136
No files matched your search
@@ -0,0 +1,42 @@
|
||||
package com.slotline.user;
|
||||
|
||||
import com.slotline.user.dto.LoginRequest;
|
||||
import com.slotline.user.dto.RegisterRequest;
|
||||
import com.slotline.user.dto.TokenResponse;
|
||||
import com.slotline.user.dto.UserResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@Tag(name = "Authentication")
|
||||
@SecurityRequirements
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
public AuthController(AuthService authService) {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "Register a new USER account")
|
||||
public UserResponse register(@Valid @RequestBody RegisterRequest request) {
|
||||
return authService.register(request);
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "Exchange credentials for a JWT bearer token")
|
||||
public TokenResponse login(@Valid @RequestBody LoginRequest request) {
|
||||
return authService.login(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.slotline.user;
|
||||
|
||||
import com.slotline.common.InvalidCredentialsException;
|
||||
import com.slotline.common.UsernameTakenException;
|
||||
import com.slotline.config.SlotlineProperties;
|
||||
import com.slotline.organization.Organization;
|
||||
import com.slotline.organization.OrganizationService;
|
||||
import com.slotline.security.JwtService;
|
||||
import com.slotline.user.dto.LoginRequest;
|
||||
import com.slotline.user.dto.RegisterRequest;
|
||||
import com.slotline.user.dto.TokenResponse;
|
||||
import com.slotline.user.dto.UserResponse;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
|
||||
private final UserRepository users;
|
||||
private final OrganizationService organizations;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
private final SlotlineProperties properties;
|
||||
|
||||
public AuthService(UserRepository users, OrganizationService organizations, PasswordEncoder passwordEncoder,
|
||||
JwtService jwtService, SlotlineProperties properties) {
|
||||
this.users = users;
|
||||
this.organizations = organizations;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtService = jwtService;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserResponse register(RegisterRequest request) {
|
||||
if (users.existsByUsername(request.username())) {
|
||||
throw new UsernameTakenException(request.username());
|
||||
}
|
||||
Organization organization = resolveOrganization(request.organizationSlug());
|
||||
User user = new User(organization.getId(), request.username(), passwordEncoder.encode(request.password()),
|
||||
Role.USER, Instant.now());
|
||||
users.save(user);
|
||||
return new UserResponse(user.getId(), user.getUsername(), user.getRole(), user.getOrganizationId());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public TokenResponse login(LoginRequest request) {
|
||||
User user = users.findByUsername(request.username())
|
||||
.filter(candidate -> passwordEncoder.matches(request.password(), candidate.getPasswordHash()))
|
||||
.orElseThrow(InvalidCredentialsException::new);
|
||||
String token = jwtService.issue(user.getId(), user.getUsername(), user.getRole(), user.getOrganizationId());
|
||||
return new TokenResponse(token, "Bearer", properties.getJwt().getExpiration().toSeconds());
|
||||
}
|
||||
|
||||
private Organization resolveOrganization(String slug) {
|
||||
String target = (slug == null || slug.isBlank()) ? OrganizationService.defaultSlug : slug;
|
||||
return organizations.requireBySlug(target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.slotline.user.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record LoginRequest(
|
||||
@NotBlank String username,
|
||||
@NotBlank String password) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.slotline.user.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record RegisterRequest(
|
||||
@NotBlank @Size(min = 3, max = 64) String username,
|
||||
@NotBlank @Size(min = 8, max = 72) String password,
|
||||
@Size(max = 64) String organizationSlug) {
|
||||
|
||||
public RegisterRequest(String username, String password) {
|
||||
this(username, password, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.slotline.user.dto;
|
||||
|
||||
public record TokenResponse(String token, String tokenType, long expiresInSeconds) {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.slotline.user.dto;
|
||||
|
||||
import com.slotline.user.Role;
|
||||
|
||||
public record UserResponse(Long id, String username, Role role, Long organizationId) {
|
||||
}
|
||||
Reference in new issue
Block a user