Skip to content
Snippets Groups Projects
[...nextauth].ts 7.37 KiB
Newer Older
Niklas P's avatar
Niklas P committed
import NextAuth, { NextAuthOptions, User } from 'next-auth';
Yuri's avatar
Yuri committed
import CredentialsProvider from 'next-auth/providers/credentials';
Niklas P's avatar
Niklas P committed
import { signatureVerify } from '@polkadot/util-crypto';
import { encodeAddress, Keyring } from '@polkadot/keyring';
Yuri's avatar
Yuri committed
import { ApiPromise, WsProvider } from '@polkadot/api';
import { BN } from '@polkadot/util';
ArmchairAncap's avatar
ArmchairAncap committed
import { AssetBalance } from '@polkadot/types/interfaces';
import { Int } from '@polkadot/types';
Niklas P's avatar
Niklas P committed

declare module 'next-auth' {
  interface Session {
Yuri's avatar
Yuri committed
    address: string | undefined;
    ksmAddress: string;
    freeBalance: BN;
ArmchairAncap's avatar
ArmchairAncap committed
    name: string;
    assetBalance?: string;
Niklas P's avatar
Niklas P committed
  }

  interface User {
Yuri's avatar
Yuri committed
    id: string;
    ksmAddress: string;
    freeBalance: BN;
ArmchairAncap's avatar
ArmchairAncap committed
    assetBalance?: string;
Niklas P's avatar
Niklas P committed
  }

  interface credentials {
Yuri's avatar
Yuri committed
    address: string;
    message: string;
    signature: string;
    csrfToken: string;
Niklas P's avatar
Niklas P committed
  }
}

type TokenLevels = {
  free?: number;
  standard?: number;
  premium?: number;
  id?: number;
};

export const tokenAssetConfig = {
  // load JSON file with token asset configuration from TOKEN_ASSET_CFG variable: {"JUNK":{"standard": 1,"premium":2},"GOLD":{"free": 0, "standard": 1, "premium":2}}
  TAConfig: new Map<string, TokenLevels>(Object.entries(JSON.parse(process.env.NEXT_PUBLIC_TOKEN_ASSET_CFG || '{}')))
};


Niklas P's avatar
Niklas P committed
export const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        address: {
          label: 'Address',
          type: 'text',
          placeholder: '0x0',
        },
        message: {
Yuri's avatar
Yuri committed
          label: 'Message',
          type: 'text',
          placeholder: '0x0',
Niklas P's avatar
Niklas P committed
        },
        signature: {
Yuri's avatar
Yuri committed
          label: 'Signature',
          type: 'text',
          placeholder: '0x0',
Niklas P's avatar
Niklas P committed
        },
        csrfToken: {
Yuri's avatar
Yuri committed
          label: 'CSRF Token',
          type: 'text',
          placeholder: '0x0',
Niklas P's avatar
Niklas P committed
        },
        name: {
Yuri's avatar
Yuri committed
          label: 'Name',
          type: 'text',
          placeholder: 'name',
        },
Niklas P's avatar
Niklas P committed
      },
Niklas P's avatar
Niklas P committed
      async authorize(credentials): Promise<User | null> {
Yuri's avatar
Yuri committed
        if (credentials === undefined) {
          return null;
        }
Niklas P's avatar
Niklas P committed
        try {
Yuri's avatar
Yuri committed
          const message = JSON.parse(credentials.message);
          // AA: specify xx Network
          const keyring = new Keyring({ type: 'sr25519', ss58Format: 55 });
Niklas P's avatar
Niklas P committed

Niklas P's avatar
Niklas P committed
          //verify the message is from the same uri
Yuri's avatar
Yuri committed
          if (message.uri !== process.env.NEXTAUTH_URL) {
            return Promise.reject(new Error('🚫 You shall not pass!'));
Niklas P's avatar
Niklas P committed
          }
Niklas P's avatar
Niklas P committed

Niklas P's avatar
Niklas P committed
          // verify the message was not compromised
Yuri's avatar
Yuri committed
          if (message.nonce !== credentials.csrfToken) {
            return Promise.reject(new Error('🚫 You shall not pass!'));
Niklas P's avatar
Niklas P committed
          }

          // verify signature of the message
Niklas P's avatar
Niklas P committed
          // highlight-start
Yuri's avatar
Yuri committed
          const { isValid } = signatureVerify(
            credentials.message,
            credentials.signature,
            credentials.address,
          );
Niklas P's avatar
Niklas P committed
          // highlight-end
Niklas P's avatar
Niklas P committed

Yuri's avatar
Yuri committed
          if (!isValid) {
            return Promise.reject(new Error('🚫 Invalid Signature'));
Niklas P's avatar
Niklas P committed
          }

          // verify the account has the defined token
          const wsProvider = new WsProvider(process.env.RPC_ENDPOINT ?? 'https://xxnetwork-rpc.dwellir.com');
Niklas P's avatar
Niklas P committed
          const api = await ApiPromise.create({ provider: wsProvider });
Niklas P's avatar
Niklas P committed
          await api.isReady;
Niklas P's avatar
Niklas P committed

          if (credentials?.address) {
            // AA: encode wallet address for ss58Format 55
            const ksmAddress = encodeAddress(credentials.address, 55);
            console.log("ksmAddress: ", ksmAddress);
            const accountInfo = (await api.query.system.account(ksmAddress)) as any;
            const balance = accountInfo.data.free.add(accountInfo.data.reserved);
            // AA: write ksmAddress for xx Network to console            
            console.log('Wallet address on xx Network: ', ksmAddress);
            console.log('Wallet balance on xx Network: ', balance.toString());

            // AA: asset balance check 
ArmchairAncap's avatar
ArmchairAncap committed
            const assetId = 5; // AA: Replace with your asset ID
            const accountAssetInfo = await api.query.assets.account(assetId, ksmAddress);
            // AA: initialize assetBalance as 0
ArmchairAncap's avatar
ArmchairAncap committed
            let assetBalance = 0;
            // AA: get Token-Asset Config 
            console.log('Token-asset config in [...nextauth].ts: ', tokenAssetConfig.TAConfig);
            // AA: find nested 'id' in tokenAssetConfig.TAConfig.entries() and compare with assetID value
            for (const [token, levels] of tokenAssetConfig.TAConfig.entries()) {
              for (const [level, value] of Object.entries(levels)) {
                if (value === assetId) {
                  console.log('Token-asset config entry: ', token, level, value);
                  console.log('Token-asset config entry found: ', token, level, value);
                }
              }
            }

            if (accountAssetInfo.isEmpty) {
              console.log(
                `No balance found for asset ${assetId} and address ${ksmAddress}`
              );
            } else {
ArmchairAncap's avatar
ArmchairAncap committed
                assetBalance = (accountAssetInfo.toHuman() as { balance: string }).balance ? parseInt((accountAssetInfo.toHuman() as { balance: string }).balance) : 0;
                console.log(
                `Account balance for asset ${assetId} and address ${ksmAddress}:`,
                assetBalance
                );
ArmchairAncap's avatar
ArmchairAncap committed
                console.log('Asset balance: ', assetBalance);
                if (assetBalance < 2) {
                console.warn(`Warning: Account balance for asset ${assetId} is less than 2.`);
                }
            };
ArmchairAncap's avatar
ArmchairAncap committed
            // end asset balance check
            
            console.log('Asset balance going into returned object: ', assetBalance.toString());
            
ArmchairAncap's avatar
ArmchairAncap committed
            if (accountInfo.data.free.gt(new BN(1_000_000_000)) || assetBalance >= 1) {
              // if the user has a free balance > 1 XX or JUNK !> 0, we let them in
              return {
                id: credentials.address,
                name: credentials.name,
                freeBalance: accountInfo.data.free,
                ksmAddress,
ArmchairAncap's avatar
ArmchairAncap committed
                assetBalance: assetBalance.toString()
              };
            } else {
              return Promise.reject(new Error('🚫 The gate is closed for you'));
            }
            // highlight-end

          return Promise.reject(new Error('🚫 API Error'));
Niklas P's avatar
Niklas P committed
        } catch (e) {
Yuri's avatar
Yuri committed
          return null;
Niklas P's avatar
Niklas P committed
        }
      },
    }),
  ],
  session: {
    strategy: 'jwt',
Niklas P's avatar
Niklas P committed
    // maxAge: 3, // uncomment to test session expiration in seconds
Niklas P's avatar
Niklas P committed
  },
  jwt: {
Niklas P's avatar
Niklas P committed
    secret: process.env.NEXTAUTH_SECRET,
Niklas P's avatar
Niklas P committed
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
Yuri's avatar
Yuri committed
        token.freeBalance = user.freeBalance;
ArmchairAncap's avatar
ArmchairAncap committed
        token.assetBalance = user.assetBalance;
Niklas P's avatar
Niklas P committed
      }
Yuri's avatar
Yuri committed
      return token;
Niklas P's avatar
Niklas P committed
    },
    async session(sessionData) {
Yuri's avatar
Yuri committed
      const { session, token } = sessionData;
Niklas P's avatar
Niklas P committed

Yuri's avatar
Yuri committed
      session.address = token.sub;
ArmchairAncap's avatar
ArmchairAncap committed
      session.assetBalance = token.assetBalance as string | undefined;
Yuri's avatar
Yuri committed
      if (session.address) {
        session.ksmAddress = encodeAddress(session.address, 55);
Niklas P's avatar
Niklas P committed
      }

      // as we already queried it, we can add whatever token to the session,
      // so pages can use it without an extra query
Yuri's avatar
Yuri committed
      session.freeBalance = token.freeBalance as BN;

      return session;
Niklas P's avatar
Niklas P committed
    },
  },
Niklas P's avatar
Niklas P committed
  secret: process.env.NEXTAUTH_SECRET,
Niklas P's avatar
Niklas P committed
  pages: {
    signIn: '/',
    signOut: '/',
    error: '/',
    newUser: '/',
  },
Yuri's avatar
Yuri committed
};
Niklas P's avatar
Niklas P committed

Yuri's avatar
Yuri committed
export default NextAuth(authOptions);