mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-23 05:46:20 +02:00
✨ Add login with microsoft (#1008)
This commit is contained in:
parent
93f98cffe6
commit
3abd3608be
8 changed files with 104 additions and 40 deletions
|
@ -10,7 +10,6 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
|
||||||
productionBrowserSourceMaps: true,
|
productionBrowserSourceMaps: true,
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@rallly/backend",
|
"@rallly/backend",
|
||||||
|
|
|
@ -221,7 +221,6 @@
|
||||||
"integrations": "Integrations",
|
"integrations": "Integrations",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"unlockFeatures": "Unlock all Pro features.",
|
"unlockFeatures": "Unlock all Pro features.",
|
||||||
"back": "Back",
|
|
||||||
"pollStatusAll": "All",
|
"pollStatusAll": "All",
|
||||||
"pollStatusLive": "Live",
|
"pollStatusLive": "Live",
|
||||||
"pollStatusFinalized": "Finalized",
|
"pollStatusFinalized": "Finalized",
|
||||||
|
@ -238,5 +237,8 @@
|
||||||
"registrations": "Registrations",
|
"registrations": "Registrations",
|
||||||
"inviteParticipantsDescription": "Copy and share the invite link to start gathering responses from your participants.",
|
"inviteParticipantsDescription": "Copy and share the invite link to start gathering responses from your participants.",
|
||||||
"inviteLink": "Invite Link",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
1
apps/web/public/static/microsoft.svg
Normal file
1
apps/web/public/static/microsoft.svg
Normal 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 |
|
@ -1,7 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { UserIcon } from "lucide-react";
|
import { AlertTriangleIcon, UserIcon } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { getProviders, signIn, useSession } from "next-auth/react";
|
import { getProviders, signIn, useSession } from "next-auth/react";
|
||||||
|
@ -21,6 +22,7 @@ const allowGuestAccess = !isSelfHosted;
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const { register, handleSubmit, getValues, formState, setError } = useForm<{
|
const { register, handleSubmit, getValues, formState, setError } = useForm<{
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -38,21 +40,13 @@ export function LoginForm() {
|
||||||
const [email, setEmail] = React.useState<string>();
|
const [email, setEmail] = React.useState<string>();
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
const router = useRouter();
|
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 alternativeLoginMethods = React.useMemo(() => {
|
||||||
const res: Array<{ login: () => void; icon: JSX.Element; name: string }> =
|
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) {
|
if (providers?.oidc) {
|
||||||
res.push({
|
res.push({
|
||||||
login: () => {
|
login: () => {
|
||||||
|
@ -78,6 +72,35 @@ export function LoginForm() {
|
||||||
name: t("loginWith", { provider: providers.google.name }),
|
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;
|
return res;
|
||||||
}, [callbackUrl, providers, router, t]);
|
}, [callbackUrl, providers, router, t]);
|
||||||
|
|
||||||
|
@ -148,7 +171,7 @@ export function LoginForm() {
|
||||||
total: 2,
|
total: 2,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<fieldset className="mb-4">
|
<fieldset className="mb-2.5">
|
||||||
<label htmlFor="email" className="mb-1 text-gray-500">
|
<label htmlFor="email" className="mb-1 text-gray-500">
|
||||||
{t("email")}
|
{t("email")}
|
||||||
</label>
|
</label>
|
||||||
|
@ -176,12 +199,35 @@ export function LoginForm() {
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className=""
|
className=""
|
||||||
>
|
>
|
||||||
{t("continue")}
|
{t("loginWith", {
|
||||||
|
provider: t("email"),
|
||||||
|
})}
|
||||||
</Button>
|
</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 ? (
|
{alternativeLoginMethods.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<hr className="border-grey-500 my-4 border-t" />
|
<div className="relative my-4">
|
||||||
<div className="grid gap-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) => (
|
{alternativeLoginMethods.map((method, i) => (
|
||||||
<Button size="lg" key={i} onClick={method.login}>
|
<Button size="lg" key={i} onClick={method.login}>
|
||||||
{method.icon}
|
{method.icon}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { NextAuthOptions, RequestInternal } from "next-auth";
|
||||||
import NextAuth, {
|
import NextAuth, {
|
||||||
getServerSession as getServerSessionWithOptions,
|
getServerSession as getServerSessionWithOptions,
|
||||||
} from "next-auth/next";
|
} from "next-auth/next";
|
||||||
|
import AzureADProvider from "next-auth/providers/azure-ad";
|
||||||
import CredentialsProvider from "next-auth/providers/credentials";
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
import EmailProvider from "next-auth/providers/email";
|
import EmailProvider from "next-auth/providers/email";
|
||||||
import GoogleProvider from "next-auth/providers/google";
|
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) =>
|
const getAuthOptions = (...args: GetServerSessionParams) =>
|
||||||
({
|
({
|
||||||
adapter: CustomPrismaAdapter(prisma),
|
adapter: CustomPrismaAdapter(prisma),
|
||||||
|
|
|
@ -44,9 +44,9 @@ test.describe.serial(() => {
|
||||||
// your login page test logic
|
// your login page test logic
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("jessie.smith@example.com")
|
.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
|
// Make sure the user doesn't exist yet and that logging in is not possible
|
||||||
await expect(
|
await expect(
|
||||||
|
@ -59,10 +59,10 @@ test.describe.serial(() => {
|
||||||
|
|
||||||
await page.getByText("Create an account").waitFor();
|
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
|
await page
|
||||||
.getByPlaceholder("jessie.smith@example.com")
|
.getByPlaceholder("jessie.smith@example.com")
|
||||||
.type(testUserEmail);
|
.fill(testUserEmail);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ test.describe.serial(() => {
|
||||||
|
|
||||||
const code = await getCode();
|
const code = await getCode();
|
||||||
|
|
||||||
await codeInput.type(code);
|
await codeInput.fill(code);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
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.getByText("Create an account").waitFor();
|
||||||
|
|
||||||
await page.getByPlaceholder("Jessie Smith").type("Test User");
|
await page.getByPlaceholder("Jessie Smith").fill("Test User");
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("jessie.smith@example.com")
|
.getByPlaceholder("jessie.smith@example.com")
|
||||||
.type(testUserEmail);
|
.fill(testUserEmail);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
|
@ -101,9 +101,9 @@ test.describe.serial(() => {
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("jessie.smith@example.com")
|
.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, {
|
const { email } = await mailServer.captureOne(testUserEmail, {
|
||||||
wait: 5000,
|
wait: 5000,
|
||||||
|
@ -131,13 +131,13 @@ test.describe.serial(() => {
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("jessie.smith@example.com")
|
.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();
|
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();
|
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
|
@ -151,13 +151,13 @@ test.describe.serial(() => {
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("jessie.smith@example.com")
|
.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();
|
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();
|
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ const alertVariants = cva(
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-gray-50 text-foreground",
|
default: "bg-gray-50 text-foreground",
|
||||||
destructive:
|
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: {
|
defaultVariants: {
|
||||||
|
@ -46,7 +46,7 @@ const AlertTitle = React.forwardRef<
|
||||||
<h5
|
<h5
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-2 text-sm font-bold leading-none tracking-tight",
|
"mb-2 text-sm font-semibold leading-none tracking-tight",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -60,10 +60,7 @@ const AlertDescription = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
"text-muted-foreground text-sm [&_p]:leading-relaxed",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -64,6 +64,10 @@
|
||||||
"DISABLE_LANDING_PAGE",
|
"DISABLE_LANDING_PAGE",
|
||||||
"EMAIL_PROVIDER",
|
"EMAIL_PROVIDER",
|
||||||
"MAINTENANCE_MODE",
|
"MAINTENANCE_MODE",
|
||||||
|
"MICROSOFT_CLIENT_ID",
|
||||||
|
"MICROSOFT_CLIENT_SECRET",
|
||||||
|
"MICROSOFT_TENANT_ID",
|
||||||
|
"NEXT_PUBLIC_ABOUT_PAGE_URL",
|
||||||
"NEXT_PUBLIC_APP_BASE_URL",
|
"NEXT_PUBLIC_APP_BASE_URL",
|
||||||
"NEXT_PUBLIC_APP_VERSION",
|
"NEXT_PUBLIC_APP_VERSION",
|
||||||
"NEXT_PUBLIC_BASE_URL",
|
"NEXT_PUBLIC_BASE_URL",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue