Skip to content

Commit f806613

Browse files
committed
v1.2.6k - feat: Implement First Contact Wizard, multi-factor authentication, and new email outreach templates.
1 parent 61e2c32 commit f806613

File tree

17 files changed

+481
-261
lines changed

17 files changed

+481
-261
lines changed

actions/teams/member-actions.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ export const updateMemberRole = async (userId: string, role: string) => {
1616
const targetUser = await prismadb.users.findUnique({ where: { id: userId } });
1717
if (!targetUser) return { error: "User not found" };
1818

19-
if ((currentUser.teamId !== targetUser.team_id || !currentUser.isAdmin)) {
19+
const isPlatformAdmin = currentUser.isGlobalAdmin || currentUser.teamRole === "PLATFORM_ADMIN";
20+
if (!isPlatformAdmin && (currentUser.teamId !== targetUser.team_id || !currentUser.isAdmin)) {
2021
return { error: "Unauthorized: You do not have permission to modify this user." };
2122
}
2223

23-
// Security Check for PLATFORM_ADMIN
24+
// Security Check: Only existing Platform Admins / Global Admins can assign PLATFORM_ADMIN
2425
if (role === "PLATFORM_ADMIN") {
25-
const isAuthorized = false;
26-
if (!isAuthorized) {
26+
if (!isPlatformAdmin) {
2727
return { error: "Unauthorized: Only Platform Admins can assign this role." };
2828
}
2929
}
@@ -48,7 +48,8 @@ export const removeMember = async (userId: string) => {
4848
const targetUser = await prismadb.users.findUnique({ where: { id: userId } });
4949
if (!targetUser) return { error: "User not found" };
5050

51-
if ((currentUser.teamId !== targetUser.team_id || !currentUser.isAdmin)) {
51+
const isPlatformAdmin = currentUser.isGlobalAdmin || currentUser.teamRole === "PLATFORM_ADMIN";
52+
if (!isPlatformAdmin && (currentUser.teamId !== targetUser.team_id || !currentUser.isAdmin)) {
5253
return { error: "Unauthorized: You do not have permission to modify this user." };
5354
}
5455

@@ -101,7 +102,8 @@ export const addMember = async (teamId: string, userId: string, role: string = "
101102
const currentUser = await getCurrentUserTeamId();
102103
if (!currentUser?.userId) return { error: "Unauthorized" };
103104

104-
if ((currentUser.teamId !== teamId || !currentUser.isAdmin)) {
105+
const isPlatformAdmin = currentUser.isGlobalAdmin || currentUser.teamRole === "PLATFORM_ADMIN";
106+
if (!isPlatformAdmin && (currentUser.teamId !== teamId || !currentUser.isAdmin)) {
105107
return { error: "Unauthorized: Admin privileges required for this team." };
106108
}
107109

@@ -169,7 +171,8 @@ export const toggleUserStatus = async (userId: string, status: "ACTIVE" | "INACT
169171
const targetUser = await prismadb.users.findUnique({ where: { id: userId } });
170172
if (!targetUser) return { error: "User not found" };
171173

172-
if ((currentUser.teamId !== targetUser.team_id || !currentUser.isAdmin)) {
174+
const isPlatformAdmin = currentUser.isGlobalAdmin || currentUser.teamRole === "PLATFORM_ADMIN";
175+
if (!isPlatformAdmin && (currentUser.teamId !== targetUser.team_id || !currentUser.isAdmin)) {
173176
return { error: "Unauthorized: You do not have permission to modify this user." };
174177
}
175178

app/(auth)/sign-in/components/LoginComponent.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export function LoginComponent() {
6060
const [email, setEmail] = useState("");
6161
const [mfaStep, setMfaStep] = useState<"login" | "verify">("login");
6262
const [mfaMethod, setMfaMethod] = useState<string | null>(null);
63+
const [availableMfaMethods, setAvailableMfaMethods] = useState<string[]>([]);
6364
const [mfaCode, setMfaCode] = useState("");
6465
const { toast } = useToast();
6566
const [showPassword, setShowPassword] = useState(false);
@@ -163,14 +164,17 @@ export function LoginComponent() {
163164
if (status?.error) {
164165
const msg = status.error as string;
165166

166-
// Catch MFA Required signal
167+
// Catch MFA Required signal — format: MFA_REQUIRED:PRIMARY_METHOD:AVAILABLE_METHODS
167168
if (msg.startsWith("MFA_REQUIRED:")) {
168-
const method = msg.split(":")[1];
169-
setMfaMethod(method);
169+
const parts = msg.split(":");
170+
const primaryMethod = parts[1];
171+
const available = parts[2] ? parts[2].split(",") : [primaryMethod];
172+
setAvailableMfaMethods(available);
173+
setMfaMethod(primaryMethod);
170174
setMfaStep("verify");
171175

172-
// If it's WebAuthn, we can auto-trigger the biometric prompt
173-
if (method === "WEBAUTHN") {
176+
// Only auto-trigger WebAuthn if it's the SOLE method (no TOTP fallback)
177+
if (primaryMethod === "WEBAUTHN" && available.length === 1) {
174178
triggerWebAuthn(normalizedEmail);
175179
}
176180
return;
@@ -469,12 +473,36 @@ export function LoginComponent() {
469473
</Button>
470474
)}
471475

476+
{/* Method switcher */}
477+
{availableMfaMethods.length > 1 && (
478+
<Button
479+
variant="outline"
480+
className="w-full text-xs gap-2 border-white/10 hover:bg-white/5"
481+
onClick={() => {
482+
const altMethod = mfaMethod === "TOTP" ? "WEBAUTHN" : "TOTP";
483+
setMfaMethod(altMethod);
484+
setMfaCode("");
485+
if (altMethod === "WEBAUTHN") {
486+
triggerWebAuthn(email);
487+
}
488+
}}
489+
disabled={isLoading}
490+
>
491+
{mfaMethod === "TOTP" ? (
492+
<><Fingerprint className="h-4 w-4" /> Use Passkey Instead</>
493+
) : (
494+
<><Smartphone className="h-4 w-4" /> Use Authenticator Instead</>
495+
)}
496+
</Button>
497+
)}
498+
472499
<Button
473500
variant="ghost"
474501
className="w-full text-xs text-gray-500 hover:text-white"
475502
onClick={() => {
476503
setMfaStep("login");
477504
setMfaCode("");
505+
setAvailableMfaMethods([]);
478506
}}
479507
>
480508
Back to credentials

0 commit comments

Comments
 (0)