Skip to content

Commit

Permalink
Keycloak user email verification (#11686)
Browse files Browse the repository at this point in the history
  • Loading branch information
josephkmh committed Mar 19, 2024
1 parent b0450b1 commit bc2fe74
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 51 deletions.
19 changes: 19 additions & 0 deletions airbyte-api/src/main/openapi/cloud-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,25 @@ paths:
$ref: "#/components/schemas/CreateKeycloakUserResponseBody"
"409":
$ref: "#/components/responses/ExceptionResponse"
/v1/users/send_verification_email:
post:
tags:
- user
summary: Triggers a verification email to be sent to the user
operationId: sendVerificationEmail
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UserIdRequestBody"
required: true
responses:
"204":
description: The verification email was sent successfully.
"404":
$ref: "#/components/responses/NotFoundResponse"
"422":
$ref: "#/components/responses/InvalidInputResponse"
# CLOUD_WORKSPACE
/v1/cloud_workspaces/create:
post:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
executeActionsSubject=Verify your email for Airbyte
executeActionsBodyHtml=<p>Hello,</p><p>Follow this link to verify your email address.</p><p><a href="{0}">{0}</a></p><p>This link will expire within {4}.</p><p>If you didn’t ask to verify this address, you can ignore this email.</p><p>Thanks,</p><p>Your Airbyte team</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parent=base
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ doLogIn=Log in
loginAccountTitle=Log in to Airbyte
usernameOrEmail=Your work email
password=Enter your password
backToApplication=Back to login
backToApplication=Back to Airbyte
backToLogin=Back to login
identity-provider-login-label=Or log in with
errorTitle=Authentication error
39 changes: 39 additions & 0 deletions airbyte-webapp/src/core/api/hooks/cloud/users.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import { useIntl } from "react-intl";

import { useCurrentWorkspaceId } from "area/workspace/utils";
import { useAuthService, useCurrentUser } from "core/services/auth";
import { trackAction } from "core/utils/datadog";
import { AppActionCodes } from "hooks/services/AppMonitoringService";
import { useNotificationService } from "hooks/services/Notification";

import {
webBackendRevokeUserFromWorkspace,
Expand All @@ -13,6 +16,7 @@ import {
updateUser,
webBackendRevokeUserSession,
createKeycloakUser,
sendVerificationEmail,
} from "../../generated/CloudApi";
import { SCOPE_WORKSPACE } from "../../scopes";
import { CreateKeycloakUserRequestBody, UserUpdate } from "../../types/CloudApi";
Expand Down Expand Up @@ -175,3 +179,38 @@ export const useCreateKeycloakUser = () => {
})
);
};

const RESEND_EMAIL_TOAST_ID = "resendEmail";
export const useResendEmailVerification = () => {
const { userId } = useCurrentUser();
const requestOptions = useRequestOptions();
const { sendEmailVerification } = useAuthService();
const { registerNotification } = useNotificationService();
const { formatMessage } = useIntl();

const sendEmail = useMemo(() => {
// The old way to send a verification email with Firebase, via the AuthContext
if (sendEmailVerification) {
return sendEmailVerification;
}
// The new way to send a verification email with Keycloak via our API
return () => sendVerificationEmail({ userId }, requestOptions);
}, [sendEmailVerification, userId, requestOptions]);

return useMutation(sendEmail, {
onSuccess: () => {
registerNotification({
id: RESEND_EMAIL_TOAST_ID,
type: "success",
text: formatMessage({ id: "credits.emailVerification.resendConfirmation" }),
});
},
onError: () => {
registerNotification({
id: RESEND_EMAIL_TOAST_ID,
type: "error",
text: formatMessage({ id: "credits.emailVerification.resendConfirmationError" }),
});
},
});
};
4 changes: 3 additions & 1 deletion airbyte-webapp/src/core/services/auth/AuthContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export type AuthSignUp = (form: SignupFormValues) => Promise<void>;
export type AuthChangeName = (name: string) => Promise<void>;

export type AuthSendEmailVerification = () => Promise<void>;
export type AuthVerifyEmail = (code: string) => Promise<void>;
export type AuthVerifyEmail = FirebaseVerifyEmail | KeycloakVerifyEmail;
type FirebaseVerifyEmail = (code: string) => Promise<void>;
type KeycloakVerifyEmail = () => Promise<void>;
export type AuthLogout = () => Promise<void>;

export type OAuthLoginState = "waiting" | "loading" | "done";
Expand Down
3 changes: 2 additions & 1 deletion airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1658,7 +1658,8 @@
"credits.creditsProblem": "You’re out of credits! To set up connections and run syncs, <lnk>add credits</lnk>.",
"credits.emailVerificationRequired": "You need to verify your email address before you can buy credits.",
"credits.emailVerification.resendConfirmation": "We sent you a new verification link.",
"credits.emailVerification.resend": "Send verification link again",
"credits.emailVerification.resendConfirmationError": "There was an error sending the verification link. Please try again.",
"credits.emailVerification.resend": "Send verification link",
"credits.lowBalance": "Your credit balance is low. Buy more credits to prevent your connections from being disabled or <lnk>enroll in auto-recharge</lnk>.",
"credits.zeroBalance": "All your connections have been disabled because your credit balance is 0. Buy credits or <lnk>enroll in auto-recharge</lnk> to enable your data to sync.",

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,38 +186,7 @@ export const CloudAuthService: React.FC<PropsWithChildren> = ({ children }) => {
console.error("sendEmailVerifiedLink should be used within auth flow");
throw new Error("Cannot send verification email if firebaseUser is null.");
}
return sendEmailVerification(firebaseUser)
.then(() => {
registerNotification({
id: "workspace.emailVerificationResendSuccess",
text: <FormattedMessage id="credits.emailVerification.resendConfirmation" />,
type: "success",
});
})
.catch((error) => {
switch (error.code) {
case AuthErrorCodes.NETWORK_REQUEST_FAILED:
registerNotification({
id: error.code,
text: <FormattedMessage id={FirebaseAuthMessageId.NetworkFailure} />,
type: "error",
});
break;
case AuthErrorCodes.TOO_MANY_ATTEMPTS_TRY_LATER:
registerNotification({
id: error.code,
text: <FormattedMessage id={FirebaseAuthMessageId.TooManyRequests} />,
type: "warning",
});
break;
default:
registerNotification({
id: error.code,
text: <FormattedMessage id={FirebaseAuthMessageId.DefaultError} />,
type: "error",
});
}
});
return sendEmailVerification(firebaseUser);
},
verifyEmail: verifyFirebaseEmail,
};
Expand Down Expand Up @@ -389,7 +358,6 @@ export const CloudAuthService: React.FC<PropsWithChildren> = ({ children }) => {
getAirbyteUser,
keycloakAuth,
logout,
registerNotification,
resendWithSignInLink,
updateAirbyteUser,
verifyFirebaseEmail,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,24 @@ import { ExternalLink, Link } from "components/ui/Link";
import { Message } from "components/ui/Message";

import { useCurrentWorkspace } from "core/api";
import { useGetCloudWorkspace } from "core/api/cloud";
import { useGetCloudWorkspace, useResendEmailVerification } from "core/api/cloud";
import { CloudWorkspaceReadCreditStatus, CloudWorkspaceReadWorkspaceTrialStatus } from "core/api/types/CloudApi";
import { AuthSendEmailVerification, useAuthService } from "core/services/auth";
import { useAuthService } from "core/services/auth";
import { links } from "core/utils/links";
import { useExperiment } from "hooks/services/Experiment";

const LOW_BALANCE_CREDIT_THRESHOLD = 20;

interface EmailVerificationHintProps {
sendEmailVerification: AuthSendEmailVerification;
}

export const EmailVerificationHint: React.FC<EmailVerificationHintProps> = ({ sendEmailVerification }) => {
const onResendVerificationMail = async () => {
return sendEmailVerification();
};
export const EmailVerificationHint: React.FC = () => {
const { mutateAsync: resendEmailVerification, isLoading } = useResendEmailVerification();

return (
<Message
type="info"
text={<FormattedMessage id="credits.emailVerificationRequired" />}
actionBtnText={<FormattedMessage id="credits.emailVerification.resend" />}
onAction={onResendVerificationMail}
actionBtnProps={{ isLoading }}
onAction={resendEmailVerification}
/>
);
};
Expand Down Expand Up @@ -99,15 +94,13 @@ const LowCreditBalanceHint: React.FC = () => {
};

export const BillingBanners: React.FC = () => {
const { sendEmailVerification, emailVerified } = useAuthService();
const { emailVerified } = useAuthService();

const isAutoRechargeEnabled = useExperiment("billing.autoRecharge", false);

return (
<FlexContainer direction="column">
{!emailVerified && sendEmailVerification && (
<EmailVerificationHint sendEmailVerification={sendEmailVerification} />
)}
{!emailVerified && <EmailVerificationHint />}
{isAutoRechargeEnabled ? <AutoRechargeEnabledBanner /> : <LowCreditBalanceHint />}
</FlexContainer>
);
Expand Down

0 comments on commit bc2fe74

Please sign in to comment.