Complete SaaS self-service onboarding sprint: - Stripe-powered signup flow: pricing page → checkout → provisioning → activation - Refresh token infrastructure: 1h access tokens + 30-day httpOnly cookie refresh - TOTP MFA with QR setup, recovery codes, and login challenge flow - Google + Azure AD SSO (conditional on env vars) with account linking - WebAuthn passkey registration and passwordless login - Guided onboarding checklist with server-side progress tracking - Stubbed email service (console + DB logging, ready for real provider) - Settings page with tabbed security settings (MFA, passkeys, linked accounts) - Login page enhanced with MFA verification, SSO buttons, passkey login - Database migration 015 with all new tables and columns - Version bump to 2026.03.17 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import {
|
|
Card, Title, Text, Stack, Group, Button, Badge, Alert,
|
|
} from '@mantine/core';
|
|
import { IconBrandGoogle, IconBrandAzure, IconLink, IconLinkOff, IconAlertCircle } from '@tabler/icons-react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { notifications } from '@mantine/notifications';
|
|
import api from '../../services/api';
|
|
|
|
export function LinkedAccounts() {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: providers } = useQuery({
|
|
queryKey: ['sso-providers'],
|
|
queryFn: async () => {
|
|
const { data } = await api.get('/auth/sso/providers');
|
|
return data;
|
|
},
|
|
});
|
|
|
|
const { data: profile } = useQuery({
|
|
queryKey: ['auth-profile'],
|
|
queryFn: async () => {
|
|
const { data } = await api.get('/auth/profile');
|
|
return data;
|
|
},
|
|
});
|
|
|
|
const unlinkMutation = useMutation({
|
|
mutationFn: (provider: string) => api.delete(`/auth/sso/unlink/${provider}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['auth-profile'] });
|
|
notifications.show({ message: 'Account unlinked', color: 'orange' });
|
|
},
|
|
onError: (err: any) => notifications.show({ message: err.response?.data?.message || 'Failed to unlink', color: 'red' }),
|
|
});
|
|
|
|
const noProviders = !providers?.google && !providers?.azure;
|
|
|
|
return (
|
|
<Card withBorder p="lg">
|
|
<Group justify="space-between" mb="md">
|
|
<div>
|
|
<Title order={4}>Linked Accounts</Title>
|
|
<Text size="sm" c="dimmed">Connect third-party accounts for single sign-on</Text>
|
|
</div>
|
|
</Group>
|
|
|
|
{noProviders && (
|
|
<Alert color="gray" variant="light" icon={<IconAlertCircle size={16} />}>
|
|
No SSO providers are configured. Contact your administrator to enable Google or Microsoft SSO.
|
|
</Alert>
|
|
)}
|
|
|
|
<Stack gap="md">
|
|
{providers?.google && (
|
|
<Group justify="space-between" p="sm" style={{ border: '1px solid var(--mantine-color-gray-3)', borderRadius: 8 }}>
|
|
<Group>
|
|
<IconBrandGoogle size={24} color="#4285F4" />
|
|
<div>
|
|
<Text fw={500}>Google</Text>
|
|
<Text size="xs" c="dimmed">Sign in with your Google account</Text>
|
|
</div>
|
|
</Group>
|
|
<Button
|
|
variant="light"
|
|
size="sm"
|
|
leftSection={<IconLink size={14} />}
|
|
onClick={() => window.location.href = '/api/auth/google'}
|
|
>
|
|
Connect
|
|
</Button>
|
|
</Group>
|
|
)}
|
|
|
|
{providers?.azure && (
|
|
<Group justify="space-between" p="sm" style={{ border: '1px solid var(--mantine-color-gray-3)', borderRadius: 8 }}>
|
|
<Group>
|
|
<IconBrandAzure size={24} color="#0078D4" />
|
|
<div>
|
|
<Text fw={500}>Microsoft</Text>
|
|
<Text size="xs" c="dimmed">Sign in with your Microsoft account</Text>
|
|
</div>
|
|
</Group>
|
|
<Button
|
|
variant="light"
|
|
size="sm"
|
|
leftSection={<IconLink size={14} />}
|
|
onClick={() => window.location.href = '/api/auth/azure'}
|
|
>
|
|
Connect
|
|
</Button>
|
|
</Group>
|
|
)}
|
|
</Stack>
|
|
</Card>
|
|
);
|
|
}
|