Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
import kr.devslab.kit.access.core.repository.JpaPlatformRoleRepository;
import kr.devslab.kit.admin.AdminApiPaths;
import kr.devslab.kit.core.id.TenantId;
import kr.devslab.kit.identity.AccountLoginException;
import kr.devslab.kit.identity.AuthToken;
import kr.devslab.kit.identity.AuthTokenService;
import kr.devslab.kit.identity.CurrentUser;
import kr.devslab.kit.identity.CurrentUserProvider;
import kr.devslab.kit.identity.LoginCommand;
import kr.devslab.kit.identity.LoginFailureReason;
import kr.devslab.kit.identity.LoginResult;
import kr.devslab.kit.identity.core.service.LocalLoginService;
import kr.devslab.kit.identity.core.service.SelfServicePasswordService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -24,15 +28,21 @@ public class AuthController {
private final LocalLoginService loginService;
private final AuthTokenService tokenService;
private final JpaPlatformRoleRepository roleRepository;
private final CurrentUserProvider currentUserProvider;
private final SelfServicePasswordService passwordService;

public AuthController(
LocalLoginService loginService,
AuthTokenService tokenService,
JpaPlatformRoleRepository roleRepository
JpaPlatformRoleRepository roleRepository,
CurrentUserProvider currentUserProvider,
SelfServicePasswordService passwordService
) {
this.loginService = loginService;
this.tokenService = tokenService;
this.roleRepository = roleRepository;
this.currentUserProvider = currentUserProvider;
this.passwordService = passwordService;
}

@PostMapping("/login")
Expand All @@ -54,9 +64,41 @@ public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest req)
base.tenantId(),
base.loginId(),
base.status(),
roleCodes
roleCodes,
base.mustChangePassword()
);
AuthToken token = tokenService.issue(enriched);
return ResponseEntity.ok(LoginResponse.of(token, enriched));
}

/**
* Self-service password change for the authenticated caller. Verifies the
* current password, stores the new one, and clears the forced-rotation
* flag — this is how a bootstrap admin (ADR 0001) escapes the
* {@code mustChangePassword} gate. Returns a freshly minted token (now with
* {@code mustChangePassword=false}) so the dashboard can swap it in without
* a re-login round-trip.
*/
@PostMapping("/change-password")
public ResponseEntity<LoginResponse> changePassword(@Valid @RequestBody ChangePasswordRequest req) {
CurrentUser principal = currentUserProvider.current()
.orElseThrow(() -> new AccountLoginException(LoginFailureReason.UNKNOWN_USER));

passwordService.changePassword(principal.id(), req.oldPassword(), req.newPassword());

// Re-issue with the flag cleared, re-reading roles so the new token is
// consistent with login's response shape.
Set<String> roleCodes = roleRepository.findRoleCodesForUserId(principal.id().value());
CurrentUser updated = new CurrentUser(
principal.id(),
principal.publicId(),
principal.tenantId(),
principal.loginId(),
principal.status(),
roleCodes,
false
);
AuthToken token = tokenService.issue(updated);
return ResponseEntity.ok(LoginResponse.of(token, updated));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.devslab.kit.admin.auth;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/**
* Self-service password change payload. The caller must be authenticated; the
* account is taken from the security principal, not the request body, so a
* user can only change their own password.
*/
public record ChangePasswordRequest(
@NotBlank @Size(max = 255) String oldPassword,
@NotBlank @Size(min = 8, max = 255) String newPassword
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public static LoginResponse of(AuthToken token, CurrentUser user) {
user.tenantId().value(),
user.loginId(),
user.status().name(),
user.roles()
user.roles(),
user.mustChangePassword()
)
);
}
Expand All @@ -28,7 +29,8 @@ public record UserSummary(
String tenantId,
String loginId,
String status,
Set<String> roles
Set<String> roles,
boolean mustChangePassword
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package kr.devslab.kit.autoconfigure;

import jakarta.persistence.EntityManager;
import kr.devslab.kit.access.core.repository.JpaPlatformPermissionRepository;
import kr.devslab.kit.access.core.repository.JpaPlatformRoleRepository;
import kr.devslab.kit.access.core.service.PermissionAdminService;
import kr.devslab.kit.access.core.service.RoleAdminService;
import kr.devslab.kit.access.core.service.RolePermissionService;
import kr.devslab.kit.access.core.service.UserRoleService;
import kr.devslab.kit.identity.core.repository.JpaPlatformUserAccountRepository;
import kr.devslab.kit.identity.core.service.PlatformUserAccountAdminService;
import kr.devslab.kit.tenant.TenantService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

/**
* Wires the first-admin bootstrap runner (ADR 0001) when
* {@code devslab.kit.bootstrap.enabled=true}. OFF by default — there is no
* {@code matchIfMissing}, so a deploy with no bootstrap config provisions
* nothing and cannot grow an accidental backdoor.
*
* <p>Ordered after the identity / access / tenant / admin-api auto-configs so
* all the services it needs are already defined.
*/
@AutoConfiguration(after = {
IdentityAutoConfiguration.class,
AccessAutoConfiguration.class,
TenantAutoConfiguration.class,
AdminApiAutoConfiguration.class
})
@ConditionalOnClass(EntityManager.class)
@ConditionalOnProperty(prefix = "devslab.kit.bootstrap", name = "enabled", havingValue = "true")
public class BootstrapAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public DevslabKitBootstrapRunner devslabKitBootstrapRunner(
DevslabKitProperties properties,
Environment environment,
TenantService tenantService,
PermissionAdminService permissionAdminService,
JpaPlatformPermissionRepository permissionRepository,
RoleAdminService roleAdminService,
JpaPlatformRoleRepository roleRepository,
RolePermissionService rolePermissionService,
PlatformUserAccountAdminService userAdminService,
JpaPlatformUserAccountRepository userRepository,
UserRoleService userRoleService
) {
return new DevslabKitBootstrapRunner(
properties.getBootstrap(),
environment,
tenantService,
permissionAdminService,
permissionRepository,
roleAdminService,
roleRepository,
rolePermissionService,
userAdminService,
userRepository,
userRoleService
);
}
}
Loading
Loading