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