Add login with microsoft (#1008)

This commit is contained in:
Luke Vella 2024-01-30 16:45:49 +07:00 committed by GitHub
parent 93f98cffe6
commit 3abd3608be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 104 additions and 40 deletions

View file

@ -10,7 +10,6 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
productionBrowserSourceMaps: true,
transpilePackages: [
"@rallly/backend",

View file

@ -221,7 +221,6 @@
"integrations": "Integrations",
"contacts": "Contacts",
"unlockFeatures": "Unlock all Pro features.",
"back": "Back",
"pollStatusAll": "All",
"pollStatusLive": "Live",
"pollStatusFinalized": "Finalized",
@ -238,5 +237,8 @@
"registrations": "Registrations",
"inviteParticipantsDescription": "Copy and share the invite link to start gathering responses from your participants.",
"inviteLink": "Invite Link",
"inviteParticipantLinkInfo": "Anyone with this link will be able to vote on your poll."
"inviteParticipantLinkInfo": "Anyone with this link will be able to vote on your poll.",
"accountNotLinkedTitle": "Your account cannot be linked to an existing user",
"accountNotLinkedDescription": "A user with this email already exists. Please log in using the original method.",
"or": "Or"
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21"><title>MS-SymbolLockup</title><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>

After

Width:  |  Height:  |  Size: 343 B

View file

@ -1,7 +1,8 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
import { Button } from "@rallly/ui/button";
import { useQuery } from "@tanstack/react-query";
import { UserIcon } from "lucide-react";
import { AlertTriangleIcon, UserIcon } from "lucide-react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { getProviders, signIn, useSession } from "next-auth/react";
@ -21,6 +22,7 @@ const allowGuestAccess = !isSelfHosted;
export function LoginForm() {
const { t } = useTranslation();
const searchParams = useSearchParams();
const { register, handleSubmit, getValues, formState, setError } = useForm<{
email: string;
@ -38,21 +40,13 @@ export function LoginForm() {
const [email, setEmail] = React.useState<string>();
const posthog = usePostHog();
const router = useRouter();
const callbackUrl = (useSearchParams()?.get("callbackUrl") as string) ?? "/";
const callbackUrl = searchParams?.get("callbackUrl") ?? "/";
const error = searchParams?.get("error");
const alternativeLoginMethods = React.useMemo(() => {
const res: Array<{ login: () => void; icon: JSX.Element; name: string }> =
[];
if (allowGuestAccess) {
res.push({
login: () => {
router.push(callbackUrl);
},
icon: <UserIcon className="text-muted-foreground size-5" />,
name: t("continueAsGuest"),
});
}
if (providers?.oidc) {
res.push({
login: () => {
@ -78,6 +72,35 @@ export function LoginForm() {
name: t("loginWith", { provider: providers.google.name }),
});
}
if (providers?.["azure-ad"]) {
res.push({
login: () => {
signIn("azure-ad", {
callbackUrl,
});
},
icon: (
<Image
src="/static/microsoft.svg"
width={20}
height={20}
alt="Azure AD"
/>
),
name: t("loginWith", { provider: "Microsoft" }),
});
}
if (allowGuestAccess) {
res.push({
login: () => {
router.push(callbackUrl);
},
icon: <UserIcon className="text-muted-foreground size-5" />,
name: t("continueAsGuest"),
});
}
return res;
}, [callbackUrl, providers, router, t]);
@ -148,7 +171,7 @@ export function LoginForm() {
total: 2,
})}
</p>
<fieldset className="mb-4">
<fieldset className="mb-2.5">
<label htmlFor="email" className="mb-1 text-gray-500">
{t("email")}
</label>
@ -176,12 +199,35 @@ export function LoginForm() {
variant="primary"
className=""
>
{t("continue")}
{t("loginWith", {
provider: t("email"),
})}
</Button>
{error === "OAuthAccountNotLinked" ? (
<Alert icon={AlertTriangleIcon} variant="destructive">
<AlertTitle>
{t("accountNotLinkedTitle", {
defaultValue:
"Your account cannot be linked to an existing user",
})}
</AlertTitle>
<AlertDescription>
{t("accountNotLinkedDescription", {
defaultValue:
"A user with this email already exists. Please log in using the original method.",
})}
</AlertDescription>
</Alert>
) : null}
{alternativeLoginMethods.length > 0 ? (
<>
<hr className="border-grey-500 my-4 border-t" />
<div className="grid gap-4">
<div className="relative my-4">
<hr className="border-grey-500 absolute top-1/2 w-full border-t" />
<span className="absolute left-1/2 -translate-x-1/2 -translate-y-1/2 transform bg-white px-2 text-center text-xs uppercase text-gray-400">
{t("or", { defaultValue: "Or" })}
</span>
</div>
<div className="grid gap-2.5">
{alternativeLoginMethods.map((method, i) => (
<Button size="lg" key={i} onClick={method.login}>
{method.icon}

View file

@ -13,6 +13,7 @@ import { NextAuthOptions, RequestInternal } from "next-auth";
import NextAuth, {
getServerSession as getServerSessionWithOptions,
} from "next-auth/next";
import AzureADProvider from "next-auth/providers/azure-ad";
import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google";
@ -146,6 +147,20 @@ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
);
}
if (
process.env.MICROSOFT_TENANT_ID &&
process.env.MICROSOFT_CLIENT_ID &&
process.env.MICROSOFT_CLIENT_SECRET
) {
providers.push(
AzureADProvider({
tenantId: process.env.MICROSOFT_TENANT_ID,
clientId: process.env.MICROSOFT_CLIENT_ID,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
}),
);
}
const getAuthOptions = (...args: GetServerSessionParams) =>
({
adapter: CustomPrismaAdapter(prisma),

View file

@ -44,9 +44,9 @@ test.describe.serial(() => {
// your login page test logic
await page
.getByPlaceholder("jessie.smith@example.com")
.type(testUserEmail);
.fill(testUserEmail);
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByRole("button", { name: "Login with Email" }).click();
// Make sure the user doesn't exist yet and that logging in is not possible
await expect(
@ -59,10 +59,10 @@ test.describe.serial(() => {
await page.getByText("Create an account").waitFor();
await page.getByPlaceholder("Jessie Smith").type("Test User");
await page.getByPlaceholder("Jessie Smith").fill("Test User");
await page
.getByPlaceholder("jessie.smith@example.com")
.type(testUserEmail);
.fill(testUserEmail);
await page.getByRole("button", { name: "Continue", exact: true }).click();
@ -70,7 +70,7 @@ test.describe.serial(() => {
const code = await getCode();
await codeInput.type(code);
await codeInput.fill(code);
await page.getByRole("button", { name: "Continue", exact: true }).click();
@ -84,10 +84,10 @@ test.describe.serial(() => {
await page.getByText("Create an account").waitFor();
await page.getByPlaceholder("Jessie Smith").type("Test User");
await page.getByPlaceholder("Jessie Smith").fill("Test User");
await page
.getByPlaceholder("jessie.smith@example.com")
.type(testUserEmail);
.fill(testUserEmail);
await page.getByRole("button", { name: "Continue", exact: true }).click();
@ -101,9 +101,9 @@ test.describe.serial(() => {
await page
.getByPlaceholder("jessie.smith@example.com")
.type(testUserEmail);
.fill(testUserEmail);
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByRole("button", { name: "Login with Email" }).click();
const { email } = await mailServer.captureOne(testUserEmail, {
wait: 5000,
@ -131,13 +131,13 @@ test.describe.serial(() => {
await page
.getByPlaceholder("jessie.smith@example.com")
.type(testUserEmail);
.fill(testUserEmail);
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByRole("button", { name: "Login with Email" }).click();
const code = await getCode();
await page.getByPlaceholder("Enter your 6-digit code").type(code);
await page.getByPlaceholder("Enter your 6-digit code").fill(code);
await page.getByRole("button", { name: "Continue", exact: true }).click();
@ -151,13 +151,13 @@ test.describe.serial(() => {
await page
.getByPlaceholder("jessie.smith@example.com")
.type("Test@example.com");
.fill("Test@example.com");
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByRole("button", { name: "Login with Email" }).click();
const code = await getCode();
await page.getByPlaceholder("Enter your 6-digit code").type(code);
await page.getByPlaceholder("Enter your 6-digit code").fill(code);
await page.getByRole("button", { name: "Continue", exact: true }).click();

View file

@ -10,7 +10,7 @@ const alertVariants = cva(
variant: {
default: "bg-gray-50 text-foreground",
destructive:
"text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive",
"text-destructive bg-rose-50 border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive",
},
},
defaultVariants: {
@ -46,7 +46,7 @@ const AlertTitle = React.forwardRef<
<h5
ref={ref}
className={cn(
"mb-2 text-sm font-bold leading-none tracking-tight",
"mb-2 text-sm font-semibold leading-none tracking-tight",
className,
)}
{...props}
@ -60,10 +60,7 @@ const AlertDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-muted-foreground text-sm [&_p]:leading-relaxed",
className,
)}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));

View file

@ -64,6 +64,10 @@
"DISABLE_LANDING_PAGE",
"EMAIL_PROVIDER",
"MAINTENANCE_MODE",
"MICROSOFT_CLIENT_ID",
"MICROSOFT_CLIENT_SECRET",
"MICROSOFT_TENANT_ID",
"NEXT_PUBLIC_ABOUT_PAGE_URL",
"NEXT_PUBLIC_APP_BASE_URL",
"NEXT_PUBLIC_APP_VERSION",
"NEXT_PUBLIC_BASE_URL",