feat: add authentication service and endpoints

This commit is contained in:
dualfroz committed 2026-08-24 14:26:55 +02:00
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) {
}