From 412697c418bdc49186985c35d1a9efbc9fa0d191 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 13 Mar 2025 12:52:30 +0100 Subject: [PATCH] feat(social-login): social login with Google is working (#7218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Víctor Fernández Poyatos --- api/src/backend/api/adapters.py | 4 ++ ui/app/api/auth/callback/github/route.ts | 68 ++++++++++++++++++++++++ ui/app/api/auth/callback/google/route.ts | 68 ++++++++++++++++++++++++ ui/auth.config.ts | 37 ++++++++++++- ui/components/auth/oss/auth-form.tsx | 12 +++-- ui/lib/helper.ts | 35 +++++++++++- ui/types/authFormSchema.ts | 2 + 7 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 ui/app/api/auth/callback/github/route.ts create mode 100644 ui/app/api/auth/callback/google/route.ts diff --git a/api/src/backend/api/adapters.py b/api/src/backend/api/adapters.py index 9a8cb2f044..7ccda0336c 100644 --- a/api/src/backend/api/adapters.py +++ b/api/src/backend/api/adapters.py @@ -30,6 +30,10 @@ class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter): with transaction.atomic(using=MainRouter.admin_db): user = super().save_user(request, sociallogin, form) user.save(using=MainRouter.admin_db) + social_account_name = sociallogin.account.extra_data.get("name") + if social_account_name: + user.name = social_account_name + user.save(using=MainRouter.admin_db) tenant = Tenant.objects.using(MainRouter.admin_db).create( name=f"{user.email.split('@')[0]} default tenant" diff --git a/ui/app/api/auth/callback/github/route.ts b/ui/app/api/auth/callback/github/route.ts new file mode 100644 index 0000000000..8bef55f8a3 --- /dev/null +++ b/ui/app/api/auth/callback/github/route.ts @@ -0,0 +1,68 @@ +"use server"; + +import { NextResponse } from "next/server"; + +import { signIn } from "@/auth.config"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + + const keyServer = process.env.API_BASE_URL; + + const code = searchParams.get("code"); + + const params = new URLSearchParams(); + params.append("code", code || ""); + + if (!code) { + return NextResponse.json( + { error: "Authorization code is missing" }, + { status: 400 }, + ); + } + + try { + const response = await fetch(`${keyServer}/tokens/github`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params.toString(), + }); + + if (!response.ok) { + throw new Error("Failed to exchange code for tokens"); + } + + const data = await response.json(); + const { access, refresh } = data.data.attributes; + + try { + const result = await signIn("social-oauth", { + accessToken: access, + refreshToken: refresh, + redirect: false, + callbackUrl: "/", + }); + + if (result?.error) { + throw new Error(result.error); + } + + return NextResponse.redirect(new URL("/", req.url)); + } catch (error) { + // eslint-disable-next-line no-console + console.error("SignIn error:", error); + return NextResponse.redirect( + new URL("/sign-in?error=AuthenticationFailed", req.url), + ); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error in Github callback:", error); + return NextResponse.json( + { error: (error as Error).message }, + { status: 500 }, + ); + } +} diff --git a/ui/app/api/auth/callback/google/route.ts b/ui/app/api/auth/callback/google/route.ts new file mode 100644 index 0000000000..d52b4eb222 --- /dev/null +++ b/ui/app/api/auth/callback/google/route.ts @@ -0,0 +1,68 @@ +"use server"; + +import { NextResponse } from "next/server"; + +import { signIn } from "@/auth.config"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + + const keyServer = process.env.API_BASE_URL; + + const code = searchParams.get("code"); + + const params = new URLSearchParams(); + params.append("code", code || ""); + + if (!code) { + return NextResponse.json( + { error: "Authorization code is missing" }, + { status: 400 }, + ); + } + + try { + const response = await fetch(`${keyServer}/tokens/google`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params.toString(), + }); + + if (!response.ok) { + throw new Error("Failed to exchange code for tokens"); + } + + const data = await response.json(); + const { access, refresh } = data.data.attributes; + + try { + const result = await signIn("social-oauth", { + accessToken: access, + refreshToken: refresh, + redirect: false, + callbackUrl: "/", + }); + + if (result?.error) { + throw new Error(result.error); + } + + return NextResponse.redirect(new URL("/", req.url)); + } catch (error) { + // eslint-disable-next-line no-console + console.error("SignIn error:", error); + return NextResponse.redirect( + new URL("/sign-in?error=AuthenticationFailed", req.url), + ); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error in Google callback:", error); + return NextResponse.json( + { error: (error as Error).message }, + { status: 500 }, + ); + } +} diff --git a/ui/auth.config.ts b/ui/auth.config.ts index 2d5d223938..6db01ce49a 100644 --- a/ui/auth.config.ts +++ b/ui/auth.config.ts @@ -99,13 +99,48 @@ export const authConfig = { }; }, }), + Credentials({ + id: "social-oauth", + name: "social-oauth", + credentials: { + accessToken: { label: "Access Token", type: "text" }, + refreshToken: { label: "Refresh Token", type: "text" }, + }, + async authorize(credentials) { + const accessToken = credentials?.accessToken; + + if (!accessToken) { + return null; + } + + try { + const userMeResponse = await getUserByMe(accessToken as string); + + const user = { + name: userMeResponse.name, + email: userMeResponse.email, + company: userMeResponse?.company, + dateJoined: userMeResponse.dateJoined, + }; + + return { + ...user, + accessToken: credentials.accessToken, + refreshToken: credentials.refreshToken, + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error in authorize:", error); + return null; + } + }, + }), ], callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; const isOnDashboard = nextUrl.pathname.startsWith("/"); const isSignUpPage = nextUrl.pathname === "/sign-up"; - //CLOUD API CHANGES // Allow access to sign-up page if (isSignUpPage) return true; diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx index 38573accd7..b9c30cc036 100644 --- a/ui/components/auth/oss/auth-form.tsx +++ b/ui/components/auth/oss/auth-form.tsx @@ -1,7 +1,8 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Checkbox, Link } from "@nextui-org/react"; +import { Icon } from "@iconify/react"; +import { Button, Checkbox, Divider, Link } from "@nextui-org/react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -17,6 +18,7 @@ import { FormField, FormMessage, } from "@/components/ui/form"; +import { getAuthUrl } from "@/lib/helper"; import { ApiError, authFormSchema } from "@/types"; export const AuthForm = ({ @@ -285,7 +287,7 @@ export const AuthForm = ({ - {/* {type === "sign-in" && ( + {type === "sign-in" && ( <>
@@ -298,6 +300,8 @@ export const AuthForm = ({ } variant="bordered" + as="a" + href={getAuthUrl("google")} > Continue with Google @@ -310,12 +314,14 @@ export const AuthForm = ({ /> } variant="bordered" + as="a" + href={getAuthUrl("github")} > Continue with Github
- )} */} + )} {type === "sign-in" ? (

Need to create an account?  diff --git a/ui/lib/helper.ts b/ui/lib/helper.ts index 66a0e413f9..e985390c7a 100644 --- a/ui/lib/helper.ts +++ b/ui/lib/helper.ts @@ -1,5 +1,38 @@ import { getTask } from "@/actions/task"; -import { MetaDataProps, PermissionInfo } from "@/types"; +import { AuthSocialProvider, MetaDataProps, PermissionInfo } from "@/types"; + +export const getAuthUrl = (provider: AuthSocialProvider) => { + const config = { + google: { + baseUrl: "https://accounts.google.com/o/oauth2/v2/auth", + params: { + redirect_uri: process.env.NEXT_PUBLIC_GOOGLE_CALLBACK_URI, + prompt: "consent", + response_type: "code", + client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, + scope: "openid email profile", + access_type: "offline", + }, + }, + github: { + baseUrl: "https://github.com/login/oauth/authorize", + params: { + client_id: process.env.NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID, + redirect_uri: process.env.NEXT_PUBLIC_GITHUB_OAUTH_CALLBACK_URL, + scope: "user:email", + }, + }, + }; + + const { baseUrl, params } = config[provider]; + const url = new URL(baseUrl); + + Object.entries(params).forEach(([key, value]) => { + url.searchParams.set(key, value || ""); + }); + + return url.toString(); +}; export async function checkTaskStatus( taskId: string, diff --git a/ui/types/authFormSchema.ts b/ui/types/authFormSchema.ts index 58737a1f8a..e08c14ea53 100644 --- a/ui/types/authFormSchema.ts +++ b/ui/types/authFormSchema.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +export type AuthSocialProvider = "google" | "github"; + export const authFormSchema = (type: string) => z .object({