CAFECA-IO / iSunFA

Artificial Intelligence in Financial
https://isunfa.com
GNU General Public License v3.0
0 stars 0 forks source link

getUserById API calling #759

Closed arealclimber closed 4 months ago

arealclimber commented 4 months ago
arealclimber commented 4 months ago

taking 1 hr

remaining

arealclimber commented 4 months ago

跟 Jackey 討論後,改成不帶參數的調用 API 拿到 user, company data

arealclimber commented 4 months ago

taking 1.5 hr

src/pages/api/v1/session.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { getSession } from '@/lib/utils/get_session';
import prisma from '@/client';

export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse) {
  const session = await getSession(req, res);

  // eslint-disable-next-line no-console
  console.log('session in session API', session);
  if (session && session.userId) {
    const user = await prisma.user.findUnique({
      where: { id: session.userId },
    });
    res.status(200).json({ user });
    const company = 'test';
    res.status(200).json({ user, company });
  } else {
    res.status(200).json({ user: null });
  }
}

// import { NextApiRequest, NextApiResponse } from 'next';
// import { getSession } from '@/lib/utils/get_session';
// import prisma from '@/client';
// // import { IUser } from '@/interfaces/user';
// // import { ICompany } from '@/interfaces/company';

// // interface IUserWithCompany {
// //   user: IUser;
// //   company: ICompany;
// // }

// export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse) {
//   const session = await getSession(req, res);

//   // eslint-disable-next-line no-console
//   console.log('session in session API', session);
//   if (session && session.userId) {
//     const user = await prisma.user.findUnique({
//       where: { id: session.userId },
//     });

//     let company = null;
//     if (session.selectedCompanyId) {
//       company = await prisma.company.findUnique({
//         where: { id: session.selectedCompanyId },
//       });
//     }

//     if (company) {
//       res.status(200).json({ user: { ...user, company } });
//     } else {
//       res.status(200).json({ user });
//     }
//   } else {
//     res.status(200).json({ user: null });
//   }
// }

src/contexts/user_context.tsx

import { client } from '@passwordless-id/webauthn';
import useStateRef from 'react-usestateref';
import { createContext, useContext, useEffect, useMemo } from 'react';
import { useRouter } from 'next/router';
import { toast as toastify } from 'react-toastify';
import { ICredential } from '@/interfaces/webauthn';
import { createChallenge } from '@/lib/utils/authorization';
import { COOKIE_NAME, DUMMY_TIMESTAMP, FIDO2_USER_HANDLE } from '@/constants/config';
import { DEFAULT_DISPLAYED_USER_NAME } from '@/constants/display';
import { ISUNFA_ROUTE } from '@/constants/url';
import { AuthenticationEncoded } from '@passwordless-id/webauthn/dist/esm/types';
import { APIName } from '@/constants/api_connection';
import APIHandler from '@/lib/utils/api_handler';
import { ICompany } from '@/interfaces/company';
import { IUser } from '@/interfaces/user';

interface SignUpProps {
  username?: string;
}

interface UserContextType {
  credential: string | null;
  signUp: ({ username }: SignUpProps) => Promise<void>;
  signIn: () => Promise<void>;
  signOut: () => void;
  userAuth: IUser | null;
  username: string | null;
  signedIn: boolean;
  isSignInError: boolean;
  selectedCompany: ICompany | null;
  selectCompany: (company: ICompany | null) => void;
  isSelectCompany: boolean;
  errorCode: string | null;
  toggleIsSignInError: () => void;
}

export const UserContext = createContext<UserContextType>({
  credential: null,
  signUp: async () => {},
  signIn: async () => {},
  signOut: () => {},
  userAuth: null,
  username: null,
  signedIn: false,
  isSignInError: false,
  selectedCompany: null,
  selectCompany: () => {},
  isSelectCompany: false,
  errorCode: null,
  toggleIsSignInError: () => {},
});

export const UserProvider = ({ children }: { children: React.ReactNode }) => {
  const router = useRouter();

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [signedIn, setSignedIn, signedInRef] = useStateRef(false);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [credential, setCredential, credentialRef] = useStateRef<string | null>(null);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [userAuth, setUserAuth, userAuthRef] = useStateRef<IUser | null>(null);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [username, setUsername, usernameRef] = useStateRef<string | null>(null);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [selectedCompany, setSelectedCompany, selectedCompanyRef] = useStateRef<ICompany | null>(
    null
  );
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [isSelectCompany, setIsSelectCompany, isSelectCompanyRef] = useStateRef(false);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [isSignInError, setIsSignInError, isSignInErrorRef] = useStateRef(false);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [errorCode, setErrorCode, errorCodeRef] = useStateRef<string | null>(null);

  // const [errorCode, setErrorCode, errorCodeRef] = useStateRef<{
  //   httpCode: number;
  //   customCode: string;
  // } | null>(null);

  const { trigger: signOutAPI } = APIHandler<void>(
    APIName.SIGN_OUT,
    {
      body: { credential: credentialRef.current },
    },
    false,
    false
  );

  const {
    trigger: signInAPI,
    data: signInData,
    error: signInError,
    success: signInSuccess,
    isLoading: isSignInLoading,
    code: signInCode,
  } = APIHandler<IUser>(
    APIName.SIGN_IN,
    {
      header: { 'Content-Type': 'application/json' },
    },
    false,
    false
  );

  const {
    trigger: signUpAPI,
    data: signUpData,
    error: signUpError,
    success: signUpSuccess,
    isLoading: isSignUpLoading,
    code: signUpCode,
  } = APIHandler<IUser>(
    APIName.SIGN_UP,
    {
      header: { 'Content-Type': 'application/json' },
    },
    false,
    false
  );

  // TODO: 調整呼叫 getUser API (20240522 - Shirley)
  const {
    trigger: getUserByIdAPI,
    data: getUserByIdData,
    error: getUserByIdError,
    success: getUserByIdSuccess,
    isLoading: isGetUserByIdLoading,
  } = APIHandler<IUser>(
    APIName.USER_GET_BY_ID,
    {
      header: {
        userId: credentialRef.current || '',
      },
    },
    false,
    false
  );

  const readFIDO2Cookie = async () => {
    const cookie = document.cookie.split('; ').find((row: string) => row.startsWith('FIDO2='));

    const FIDO2 = cookie ? cookie.split('=')[1] : null;

    if (FIDO2) {
      const decoded = decodeURIComponent(FIDO2);
      if (
        !decoded ||
        decoded === undefined ||
        decoded === 'undefined' ||
        decoded === 'null' ||
        decoded === null
      ) {
        return null;
      }
      const credentialData = JSON.parse(decoded) as ICredential;
      return credentialData;
    }

    return null;
  };

  const writeFIDO2Cookie = async () => {
    const expiration = new Date();
    expiration.setHours(expiration.getHours() + 1);

    // TODO: read cookie first (20240520 - Shirley)
    // const credentialData = await readFIDO2Cookie();
    document.cookie = `FIDO2=${encodeURIComponent(JSON.stringify(credentialRef.current))}; expires=${expiration.toUTCString()}; path=/`;
  };

  const deleteCookie = (name: string) => {
    document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
  };

  const toggleIsSignInError = () => {
    setIsSignInError(!isSignInErrorRef.current);
  };

  const refreshUserFromSession = async () => {
    try {
      const response = await fetch('/api/v1/session');
      const data = (await response.json()) as { user: IUser };
      // eslint-disable-next-line no-console
      console.log('refreshUserFromSession', data, !data.user);

      if (!data.user) {
        return;
      }

      setUserAuth(data.user);
      setUsername(data.user.name);
      setCredential(data.user.credentialId);
      setSignedIn(true);
      setIsSignInError(false);
      setIsSelectCompany(false);
      setSelectedCompany(null);
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error('refreshUserFromSession error:', error);
    }

    // setUser(data.user);
    // eslint-disable-next-line

    // const session = getSession; // 假設 getSession 是異步並返回當前 session 對象
    // console.log('refreshUserFromSession', session);
    // // if (session && session.userId) {
    // //   // const rs =
    // //   // const { data: userData } = await APIHandler<IUser>({
    // //   //   method: 'GET',
    // //   //   url: `/api/user/${session.userId}`,
    // //   // });
    // //   // if (userData) {
    // //   //   setUser(userData);
    // //   // }
    // // } else {
    // //   // setUser(null);
    // // }
  };

  const signUp = async ({ username: usernameForSignUp }: SignUpProps) => {
    const name = usernameForSignUp || DEFAULT_DISPLAYED_USER_NAME;

    try {
      setIsSignInError(false);

      const newChallenge = await createChallenge(
        'FIDO2.TEST.reg-' + DUMMY_TIMESTAMP.toString() + '-hello'
      );

      const registration = await client.register(name, newChallenge, {
        authenticatorType: 'both',
        userVerification: 'required',
        timeout: 60000, // Info: 60 seconds (20240408 - Shirley)
        attestation: true,
        userHandle: FIDO2_USER_HANDLE, // Info: optional userId less than 64 bytes (20240403 - Shirley)
        debug: false,
        discoverable: 'required', // TODO: to fix/limit user to login with the same public-private key pair (20240410 - Shirley)
      });

      signUpAPI({ body: { registration } });
    } catch (error) {
      // Deprecated: dev (20240410 - Shirley)
      // eslint-disable-next-line no-console
      console.error('signUp error:', error);
    }
  };

  // TODO: refactor the signIn function (20240409 - Shirley)
  /* TODO: (20240410 - Shirley)
      拿登入聲明書 / 用戶條款 / challenge
      先檢查 cookie ,然後檢查是否有 credential 、驗證 credential 有沒有過期或亂寫,
      拿著 credential 跟 server 去拿 member 資料、付錢資料
  */
  const signIn = async () => {
    try {
      setIsSignInError(false);

      const newChallenge = await createChallenge(
        'FIDO2.TEST.reg-' + DUMMY_TIMESTAMP.toString() + '-hello'
      );

      const authentication: AuthenticationEncoded = await client.authenticate([], newChallenge, {
        authenticatorType: 'both',
        userVerification: 'required',
        timeout: 60000, // Info: 60 seconds (20240408 - Shirley)
        debug: false,
      });

      signInAPI({ body: { authentication, challenge: newChallenge } });

      const response = await fetch('/api/v1/session');
      const data = (await response.json()) as { user: IUser };
      // eslint-disable-next-line no-console
      console.log('signIn and get session', data, !data.user);
    } catch (error) {
      // Deprecated: dev (20240410 - Shirley)
      // eslint-disable-next-line no-console
      console.error('signIn error and try to call singUp function:', error);
      // signUp({ username: '' });

      if (!(error instanceof DOMException)) {
        setIsSignInError(true);

        throw new Error('signIn error thrown in userCtx');
      } else {
        throw new Error(`signIn error thrown in userCtx: ${error.message}`);
      }
    }
  };

  // TODO: 調整呼叫 getUser API (20240522 - Shirley)
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const getUserById = async ({ credentialId }: { credentialId: string }) => {
    try {
      getUserByIdAPI({ header: { userId: credentialId }, body: { credential: credentialId } });
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error('getUserById error:', error);
    }
  };

  // TODO: 在用戶一進到網站後就去驗證是否登入 (20240409 - Shirley)
  const setPrivateData = async () => {
    // TODO: 調整呼叫 getUser API (20240522 - Shirley)
    // getUserById({ credentialId: credentialRef.current || '' });
    refreshUserFromSession();

    // const credentialFromCookie = await readFIDO2Cookie();

    // if (credentialFromCookie !== null) {
    //   setCredential(credentialFromCookie.id);
    //   setSignedIn(true);
    // } else {
    //   setSignedIn(false);
    //   deleteCookie(COOKIE_NAME.FIDO2);
    // }
  };

  // ToDo: (20240513 - Julian) 選擇公司的功能
  const selectCompany = (company: ICompany | null) => {
    if (company) {
      setSelectedCompany(company);
    } else {
      setSelectedCompany(null);
    }
  };

  const clearState = () => {
    setUserAuth(null);
    setUsername(null);
    setCredential(null);
    setSignedIn(false);
    setIsSignInError(false);
    setSelectedCompany(null);
    setIsSelectCompany(false);

    toastify.dismiss(); // Info: (20240513 - Julian) 清除所有的 Toast
  };

  const init = async () => {
    await setPrivateData();
    const result = await Promise.resolve();
    return result;
  };

  const checkCookieAndSignOut = async () => {
    const cookie = await readFIDO2Cookie();

    if (!cookie) {
      clearState();
    } else {
      // TODO: send request with session cookie (20240520 - Shirley)
    }
  };

  const signOut = async () => {
    signOutAPI(); // Deprecated: 登出只需要在前端刪掉 cookie 就好 (20240517 - Shirley)
    clearState();
    router.push(ISUNFA_ROUTE.LOGIN);
    const cookieName = COOKIE_NAME.FIDO2;
    deleteCookie(cookieName);
  };

  useEffect(() => {
    (async () => {
      await init();
    })();
  }, []);

  useEffect(() => {
    checkCookieAndSignOut();
  }, [router.pathname]);

  useEffect(() => {
    if (isSignUpLoading) return;

    if (signUpSuccess) {
      if (signUpData) {
        setUsername(signUpData.name);
        setUserAuth(signUpData);
        setCredential(signUpData.credentialId);
        setSignedIn(true);
        setIsSignInError(false);
        writeFIDO2Cookie();
      }
    } else {
      setIsSignInError(true);
      // eslint-disable-next-line no-console
      console.log('signUpError:', signUpError);

      setErrorCode(signUpCode ?? '');
    }
  }, [signUpData, isSignUpLoading, signUpSuccess, signUpCode]);

  useEffect(() => {
    if (isSignInLoading) return;

    if (signInSuccess) {
      if (signInData) {
        setUsername(signInData.name);
        setUserAuth(signInData);
        setCredential(signInData.credentialId);
        setSignedIn(true);
        setIsSignInError(false);
        writeFIDO2Cookie();
      }
    } else {
      setIsSignInError(true);
      // eslint-disable-next-line no-console
      console.log('signInError:', signInError);
      setErrorCode(signInCode ?? '');
    }
  }, [signInData, isSignInLoading, signInSuccess, signInCode]);

  useEffect(() => {
    if (isGetUserByIdLoading) return;

    if (getUserByIdSuccess) {
      if (getUserByIdData) {
        setUsername(getUserByIdData.name);
        setUserAuth(getUserByIdData);
        setCredential(getUserByIdData.credentialId);
        setSignedIn(true);
        setIsSignInError(false);
        writeFIDO2Cookie();
      }
    } else {
      setIsSignInError(true);
      // eslint-disable-next-line no-console
      console.log('getUserByIdError:', getUserByIdError);
    }
  }, [getUserByIdData, isGetUserByIdLoading, getUserByIdSuccess]);

  useEffect(() => {
    if (selectedCompany) {
      setIsSelectCompany(true);
    } else {
      setIsSelectCompany(false);
    }
  }, [selectedCompany]);

  // Info: dependency array 的值改變,才會讓更新後的 value 傳到其他 components (20240522 - Shirley)
  const value = useMemo(
    () => ({
      credential: credentialRef.current,
      signUp,
      signIn,
      signOut,
      userAuth: userAuthRef.current,
      username: usernameRef.current,
      signedIn: signedInRef.current,
      isSignInError: isSignInErrorRef.current,
      selectedCompany: selectedCompanyRef.current,
      selectCompany,
      isSelectCompany: isSelectCompanyRef.current,
      errorCode: errorCodeRef.current,
      toggleIsSignInError,
    }),
    [
      credentialRef.current,
      selectedCompanyRef.current,
      isSelectCompanyRef.current,
      errorCodeRef.current,
      isSignInErrorRef.current,
    ]
  );
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};

export const useUserCtx = () => {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUserCtx must be used within a UserProvider');
  }
  return context;
};

src/pages/users/login.tsx

export const getServerSideProps: GetServerSideProps = async ({ locale, req, res }) => {
  const sessionReq = await getSession(req, res);
  const sessionData = sessionReq || {};

  console.log('sessionData in getServerSideProps in LoginPage', sessionData);

  if (sessionData.userId) {
    return {
      redirect: {
        destination: ISUNFA_ROUTE.SELECT_COMPANY,
        permanent: false,
      },
    };
  }

  // Clean up undefined values in sessionData
  if (sessionData.cookie) {
    if (sessionData.cookie.domain === undefined) {
      delete sessionData.cookie.domain;
    }
    if (sessionData.cookie.sameSite === undefined) {
      delete sessionData.cookie.sameSite;
    }
  }

  return {
    props: {
      ...(await serverSideTranslations(locale as string, ['common'])),
      sessionData,
    },
  };
};

src/pages/users/select-company.tsx

export const getServerSideProps: GetServerSideProps = async ({ locale, req, res }) => {
  const sessionReq = await getSession(req, res);
  const sessionData = sessionReq || {};

  console.log('sessionData in getServerSideProps in SelectCompanyPage', sessionData);
  console.log('sessionData.userId in getServerSideProps in SelectCompanyPage', sessionData.userId);

  if (sessionData.userId) {
    return {
      redirect: {
        destination: ISUNFA_ROUTE.SELECT_COMPANY,
        permanent: false,
      },
    };
  }

  // Clean up undefined values in sessionData
  if (sessionData.cookie) {
    if (sessionData.cookie.domain === undefined) {
      delete sessionData.cookie.domain;
    }
    if (sessionData.cookie.sameSite === undefined) {
      delete sessionData.cookie.sameSite;
    }
  }

  return {
    props: {
      ...(await serverSideTranslations(locale as string, ['common'])),
      sessionData,
    },
  };
};