diff --git a/src/main/java/net/siegeln/cameleer/saas/identity/ServerApiClient.java b/src/main/java/net/siegeln/cameleer/saas/identity/ServerApiClient.java index 8b3e846..a4925cc 100644 --- a/src/main/java/net/siegeln/cameleer/saas/identity/ServerApiClient.java +++ b/src/main/java/net/siegeln/cameleer/saas/identity/ServerApiClient.java @@ -156,6 +156,19 @@ public class ServerApiClient { } } + /** Reset the built-in admin password on a tenant's server. */ + public void resetServerAdminPassword(String serverEndpoint, String newPassword) { + RestClient.create(serverEndpoint) + .post() + .uri("/api/v1/admin/users/user:admin/password") + .header("Authorization", "Bearer " + getAccessToken()) + .header("X-Cameleer-Protocol-Version", "1") + .contentType(MediaType.APPLICATION_JSON) + .body(Map.of("password", newPassword)) + .retrieve() + .toBodilessEntity(); + } + public record ServerHealthResponse(boolean healthy, String status) {} private synchronized String getAccessToken() { diff --git a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalController.java b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalController.java index da6f5ba..d6aed97 100644 --- a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalController.java +++ b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalController.java @@ -83,6 +83,18 @@ public class TenantPortalController { return ResponseEntity.ok().build(); } + @PostMapping("/server/admin-password") + public ResponseEntity resetServerAdminPassword(@RequestBody PasswordChangeRequest body) { + try { + portalService.resetServerAdminPassword(body.password()); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } catch (IllegalStateException e) { + return ResponseEntity.badRequest().build(); + } + } + @PostMapping("/password") public ResponseEntity changeOwnPassword(@AuthenticationPrincipal Jwt jwt, @RequestBody PasswordChangeRequest body) { diff --git a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java index 0a79e0c..f1885f1 100644 --- a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java +++ b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java @@ -176,6 +176,18 @@ public class TenantPortalService { logtoClient.assignOrganizationRole(orgId, userId, roleId); } + public void resetServerAdminPassword(String newPassword) { + TenantEntity tenant = resolveTenant(); + String endpoint = tenant.getServerEndpoint(); + if (endpoint == null || endpoint.isBlank()) { + throw new IllegalStateException("Server not provisioned yet"); + } + if (newPassword == null || newPassword.length() < 8) { + throw new IllegalArgumentException("Password must be at least 8 characters"); + } + serverApiClient.resetServerAdminPassword(endpoint, newPassword); + } + public void changePassword(String userId, String newPassword) { if (newPassword == null || newPassword.length() < 8) { throw new IllegalArgumentException("Password must be at least 8 characters"); diff --git a/ui/src/api/tenant-hooks.ts b/ui/src/api/tenant-hooks.ts index bf4b7e2..67c0c51 100644 --- a/ui/src/api/tenant-hooks.ts +++ b/ui/src/api/tenant-hooks.ts @@ -94,6 +94,12 @@ export function useRemoveTeamMember() { }); } +export function useResetServerAdminPassword() { + return useMutation({ + mutationFn: (password) => api.post('/tenant/server/admin-password', { password }), + }); +} + export function useChangeOwnPassword() { return useMutation({ mutationFn: (password) => api.post('/tenant/password', { password }), diff --git a/ui/src/pages/tenant/SettingsPage.tsx b/ui/src/pages/tenant/SettingsPage.tsx index 7c90f76..d97c675 100644 --- a/ui/src/pages/tenant/SettingsPage.tsx +++ b/ui/src/pages/tenant/SettingsPage.tsx @@ -9,7 +9,7 @@ import { Spinner, useToast, } from '@cameleer/design-system'; -import { useTenantSettings, useChangeOwnPassword } from '../../api/tenant-hooks'; +import { useTenantSettings, useChangeOwnPassword, useResetServerAdminPassword } from '../../api/tenant-hooks'; import { tierColor } from '../../utils/tier'; import styles from '../../styles/platform.module.css'; @@ -26,10 +26,12 @@ function statusColor(status: string): 'success' | 'error' | 'warning' | 'auto' { export function SettingsPage() { const { data, isLoading, isError } = useTenantSettings(); const changePassword = useChangeOwnPassword(); + const resetServerAdmin = useResetServerAdminPassword(); const { toast } = useToast(); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [serverAdminPw, setServerAdminPw] = useState(''); async function handleChangePassword(e: React.FormEvent) { e.preventDefault(); @@ -140,6 +142,46 @@ export function SettingsPage() { + + +

+ Reset the built-in admin password for your server dashboard (local login at /login?local). +

+
{ + e.preventDefault(); + if (serverAdminPw.length < 8) { + toast({ title: 'Password must be at least 8 characters', variant: 'error' }); + return; + } + try { + await resetServerAdmin.mutateAsync(serverAdminPw); + toast({ title: 'Server admin password reset successfully', variant: 'success' }); + setServerAdminPw(''); + } catch (err) { + toast({ title: 'Failed to reset server admin password', description: String(err), variant: 'error' }); + } + }} + style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12 }} + > + + setServerAdminPw(e.target.value)} + placeholder="Enter new admin password" + required + minLength={8} + /> + +
+ +
+
+
); }