Closed arealclimber closed 4 months ago
taking 1 hr
remaining
跟 Jackey 討論後,改成不帶參數的調用 API 拿到 user, company data
taking 1.5 hr
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 });
// }
// }
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;
};
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,
},
};
};
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,
},
};
};