feat: login with a wallet

This commit is contained in:
Nevo David 2025-02-09 00:54:51 +07:00
parent 43910d8370
commit cc7a07f162
14 changed files with 7199 additions and 1338 deletions

View File

@ -5,6 +5,7 @@ import {
Ip,
Param,
Post,
Query,
Req,
Res,
} from '@nestjs/common';
@ -194,8 +195,8 @@ export class AuthController {
}
@Get('/oauth/:provider')
async oauthLink(@Param('provider') provider: string) {
return this._authService.oauthLink(provider);
async oauthLink(@Param('provider') provider: string, @Query() query: any) {
return this._authService.oauthLink(provider, query);
}
@Post('/activate')

View File

@ -215,11 +215,11 @@ export class AuthService {
return false;
}
oauthLink(provider: string) {
oauthLink(provider: string, query?: any) {
const providerInstance = ProvidersFactory.loadProvider(
provider as Provider
);
return providerInstance.generateLink();
return providerInstance.generateLink(query);
}
async checkExists(provider: string, code: string) {

View File

@ -1,5 +1,5 @@
export interface ProvidersInterface {
generateLink(): string;
generateLink(query?: any): Promise<string> | string;
getToken(code: string): Promise<string>;
getUser(providerToken: string): Promise<{email: string, id: string}> | false;
}

View File

@ -3,6 +3,7 @@ import { GithubProvider } from '@gitroom/backend/services/auth/providers/github.
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';
import { WalletProvider } from '@gitroom/backend/services/auth/providers/wallet.provider';
export class ProvidersFactory {
static loadProvider(provider: Provider): ProvidersInterface {
@ -13,6 +14,8 @@ export class ProvidersFactory {
return new GoogleProvider();
case Provider.FARCASTER:
return new FarcasterProvider();
case Provider.WALLET:
return new WalletProvider();
}
}
}

View File

@ -0,0 +1,90 @@
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
import { randomBytes } from 'crypto';
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
import bs58 from 'bs58';
import nacl from 'tweetnacl';
function hexToUint8Array(hex) {
// Remove any potential "0x" prefix
if (hex.startsWith('0x')) {
hex = hex.slice(2);
}
// Ensure the hex string has an even length
if (hex.length % 2 !== 0) {
throw new Error('Invalid hex string. It must have an even length.');
}
const byteLength = hex.length / 2;
const uint8Array = new Uint8Array(byteLength);
for (let i = 0; i < byteLength; i++) {
// Get two characters from the hex string
const byteHex = hex.substr(i * 2, 2);
// Parse the two characters as a hexadecimal number
uint8Array[i] = parseInt(byteHex, 16);
}
return uint8Array;
}
export class WalletProvider implements ProvidersInterface {
async generateLink(params: { publicKey: string }) {
if (!params.publicKey) {
return;
}
const challenge = randomBytes(32).toString('hex');
await ioRedis.set(`wallet:${params.publicKey}`, challenge, 'EX', 60);
return challenge;
}
async getToken(code: string) {
const { publicKey, challenge, signature } = JSON.parse(
Buffer.from(code, 'base64').toString()
);
if (!publicKey || !challenge || !signature) {
return '';
}
const redisGet = await ioRedis.get(`wallet:${publicKey}`);
if (redisGet !== challenge) {
return '';
}
const publicKeyUint8 = bs58.decode(publicKey);
const messageUint8 = new TextEncoder().encode(challenge);
const signatureUint8 = hexToUint8Array(signature);
const isValid = nacl.sign.detached.verify(
messageUint8,
signatureUint8,
publicKeyUint8
);
if (!isValid) {
return '';
}
return code;
}
async getUser(providerToken: string) {
if ((await this.getToken(providerToken)) === '') {
return {
id: '',
email: '',
};
}
const { publicKey } = JSON.parse(
Buffer.from(providerToken, 'base64').toString()
);
return {
id: String(`wallet_${publicKey}`),
email: String(`wallet_${publicKey}`),
};
}
}

View File

@ -60,7 +60,7 @@ export default async function AuthLayout({
</div>
</div>
</div>
<div className="p-[32px] w-full h-[614px] text-textColor">
<div className="p-[32px] w-full h-[660px] text-textColor">
{children}
</div>
<div className="flex flex-1 flex-col">

View File

@ -415,4 +415,8 @@ div div .set-font-family {
0% {
max-width: 0;
}
}
.tbaom7c {
display: none;
}

View File

@ -13,6 +13,7 @@ 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';
import WalletProvider from '@gitroom/frontend/components/auth/providers/wallet.provider';
type Inputs = {
email: string;
@ -23,7 +24,7 @@ type Inputs = {
export function Login() {
const [loading, setLoading] = useState(false);
const { isGeneral, neynarClientId } = useVariables();
const { isGeneral, neynarClientId, billingEnabled } = useVariables();
const resolver = useMemo(() => {
return classValidatorResolver(LoginUserDto);
}, []);
@ -69,6 +70,7 @@ export function Login() {
<div className="gap-[5px] flex flex-col">
<GoogleProvider />
{!!neynarClientId && <FarcasterProvider />}
{billingEnabled && <WalletProvider />}
</div>
)}
<div className="h-[20px] mb-[24px] mt-[24px] relative">

View File

@ -0,0 +1,23 @@
import { FC } from 'react';
import interClass from '@gitroom/react/helpers/inter.font';
export const WalletUiProvider: FC = () => {
return (
<div
className={`cursor-pointer bg-[#0b2181] h-[44px] rounded-[4px] flex justify-center items-center text-white ${interClass} gap-[7px]`}
>
<svg
width="18"
viewBox="0 0 25 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23 4H3C2.73478 4 2.48043 3.89464 2.29289 3.70711C2.10536 3.51957 2 3.26522 2 3C2 2.73478 2.10536 2.48043 2.29289 2.29289C2.48043 2.10536 2.73478 2 3 2H20C20.2652 2 20.5196 1.89464 20.7071 1.70711C20.8946 1.51957 21 1.26522 21 1C21 0.734784 20.8946 0.48043 20.7071 0.292893C20.5196 0.105357 20.2652 0 20 0H3C2.20435 0 1.44129 0.316071 0.87868 0.87868C0.316071 1.44129 0 2.20435 0 3V19C0 19.7956 0.316071 20.5587 0.87868 21.1213C1.44129 21.6839 2.20435 22 3 22H23C23.5304 22 24.0391 21.7893 24.4142 21.4142C24.7893 21.0391 25 20.5304 25 20V6C25 5.46957 24.7893 4.96086 24.4142 4.58579C24.0391 4.21071 23.5304 4 23 4ZM18.5 14C18.2033 14 17.9133 13.912 17.6666 13.7472C17.42 13.5824 17.2277 13.3481 17.1142 13.074C17.0007 12.7999 16.9709 12.4983 17.0288 12.2074C17.0867 11.9164 17.2296 11.6491 17.4393 11.4393C17.6491 11.2296 17.9164 11.0867 18.2074 11.0288C18.4983 10.9709 18.7999 11.0007 19.074 11.1142C19.3481 11.2277 19.5824 11.42 19.7472 11.6666C19.912 11.9133 20 12.2033 20 12.5C20 12.8978 19.842 13.2794 19.5607 13.5607C19.2794 13.842 18.8978 14 18.5 14Z"
fill="#fff"
/>
</svg>
Continue with your Wallet
</div>
);
};

View File

@ -0,0 +1,203 @@
'use client';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import {
ConnectionProvider,
useWallet,
WalletProvider as WalletProviderWrapper,
} from '@solana/wallet-adapter-react';
import { useWalletMultiButton } from '@solana/wallet-adapter-base-ui';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import {
TorusWalletAdapter,
BitgetWalletAdapter,
CloverWalletAdapter,
Coin98WalletAdapter,
FractalWalletAdapter,
HyperPayWalletAdapter,
KeystoneWalletAdapter,
KrystalWalletAdapter,
LedgerWalletAdapter,
MathWalletAdapter,
NightlyWalletAdapter,
NufiWalletAdapter,
OntoWalletAdapter,
ParticleAdapter,
PhantomWalletAdapter,
SafePalWalletAdapter,
SaifuWalletAdapter,
SalmonWalletAdapter,
SolflareWalletAdapter,
TokenaryWalletAdapter,
TrezorWalletAdapter,
TrustWalletAdapter,
XDEFIWalletAdapter,
TokenPocketWalletAdapter,
} from '@solana/wallet-adapter-wallets';
import {
WalletModalProvider,
useWalletModal,
} from '@solana/wallet-adapter-react-ui';
import { clusterApiUrl } from '@solana/web3.js';
// Default styles that can be overridden by your app
import '@solana/wallet-adapter-react-ui/styles.css';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { WalletUiProvider } from '@gitroom/frontend/components/auth/providers/placeholder/wallet.ui.provider';
const WalletProvider = () => {
const gotoLogin = useCallback(async (code: string) => {
window.location.href = `/auth?provider=FARCASTER&code=${code}`;
}, []);
return <ButtonCaster login={gotoLogin} />;
};
export const ButtonCaster: FC<{ login: (code: string) => void }> = (props) => {
const network = WalletAdapterNetwork.Mainnet;
// You can also provide a custom RPC endpoint.
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [
new TokenPocketWalletAdapter(),
new TorusWalletAdapter(),
new BitgetWalletAdapter(),
new CloverWalletAdapter(),
new Coin98WalletAdapter(),
new FractalWalletAdapter(),
new HyperPayWalletAdapter(),
new KeystoneWalletAdapter(),
new KrystalWalletAdapter(),
new LedgerWalletAdapter(),
new MathWalletAdapter(),
new NightlyWalletAdapter(),
new NufiWalletAdapter(),
new OntoWalletAdapter(),
new ParticleAdapter(),
new PhantomWalletAdapter(),
new SafePalWalletAdapter(),
new SaifuWalletAdapter(),
new SalmonWalletAdapter(),
new SolflareWalletAdapter(),
new TokenaryWalletAdapter(),
new TrezorWalletAdapter(),
new TrustWalletAdapter(),
new XDEFIWalletAdapter(),
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[network]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProviderWrapper wallets={wallets} autoConnect={false}>
<WalletModalProvider>
<DisabledAutoConnect />
</WalletModalProvider>
</WalletProviderWrapper>
</ConnectionProvider>
);
};
const DisabledAutoConnect = () => {
const [connect, setConnect] = useState(false);
const wallet = useWallet();
const toConnect = useCallback(async () => {
try {
wallet.select(null);
} catch (err) {
/** empty */
}
try {
await wallet.disconnect();
} catch (err) {
/** empty */
}
setConnect(true);
}, []);
useEffect(() => {
toConnect();
}, []);
if (connect) {
return <InnerWallet />;
}
return <WalletUiProvider />;
};
const InnerWallet = () => {
const walletModal = useWalletModal();
const wallet = useWallet();
const fetch = useFetch();
const { buttonState } = useWalletMultiButton({
onSelectWallet: () => {
return;
},
});
const connect = useCallback(async () => {
if (buttonState !== 'connected') {
return;
}
try {
const challenge = await (
await fetch(
`/auth/oauth/WALLET?publicKey=${wallet?.publicKey?.toString()}`
)
).text();
const encoded = new TextEncoder().encode(challenge);
const signed = await wallet?.signMessage?.(encoded)!;
const info = Buffer.from(
JSON.stringify({
// @ts-ignore
signature: Buffer.from(signed).toString('hex'),
challenge,
publicKey: wallet?.publicKey?.toString(),
})
).toString('base64');
window.location.href = `/auth?provider=WALLET&code=${info}`;
} catch (err) {
walletModal.setVisible(false);
wallet.select(null);
wallet.disconnect().catch(() => {
/** empty */
});
}
}, [wallet, buttonState]);
useEffect(() => {
if (buttonState === 'has-wallet') {
wallet
.connect()
.then(() => {
/** empty */
})
.catch(() => {
wallet.select(null);
wallet.disconnect();
});
}
if (buttonState === 'connected') {
connect();
}
}, [buttonState]);
return (
<div onClick={() => walletModal.setVisible(true)}>
<WalletUiProvider />
</div>
);
};
export default WalletProvider;

View File

@ -19,7 +19,12 @@ 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';
import dynamic from 'next/dynamic';
import { WalletUiProvider } from '@gitroom/frontend/components/auth/providers/placeholder/wallet.ui.provider';
const WalletProvider = dynamic(
() => import('@gitroom/frontend/components/auth/providers/wallet.provider'),
{ ssr: false, loading: () => <WalletUiProvider /> }
);
type Inputs = {
email: string;
password: string;
@ -86,7 +91,7 @@ export function RegisterAfter({
token: string;
provider: string;
}) {
const { isGeneral, neynarClientId } = useVariables();
const { isGeneral, neynarClientId, billingEnabled } = useVariables();
const [loading, setLoading] = useState(false);
const router = useRouter();
const fireEvents = useFireEvents();
@ -160,6 +165,7 @@ export function RegisterAfter({
<div className="gap-[5px] flex flex-col">
<GoogleProvider />
{!!neynarClientId && <FarcasterProvider />}
{billingEnabled && <WalletProvider />}
</div>
))}
{!isAfterProvider && (

View File

@ -566,6 +566,7 @@ enum Provider {
GITHUB
GOOGLE
FARCASTER
WALLET
}
enum Role {

8181
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -71,6 +71,9 @@
"@nx/webpack": "19.7.2",
"@nx/workspace": "19.7.2",
"@prisma/client": "^5.8.1",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/wallet-adapter-react-ui": "^0.9.35",
"@solana/wallet-adapter-wallets": "^0.19.32",
"@swc/helpers": "0.5.13",
"@sweetalert2/theme-dark": "^5.0.16",
"@types/bcrypt": "^5.0.2",
@ -105,6 +108,7 @@
"array-move": "^4.0.0",
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"bs58": "^6.0.0",
"bufferutil": "^4.0.8",
"bullmq": "^5.12.12",
"canvas": "^2.11.2",
@ -166,6 +170,7 @@
"tailwindcss": "3.4.3",
"tldts": "^6.1.47",
"tslib": "^2.3.0",
"tweetnacl": "^1.0.3",
"twitter-api-v2": "^1.16.0",
"twitter-text": "^3.1.0",
"use-debounce": "^10.0.0",