feat: password management for tenant portal
- POST /api/tenant/password — change own Logto password
- POST /api/tenant/team/{userId}/password — reset team member password
- Settings page: "Change Password" card with confirm field
- Team page: "Reset Password" button per member with inline form
- LogtoManagementClient.updateUserPassword() via Logto Management API
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -398,6 +398,18 @@ public class LogtoManagementClient {
|
||||
.toBodilessEntity();
|
||||
}
|
||||
|
||||
/** Update a user's password. */
|
||||
public void updateUserPassword(String userId, String newPassword) {
|
||||
if (!isAvailable()) throw new IllegalStateException("Logto not configured");
|
||||
restClient.patch()
|
||||
.uri(config.getLogtoEndpoint() + "/api/users/" + userId)
|
||||
.header("Authorization", "Bearer " + getAccessToken())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(Map.of("password", newPassword))
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
}
|
||||
|
||||
/** Get a user by ID. Returns username, primaryEmail, name. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> getUser(String userId) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import net.siegeln.cameleer.saas.certificate.TenantCaCertEntity;
|
||||
import net.siegeln.cameleer.saas.certificate.TenantCaCertService;
|
||||
import net.siegeln.cameleer.saas.config.TenantContext;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
@@ -39,6 +41,8 @@ public class TenantPortalController {
|
||||
|
||||
public record RoleChangeRequest(String roleId) {}
|
||||
|
||||
public record PasswordChangeRequest(String password) {}
|
||||
|
||||
// --- Endpoints ---
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
@@ -79,6 +83,28 @@ public class TenantPortalController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/password")
|
||||
public ResponseEntity<Void> changeOwnPassword(@AuthenticationPrincipal Jwt jwt,
|
||||
@RequestBody PasswordChangeRequest body) {
|
||||
try {
|
||||
portalService.changePassword(jwt.getSubject(), body.password());
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/team/{userId}/password")
|
||||
public ResponseEntity<Void> resetTeamMemberPassword(@PathVariable String userId,
|
||||
@RequestBody PasswordChangeRequest body) {
|
||||
try {
|
||||
portalService.resetTeamMemberPassword(userId, body.password());
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/server/restart")
|
||||
public ResponseEntity<Void> restartServer() {
|
||||
portalService.restartServer();
|
||||
|
||||
@@ -176,6 +176,32 @@ public class TenantPortalService {
|
||||
logtoClient.assignOrganizationRole(orgId, userId, roleId);
|
||||
}
|
||||
|
||||
public void changePassword(String userId, String newPassword) {
|
||||
if (newPassword == null || newPassword.length() < 8) {
|
||||
throw new IllegalArgumentException("Password must be at least 8 characters");
|
||||
}
|
||||
logtoClient.updateUserPassword(userId, newPassword);
|
||||
}
|
||||
|
||||
public void resetTeamMemberPassword(String userId, String newPassword) {
|
||||
TenantEntity tenant = resolveTenant();
|
||||
String orgId = tenant.getLogtoOrgId();
|
||||
if (orgId == null || orgId.isBlank()) {
|
||||
throw new IllegalStateException("Tenant has no Logto organization configured");
|
||||
}
|
||||
// Verify the target user belongs to this tenant's org
|
||||
var members = logtoClient.listOrganizationMembers(orgId);
|
||||
boolean isMember = members.stream()
|
||||
.anyMatch(m -> userId.equals(String.valueOf(m.get("id"))));
|
||||
if (!isMember) {
|
||||
throw new IllegalArgumentException("User is not a member of this organization");
|
||||
}
|
||||
if (newPassword == null || newPassword.length() < 8) {
|
||||
throw new IllegalArgumentException("Password must be at least 8 characters");
|
||||
}
|
||||
logtoClient.updateUserPassword(userId, newPassword);
|
||||
}
|
||||
|
||||
public TenantSettingsData getSettings() {
|
||||
TenantEntity tenant = resolveTenant();
|
||||
String publicEndpoint = provisioningProps.publicProtocol() + "://"
|
||||
|
||||
Reference in New Issue
Block a user