+
+ {/* Org ownership warning */}
+
Organization ownership check
- If you are the sole owner of any organization that has other members,
- you must transfer ownership to another member or delete
- those organizations before proceeding.
+ If you own organizations with other members, you must{" "}
+ transfer ownership to another member first.
+
+
+ Organizations where you are the sole member will
+ be automatically deleted along with your account.
+
+ {/* What will be deleted */}
+
+
+
+ The following will be permanently deleted:
+
+
+ - Your profile and account data
+ - All SSH keys and active certificates
+ - All linked accounts (Google, GitHub, etc.)
+ - All active sessions
+ - All passkeys and MFA methods
+
+
+
+ {/* Email confirmation input */}
+
+
+ setConfirmEmail(e.target.value)}
+ disabled={isDeleting}
+ autoComplete="off"
+ />
+
+
-
diff --git a/src/pages/user/SecurityPage.tsx b/src/pages/user/SecurityPage.tsx
index 7f55314..eb5a7db 100644
--- a/src/pages/user/SecurityPage.tsx
+++ b/src/pages/user/SecurityPage.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
-import { Lock, Fingerprint, Smartphone, Shield, Plus, CheckCircle, Loader2, Pencil, Trash2 } from "lucide-react";
+import { Lock, Fingerprint, Smartphone, Shield, Plus, CheckCircle, Loader2, Pencil, Trash2, Link2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -9,7 +9,7 @@ import { AddPasskeyWizard } from "@/components/security/AddPasskeyWizard";
import { TotpEnrollmentWizard } from "@/components/security/TotpEnrollmentWizard";
import { TotpRemoveDialog } from "@/components/security/TotpRemoveDialog";
import { PasswordStrengthMeter, isPasswordValid } from "@/components/auth/PasswordStrengthMeter";
-import { api, ApiError, TotpStatusResponse, PasskeyCredential } from "@/lib/api";
+import { api, ApiError, TotpStatusResponse, PasskeyCredential, User } from "@/lib/api";
import { useToast } from "@/hooks/use-toast";
import { ComplianceBanner } from "@/components/auth/ComplianceBanner";
import { useAuth } from "@/contexts/AuthContext";
@@ -29,6 +29,9 @@ export default function SecurityPage() {
const [showAddPasskey, setShowAddPasskey] = useState(false);
const [showTotpEnrollment, setShowTotpEnrollment] = useState(false);
const [showTotpRemove, setShowTotpRemove] = useState(false);
+
+ // Profile (for has_password / linked_providers)
+ const [profile, setProfile] = useState
(null);
// Password form state
const [currentPassword, setCurrentPassword] = useState("");
@@ -53,19 +56,49 @@ export default function SecurityPage() {
const { toast } = useToast();
const { mfaCompliance } = useAuth();
- // Policy requirements (could come from org settings in future)
+ // Whether this user has a password (false for pure OAuth signups)
+ const hasPassword = profile?.has_password ?? true; // default true until loaded
+ const linkedProviders = profile?.linked_providers ?? [];
+
+ // Derive policy requirements from actual org compliance data
+ const effectiveModes = mfaCompliance?.orgs?.map(o => o.effective_mode) ?? [];
const policyRequirements = {
- totpRequired: true,
- passkeysRequired: false,
+ totpRequired: effectiveModes.some(m =>
+ m === 'require_totp' || m === 'require_totp_or_webauthn'
+ ),
+ passkeysRequired: effectiveModes.some(m =>
+ m === 'require_webauthn' || m === 'require_totp_or_webauthn'
+ ),
minPasswordLength: 12,
};
+ // Build a human-readable policy description from the strictest mode active
+ const activePolicyModes = effectiveModes.filter(m => m && m.startsWith('require_'));
+ const policyDescription = (() => {
+ if (activePolicyModes.includes('require_totp_or_webauthn'))
+ return 'Your organization requires TOTP or a passkey for all members.';
+ if (activePolicyModes.includes('require_totp'))
+ return 'Your organization requires TOTP to be enabled for all members.';
+ if (activePolicyModes.includes('require_webauthn'))
+ return 'Your organization requires a passkey for all members.';
+ return null;
+ })();
// Fetch TOTP status on mount
useEffect(() => {
+ fetchProfile();
fetchTotpStatus();
fetchPasskeys();
}, []);
+ const fetchProfile = async () => {
+ try {
+ const res = await api.users.me();
+ setProfile(res.user);
+ } catch {
+ // Non-fatal — UI falls back to showing password section
+ }
+ };
+
const fetchTotpStatus = async () => {
setIsTotpStatusLoading(true);
try {
@@ -234,20 +267,20 @@ export default function SecurityPage() {
- {/* Policy Status */}
-
-
-
-
-
-
Organization Policy
-
- Your organization requires TOTP to be enabled for all members.
-
+ {/* Policy Status — only shown when the org actually enforces MFA */}
+ {policyDescription && (
+
+
+
+
+
+
Organization Policy
+
{policyDescription}
+
-
-
-
+
+
+ )}
{/* Password */}
@@ -260,16 +293,40 @@ export default function SecurityPage() {
Manage your account password
- setShowPasswordForm(!showPasswordForm)}
- >
- Change password
-
+ {hasPassword ? (
+ setShowPasswordForm(!showPasswordForm)}
+ >
+ Change password
+
+ ) : (
+
+ Not set
+
+ )}
- {showPasswordForm && (
+ {!hasPassword && linkedProviders.length > 0 && (
+
+
+
+
+ Your account uses{" "}
+
+ {linkedProviders
+ .map((p) =>
+ ({ google: "Google", github: "GitHub", microsoft: "Microsoft", oidc: "SSO" }[p] ?? p)
+ )
+ .join(", ")}
+ {" "}
+ for sign-in. No password is set. Contact your admin if you need one added.
+
+
+
+ )}
+ {hasPassword && showPasswordForm && (
{passwordError && (
@@ -505,6 +562,7 @@ export default function SecurityPage() {
setShowTotpRemove(false);
}}
isRequired={policyRequirements.totpRequired}
+ hasPassword={hasPassword}
/>
{/* Delete Passkey Confirmation */}