feat: login with a wallet
This commit is contained in:
parent
43910d8370
commit
cc7a07f162
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -415,4 +415,8 @@ div div .set-font-family {
|
|||
0% {
|
||||
max-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tbaom7c {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -566,6 +566,7 @@ enum Provider {
|
|||
GITHUB
|
||||
GOOGLE
|
||||
FARCASTER
|
||||
WALLET
|
||||
}
|
||||
|
||||
enum Role {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue