Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions infra/stacks/fusionauth-instance/Pulumi.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ config:
fusionauth-instance:default-from-email: authdev@macro.com
fusionauth-instance:fusionauth-google-idp-id: 17dc55db-c78e-4b19-940b-2e847479a08f
fusionauth-instance:fusionauth-google-gmail-idp-id: b99932ac-bce5-4f1d-99a1-e64760aae811
fusionauth-instance:fusionauth-google-drive-idp-id: c6dd2af6-228e-4925-862c-6ff19dfdc177
fusionauth-instance:google-client-id-secret-key: google-client-id-dev
fusionauth-instance:google-client-secret-secret-key: google-client-secret-dev
fusionauth:host: https://fusionauth-dev.macro.com
Expand Down
1 change: 1 addition & 0 deletions infra/stacks/fusionauth-instance/Pulumi.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ config:
fusionauth-instance:default-from-email: auth@macro.com
fusionauth-instance:fusionauth-google-idp-id: 3707ffc1-c785-4537-a537-10c9ba363e1e
fusionauth-instance:fusionauth-google-gmail-idp-id: 33fbd135-08c1-4975-8987-cf2456752c2d
fusionauth-instance:fusionauth-google-drive-idp-id: b79d07f8-ea16-4ab8-ba64-c5f80515c4d1
fusionauth-instance:google-client-id-secret-key: google-client-id-prod
fusionauth-instance:google-client-secret-secret-key: google-client-secret-prod
fusionauth:host: https://auth.macro.com
Expand Down
42 changes: 42 additions & 0 deletions infra/stacks/fusionauth-instance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,12 @@ const GOOGLE_GMAIL_IDP_ID =
? undefined
: config.require('fusionauth-google-gmail-idp-id');

// Google drive identity provider id
const GOOGLE_DRIVE_IDP_ID =
stack === 'local'
? undefined
: config.require('fusionauth-google-drive-idp-id');

const GOOGLE_CLIENT_ID = aws.secretsmanager
.getSecretVersionOutput({
secretId: config.require('google-client-id-secret-key'),
Expand Down Expand Up @@ -420,3 +426,39 @@ new FusionAuthIdpOpenIdConnect(
protect: stack !== 'local',
}
);

// The google drive identity provider — reuses the shared Google OAuth client
// but requests Drive scopes, so a user can connect Drive independently of Gmail.
new FusionAuthIdpOpenIdConnect(
'google-drive-idp',
{
enabled: true,
idpId: GOOGLE_DRIVE_IDP_ID,
name: 'google_drive',
oauth2ClientId: GOOGLE_CLIENT_ID,
oauth2ClientSecret: GOOGLE_CLIENT_SECRET,
oauth2ClientAuthenticationMethod: 'client_secret_basic',
oauth2AuthorizationEndpoint:
'https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline',
oauth2TokenEndpoint: 'https://oauth2.googleapis.com/token',
oauth2UserInfoEndpoint: 'https://openidconnect.googleapis.com/v1/userinfo',
buttonText: 'GoogleDrive',
oauth2Scope: 'openid profile email https://www.googleapis.com/auth/drive',
oauth2UniqueIdClaim: 'sub',
linkingStrategy: 'LinkByEmail',
debug: stack !== 'prod',
lambdaReconcileId: reconcileSecondaryIdpLinkLambdaId,
applicationConfigurations: [
{
applicationId: pulumi.interpolate`${macroApplication.oauthConfiguration.clientId}`,
enabled: true,
createRegistration: true,
},
],
},
{
dependsOn: macroApplication,
provider: fusionAuthProvider,
protect: stack !== 'local',
}
);
28 changes: 28 additions & 0 deletions js/app/packages/app/component/GoogleDriveLinkCallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LoadingBlock } from '@core/component/LoadingBlock';
import { toast } from '@core/component/Toast/Toast';
import { useFinalizeGoogleDriveLink } from '@queries/drive';
import { useNavigate } from '@solidjs/router';
import { onMount } from 'solid-js';

/**
* Landing page for the Google Drive OAuth redirect. The shared
* `/oauth2/google/callback` has already created the FusionAuth identity-provider
* link; here we call `finalize` (authenticated) to persist the
* `google_drive_links` row, then return to the app.
*/
export function GoogleDriveLinkCallback(props: { successPath: string }) {
const navigate = useNavigate();
const finalize = useFinalizeGoogleDriveLink();

onMount(async () => {
try {
await finalize.mutateAsync();
toast.success('Google Drive connected');
} catch {
toast.failure('Failed to connect Google Drive');
}
navigate(props.successPath, { replace: true });
});

return <LoadingBlock />;
}
5 changes: 5 additions & 0 deletions js/app/packages/app/component/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import { setCookie } from './auth/Shared';
import { Signup } from './auth/Signup';
import { makeEmailAuthComponents } from './EmailAuth';
import { GlobalAppStateProvider } from './GlobalAppState';
import { GoogleDriveLinkCallback } from './GoogleDriveLinkCallback';
import { InteractiveOnboardingModal } from './interactive-onboarding/InteractiveOnboardingModal';
import { Layout } from './Layout';
import { SearchProvider } from './next-soup/search-context';
Expand Down Expand Up @@ -306,6 +307,10 @@ const ROUTES: RouteDefinition[] = [
path: LINK_CALLBACK_PATH,
component: EmailLinkCallback,
},
{
path: '/drive-link-callback',
component: () => <GoogleDriveLinkCallback successPath="/" />,
},
{
path: '/login/popup/success',
component: () => {
Expand Down
111 changes: 111 additions & 0 deletions js/app/packages/app/component/settings/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import PaywallTeamOwnerView from '../paywall/PaywallTeamOwnerView';
import { ROUTER_BASE_CONCAT } from '@app/constants/routerBase';
import { useEmailLinks, useEmailLinksStatus } from '@core/email-link';
import { useInitGmailLink } from '@queries/auth';
import GoogleDriveIcon from '@icon/mcp-google-drive.svg';
import { GoogleDriveImportDialog } from './GoogleDriveImportDialog';
import { useRemoveInboxMutation } from '@queries/email/link';
import {
type SupportedNotificationSettings,
Expand Down Expand Up @@ -421,6 +423,52 @@ export function Account() {
}
};

const [driveLinkStatus, { refetch: refetchDriveLinkStatus }] =
createResource(async (): Promise<GithubLinkStatus> => {
const response = await authServiceClient.checkGoogleDriveLinkStatus();
if (response.isOk()) {
if (response.value.reauthentication_required) {
return 'reauthentication_required';
}
return response.value.connected ? 'linked' : 'unlinked';
}
return 'unlinked';
});

const [driveImportOpen, setDriveImportOpen] = createSignal(false);

// Google's OAuth redirect lands back here; the callback component finalizes
// the link (see GoogleDriveLinkCallback / Root.tsx).
const driveCallbackUrl = () =>
`${window.location.origin}${ROUTER_BASE_CONCAT}drive-link-callback`;

const handleDriveEnable = async () => {
const result = await authServiceClient.initGoogleDriveLink(
driveCallbackUrl()
);
if (result.isOk()) {
window.location.href = result.value.authorization_url;
} else {
toast.failure('Failed to start Google Drive connect flow');
}
};

const handleDriveDisable = async () => {
await authServiceClient.deleteGoogleDriveLink();
refetchDriveLinkStatus();
};

const handleDriveReconnect = async () => {
const result = await authServiceClient.reauthenticateGoogleDrive(
driveCallbackUrl()
);
if (result.isOk()) {
window.location.href = result.value.authorization_url;
} else {
toast.failure('Failed to start Google Drive reconnect flow');
}
};

const firstName = () => {
// Display any updated first name immediately without having to refetch
if (updatedFirstName() !== undefined) return updatedFirstName();
Expand Down Expand Up @@ -759,6 +807,69 @@ export function Account() {
</Show>
</Row>

<Row label="Google Drive">
<Show
when={!driveLinkStatus.loading}
fallback={
<span class="text-sm text-ink-muted">Loading…</span>
}
>
<div class="flex items-center gap-2">
<Show when={driveLinkStatus() === 'linked'}>
<Button
variant="base"
size="sm"
depth={3}
onClick={() => setDriveImportOpen(true)}
>
<GoogleDriveIcon class="size-4" />
Import files
</Button>
</Show>
<Switch
fallback={
<Button
variant="base"
size="sm"
depth={3}
onClick={handleDriveEnable}
>
Enable
</Button>
}
>
<Match
when={driveLinkStatus() === 'reauthentication_required'}
>
<Button
variant="base"
size="sm"
depth={3}
onClick={handleDriveReconnect}
>
Reconnect
</Button>
</Match>
<Match when={driveLinkStatus() === 'linked'}>
<Button
variant="base"
size="sm"
depth={3}
onClick={handleDriveDisable}
>
Disable
</Button>
</Match>
</Switch>
</div>
</Show>
</Row>

<GoogleDriveImportDialog
open={driveImportOpen()}
onOpenChange={setDriveImportOpen}
/>

<NotificationToggle />
</div>

Expand Down
Loading
Loading