mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-22 21:36:25 +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} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
productionBrowserSourceMaps: true,
|
||||
transpilePackages: [
|
||||
"@rallly/backend",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
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";
|
||||
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}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue