diff --git a/apps/backend/src/services/auth/providers/farcaster.provider.ts b/apps/backend/src/services/auth/providers/farcaster.provider.ts
new file mode 100644
index 00000000..55d6f877
--- /dev/null
+++ b/apps/backend/src/services/auth/providers/farcaster.provider.ts
@@ -0,0 +1,43 @@
+import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
+import { NeynarAPIClient } from '@neynar/nodejs-sdk';
+
+const client = new NeynarAPIClient({
+ apiKey: process.env.NEYNAR_SECRET_KEY || '00000000-000-0000-000-000000000000',
+});
+
+export class FarcasterProvider implements ProvidersInterface {
+ generateLink() {
+ return '';
+ }
+
+ async getToken(code: string) {
+ const data = JSON.parse(Buffer.from(code, 'base64').toString());
+ const status = await client.lookupSigner({signerUuid: data.signer_uuid});
+ if (status.status === 'approved') {
+ return data.signer_uuid;
+ }
+
+ return '';
+ }
+
+ async getUser(providerToken: string) {
+ const status = await client.lookupSigner({signerUuid: providerToken});
+ if (status.status !== 'approved') {
+ return {
+ id: '',
+ email: '',
+ };
+ }
+
+
+ // const { client, oauth2 } = clientAndYoutube();
+ // client.setCredentials({ access_token: providerToken });
+ // const user = oauth2(client);
+ // const { data } = await user.userinfo.get();
+
+ return {
+ id: String('farcaster_' + status.fid),
+ email: String('farcaster_' + status.fid),
+ };
+ }
+}
diff --git a/apps/backend/src/services/auth/providers/providers.factory.ts b/apps/backend/src/services/auth/providers/providers.factory.ts
index 61048d50..0864e722 100644
--- a/apps/backend/src/services/auth/providers/providers.factory.ts
+++ b/apps/backend/src/services/auth/providers/providers.factory.ts
@@ -2,6 +2,7 @@ import { Provider } from '@prisma/client';
import { GithubProvider } from '@gitroom/backend/services/auth/providers/github.provider';
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
import { GoogleProvider } from '@gitroom/backend/services/auth/providers/google.provider';
+import { FarcasterProvider } from '@gitroom/backend/services/auth/providers/farcaster.provider';
export class ProvidersFactory {
static loadProvider(provider: Provider): ProvidersInterface {
@@ -10,6 +11,8 @@ export class ProvidersFactory {
return new GithubProvider();
case Provider.GOOGLE:
return new GoogleProvider();
+ case Provider.FARCASTER:
+ return new FarcasterProvider();
}
}
}
diff --git a/apps/frontend/src/app/auth/layout.tsx b/apps/frontend/src/app/auth/layout.tsx
index 14082286..9b5392d7 100644
--- a/apps/frontend/src/app/auth/layout.tsx
+++ b/apps/frontend/src/app/auth/layout.tsx
@@ -68,7 +68,7 @@ export default async function AuthLayout({
diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx
index 28fc7946..ce355481 100644
--- a/apps/frontend/src/app/layout.tsx
+++ b/apps/frontend/src/app/layout.tsx
@@ -42,6 +42,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
tolt={process.env.NEXT_PUBLIC_TOLT!}
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
+ neynarClientId={process.env.NEYNAR_CLIENT_ID!}
>
diff --git a/apps/frontend/src/components/auth/login.tsx b/apps/frontend/src/components/auth/login.tsx
index 13a458ec..a0279e91 100644
--- a/apps/frontend/src/components/auth/login.tsx
+++ b/apps/frontend/src/components/auth/login.tsx
@@ -12,6 +12,7 @@ import { GithubProvider } from '@gitroom/frontend/components/auth/providers/gith
import interClass from '@gitroom/react/helpers/inter.font';
import { GoogleProvider } from '@gitroom/frontend/components/auth/providers/google.provider';
import { useVariables } from '@gitroom/react/helpers/variable.context';
+import { FarcasterProvider } from '@gitroom/frontend/components/auth/providers/farcaster.provider';
type Inputs = {
email: string;
@@ -22,7 +23,7 @@ type Inputs = {
export function Login() {
const [loading, setLoading] = useState(false);
- const {isGeneral} = useVariables();
+ const { isGeneral, neynarClientId } = useVariables();
const resolver = useMemo(() => {
return classValidatorResolver(LoginUserDto);
}, []);
@@ -62,7 +63,14 @@ export function Login() {
- {!isGeneral ? : }
+ {!isGeneral ? (
+
+ ) : (
+
+
+ {!!neynarClientId && }
+
+ )}
-
diff --git a/apps/frontend/src/components/auth/nayner.auth.button.tsx b/apps/frontend/src/components/auth/nayner.auth.button.tsx
new file mode 100644
index 00000000..ac659f79
--- /dev/null
+++ b/apps/frontend/src/components/auth/nayner.auth.button.tsx
@@ -0,0 +1,100 @@
+'use client';
+
+import React, {
+ useCallback,
+ useEffect,
+ useState,
+ useRef,
+ FC,
+ ReactNode,
+} from 'react';
+import { useNeynarContext } from '@neynar/react';
+
+export const NeynarAuthButton: FC<{
+ children: ReactNode;
+ onLogin: (code: string) => void;
+}> = (props) => {
+ const { children, onLogin } = props;
+ const { client_id } = useNeynarContext();
+
+ const [showModal, setShowModal] = useState(false);
+
+ const authWindowRef = useRef
(null);
+ const neynarLoginUrl = `${
+ process.env.NEYNAR_LOGIN_URL ?? 'https://app.neynar.com/login'
+ }?client_id=${client_id}`;
+ const authOrigin = new URL(neynarLoginUrl).origin;
+
+ const modalRef = useRef(null);
+
+ const handleMessage = useCallback(
+ async (event: MessageEvent) => {
+ if (
+ event.origin === authOrigin &&
+ event.data &&
+ event.data.is_authenticated
+ ) {
+ authWindowRef.current?.close();
+ window.removeEventListener('message', handleMessage); // Remove listener here
+ const _user = {
+ signer_uuid: event.data.signer_uuid,
+ ...event.data.user,
+ };
+
+ onLogin(Buffer.from(JSON.stringify(_user)).toString('base64'));
+ }
+ },
+ [client_id, onLogin]
+ );
+
+ const handleSignIn = useCallback(() => {
+ const width = 600,
+ height = 700;
+ const left = window.screen.width / 2 - width / 2;
+ const top = window.screen.height / 2 - height / 2;
+ const windowFeatures = `width=${width},height=${height},top=${top},left=${left}`;
+
+ authWindowRef.current = window.open(
+ neynarLoginUrl,
+ '_blank',
+ windowFeatures
+ );
+
+ if (!authWindowRef.current) {
+ console.error(
+ 'Failed to open the authentication window. Please check your pop-up blocker settings.'
+ );
+ return;
+ }
+
+ window.addEventListener('message', handleMessage, false);
+ }, [client_id, handleMessage]);
+
+ const closeModal = () => setShowModal(false);
+
+ useEffect(() => {
+ return () => {
+ window.removeEventListener('message', handleMessage); // Cleanup function to remove listener
+ };
+ }, [handleMessage]);
+
+ const handleOutsideClick = useCallback((event: any) => {
+ if (modalRef.current && !modalRef.current.contains(event.target)) {
+ closeModal();
+ }
+ }, []);
+
+ useEffect(() => {
+ if (showModal) {
+ document.addEventListener('mousedown', handleOutsideClick);
+ } else {
+ document.removeEventListener('mousedown', handleOutsideClick);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleOutsideClick);
+ };
+ }, [showModal, handleOutsideClick]);
+
+ return {children}
;
+};
diff --git a/apps/frontend/src/components/auth/providers/farcaster.provider.tsx b/apps/frontend/src/components/auth/providers/farcaster.provider.tsx
new file mode 100644
index 00000000..fbba5466
--- /dev/null
+++ b/apps/frontend/src/components/auth/providers/farcaster.provider.tsx
@@ -0,0 +1,51 @@
+import { useCallback } from 'react';
+import interClass from '@gitroom/react/helpers/inter.font';
+import { useVariables } from '@gitroom/react/helpers/variable.context';
+import { NeynarContextProvider, Theme, useNeynarContext } from '@neynar/react';
+import { NeynarAuthButton } from '@gitroom/frontend/components/auth/nayner.auth.button';
+import { useRouter } from 'next/navigation';
+
+export const FarcasterProvider = () => {
+ const { neynarClientId } = useVariables();
+
+ const gotoLogin = useCallback(async (code: string) => {
+ window.location.href = `/auth?provider=FARCASTER&code=${code}`;
+ }, []);
+
+ return (
+
+
+
+
+
Continue with Farcaster
+
+
+
+ );
+};
diff --git a/apps/frontend/src/components/auth/providers/google.provider.tsx b/apps/frontend/src/components/auth/providers/google.provider.tsx
index 64424a3a..a3d9b337 100644
--- a/apps/frontend/src/components/auth/providers/google.provider.tsx
+++ b/apps/frontend/src/components/auth/providers/google.provider.tsx
@@ -39,7 +39,7 @@ export const GoogleProvider = () => {
/>
-
Sign in with Google
+
Continue with Google
);
};
diff --git a/apps/frontend/src/components/auth/register.tsx b/apps/frontend/src/components/auth/register.tsx
index 1db5175b..ec23af0a 100644
--- a/apps/frontend/src/components/auth/register.tsx
+++ b/apps/frontend/src/components/auth/register.tsx
@@ -18,6 +18,7 @@ import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { useTrack } from '@gitroom/react/helpers/use.track';
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
+import { FarcasterProvider } from '@gitroom/frontend/components/auth/providers/farcaster.provider';
type Inputs = {
email: string;
@@ -85,7 +86,7 @@ export function RegisterAfter({
token: string;
provider: string;
}) {
- const { isGeneral } = useVariables();
+ const { isGeneral, neynarClientId } = useVariables();
const [loading, setLoading] = useState(false);
const router = useRouter();
const fireEvents = useFireEvents();
@@ -153,7 +154,14 @@ export function RegisterAfter({
{!isAfterProvider &&
- (!isGeneral ? : )}
+ (!isGeneral ? (
+
+ ) : (
+
+
+ {!!neynarClientId && }
+
+ ))}
{!isAfterProvider && (
diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
index 796645ad..ef4dbf0b 100644
--- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma
+++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
@@ -536,6 +536,7 @@ enum Provider {
LOCAL
GITHUB
GOOGLE
+ FARCASTER
}
enum Role {
diff --git a/libraries/nestjs-libraries/src/services/email.service.ts b/libraries/nestjs-libraries/src/services/email.service.ts
index 135dceb7..e1760766 100644
--- a/libraries/nestjs-libraries/src/services/email.service.ts
+++ b/libraries/nestjs-libraries/src/services/email.service.ts
@@ -33,6 +33,10 @@ export class EmailService {
}
async sendEmail(to: string, subject: string, html: string, replyTo?: string) {
+ if (to.indexOf('@') === -1) {
+ return ;
+ }
+
if (!process.env.EMAIL_FROM_ADDRESS || !process.env.EMAIL_FROM_NAME) {
console.log(
'Email sender information not found in environment variables'
diff --git a/libraries/nestjs-libraries/src/services/newsletter.service.ts b/libraries/nestjs-libraries/src/services/newsletter.service.ts
index f3d5cea9..db2c7939 100644
--- a/libraries/nestjs-libraries/src/services/newsletter.service.ts
+++ b/libraries/nestjs-libraries/src/services/newsletter.service.ts
@@ -1,6 +1,11 @@
export class NewsletterService {
static async register(email: string) {
- if (!process.env.BEEHIIVE_API_KEY || !process.env.BEEHIIVE_PUBLICATION_ID || process.env.NODE_ENV === 'development') {
+ if (
+ !process.env.BEEHIIVE_API_KEY ||
+ !process.env.BEEHIIVE_PUBLICATION_ID ||
+ process.env.NODE_ENV === 'development' ||
+ email.indexOf('@') === -1
+ ) {
return;
}
const body = {
diff --git a/libraries/react-shared-libraries/src/helpers/variable.context.tsx b/libraries/react-shared-libraries/src/helpers/variable.context.tsx
index 66c91f47..95decf76 100644
--- a/libraries/react-shared-libraries/src/helpers/variable.context.tsx
+++ b/libraries/react-shared-libraries/src/helpers/variable.context.tsx
@@ -12,6 +12,7 @@ interface VariableContextInterface {
discordUrl: string;
uploadDirectory: string;
facebookPixel: string;
+ neynarClientId: string;
tolt: string;
}
const VariableContext = createContext({
@@ -24,6 +25,7 @@ const VariableContext = createContext({
discordUrl: '',
uploadDirectory: '',
facebookPixel: '',
+ neynarClientId: '',
tolt: '',
} as VariableContextInterface);