From f0293c6d593a6fa5082b37c5e46c610fd50d687d Mon Sep 17 00:00:00 2001 From: TzuHanLiang Date: Mon, 12 Aug 2024 17:37:18 +0800 Subject: [PATCH 1/2] feat: add basic UI --- .env.example | 12 ++ package.json | 5 +- public/icons/apple_logo.svg | 4 + public/icons/google_logo.svg | 7 + public/images/login_bg.svg | 9 + .../add_journal_body/add_journal_body.tsx | 2 +- src/components/kyc/kyc_form.tsx | 6 +- src/components/kyc/kyc_leave_button.tsx | 2 +- .../login_confirm_modal.beta.tsx | 50 +++++ .../login_page_body/login_page_body.beta.tsx | 203 ++++++++++++++++++ src/constants/api_connection.ts | 16 ++ src/constants/terms.ts | 107 +++++++++ src/constants/url.ts | 1 + src/contexts/global_context.tsx | 64 +++++- src/interfaces/api_connection.ts | 2 + src/interfaces/page_query.ts | 13 ++ src/locales/cn/common.json | 8 +- src/locales/en/common.json | 8 +- src/locales/tw/common.json | 8 +- src/pages/api/auth/next_auth.ts | 79 +++++++ src/pages/users/login-beta.tsx | 66 ++++++ 21 files changed, 656 insertions(+), 16 deletions(-) create mode 100644 public/icons/apple_logo.svg create mode 100644 public/icons/google_logo.svg create mode 100644 public/images/login_bg.svg create mode 100644 src/components/login_confirm_modal/login_confirm_modal.beta.tsx create mode 100644 src/components/login_page_body/login_page_body.beta.tsx create mode 100644 src/constants/terms.ts create mode 100644 src/pages/api/auth/next_auth.ts create mode 100644 src/pages/users/login-beta.tsx diff --git a/.env.example b/.env.example index 6989bd57d..bf29afc8c 100644 --- a/.env.example +++ b/.env.example @@ -37,3 +37,15 @@ BASE_STORAGE_PATH = PAYMENT_TOKEN = PAYMENT_ID = PAYMENT_SERVICE = + +GOOGLE_CLIENT_ID = google-client-id +GOOGLE_CLIENT_SECRET = google-client-secret + +APPLE_CLIENT_ID = com.company.app +APPLE_CLIENT_SECRET = apple-client-secret +APPLE_KEY_ID = apple-key-id +APPLE_TEAM_ID = apple-team-id +APPLE_PRIVATE_KEY = apple-private-key + +NEXTAUTH_URL = https://isunfa.com/ +NEXTAUTH_SECRET = generated-random-secret \ No newline at end of file diff --git a/package.json b/package.json index 3bfdf2f1c..6a280438d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.0", + "version": "0.8.0+1", "private": false, "scripts": { "dev": "next dev", @@ -35,8 +35,10 @@ "formidable": "^3.5.1", "i18next": "^23.11.5", "jest-mock-extended": "^3.0.7", + "jsonwebtoken": "^9.0.2", "lodash-es": "^4.17.21", "next": "^14.2.5", + "next-auth": "^4.24.7", "next-i18next": "^15.2.0", "next-session": "^4.0.5", "nodemailer": "^6.9.8", @@ -60,6 +62,7 @@ "@testing-library/react": "^14.3.1", "@types/cookie": "^0.6.0", "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20", "@types/nodemailer": "^6.4.15", "@types/react": "^18", diff --git a/public/icons/apple_logo.svg b/public/icons/apple_logo.svg new file mode 100644 index 000000000..abc0dfd5b --- /dev/null +++ b/public/icons/apple_logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/google_logo.svg b/public/icons/google_logo.svg new file mode 100644 index 000000000..00bc05401 --- /dev/null +++ b/public/icons/google_logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/login_bg.svg b/public/images/login_bg.svg new file mode 100644 index 000000000..2da91feca --- /dev/null +++ b/public/images/login_bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/add_journal_body/add_journal_body.tsx b/src/components/add_journal_body/add_journal_body.tsx index 9959caed3..e5c5e3c23 100644 --- a/src/components/add_journal_body/add_journal_body.tsx +++ b/src/components/add_journal_body/add_journal_body.tsx @@ -42,7 +42,7 @@ const AddJournalBody = () => { content: t('JOURNAL.LEAVE_HINT_CONTENT'), // 'Are you sure you want to leave the form?', submitBtnStr: t('JOURNAL.LEAVE'), submitBtnFunction: () => backClickHandler(), - backBtnStr: t('JOURNAL.CANCEL'), + backBtnStr: t('COMMON.CANCEL'), messageType: MessageType.WARNING, }; diff --git a/src/components/kyc/kyc_form.tsx b/src/components/kyc/kyc_form.tsx index 9c5019b16..5f5b4d5ec 100644 --- a/src/components/kyc/kyc_form.tsx +++ b/src/components/kyc/kyc_form.tsx @@ -101,7 +101,7 @@ const KYCForm = () => { messageType: MessageType.SUCCESS, title: t('KYC.SUBMIT_SUCCESS'), content: t('KYC.SUBMIT_SUCCESS_MESSAGE'), - backBtnStr: t('KYC.CANCEL'), + backBtnStr: t('COMMON.CANCEL'), submitBtnStr: t('KYC.CONFIRM'), submitBtnFunction: () => { messageModalVisibilityHandler(); @@ -115,7 +115,7 @@ const KYCForm = () => { title: t('KYC.SUBMIT_FAILED'), content: t('KYC.CONTACT_SERVICE_TEAM'), subMsg: t('KYC.SUBMIT_FAILED_MESSAGE', code), - backBtnStr: t('KYC.CANCEL'), + backBtnStr: t('COMMON.CANCEL'), submitBtnStr: t('KYC.CONFIRM'), submitBtnFunction: () => { messageModalVisibilityHandler(); @@ -130,7 +130,7 @@ const KYCForm = () => { subMsg: t('KYC.INCOMPLETE_FORM_SUB_MESSAGE', { fields: missingFields.join(', ') }), content: t('KYC.CONTACT_SERVICE_TEAM'), submitBtnStr: t('KYC.CONFIRM'), - backBtnStr: t('KYC.CANCEL'), + backBtnStr: t('COMMON.CANCEL'), submitBtnFunction: () => { messageModalVisibilityHandler(); goCompanyInfo(); diff --git a/src/components/kyc/kyc_leave_button.tsx b/src/components/kyc/kyc_leave_button.tsx index 58e37c4f8..b92cfaaaf 100644 --- a/src/components/kyc/kyc_leave_button.tsx +++ b/src/components/kyc/kyc_leave_button.tsx @@ -21,7 +21,7 @@ const LeaveButton = () => { subMsg: `${t('KYC.ARE_YOU_SURE_YOU_WANT_TO_LEAVE_THIS_PAGE')} ?`, submitBtnStr: t('KYC.LEAVE_NOW'), submitBtnFunction: handleBack, - backBtnStr: t('KYC.CANCEL'), + backBtnStr: t('COMMON.CANCEL'), }); messageModalVisibilityHandler(); }; diff --git a/src/components/login_confirm_modal/login_confirm_modal.beta.tsx b/src/components/login_confirm_modal/login_confirm_modal.beta.tsx new file mode 100644 index 000000000..c5b8c6b68 --- /dev/null +++ b/src/components/login_confirm_modal/login_confirm_modal.beta.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ILoginConfirmProps { + isModalVisible: boolean; + modalData: { + title: string; + content: string; + buttonText: string; + }; + onAgree: () => void; + onCancel: () => void; +} + +const LoginConfirmModal: React.FC = ({ + isModalVisible, + modalData, + onAgree, + onCancel, +}) => { + const { t } = useTranslation('common'); + return ( + isModalVisible && ( +
+
+

{modalData.title}

+

{modalData.content}

+
+ + +
+
+
+ ) + ); +}; + +export default LoginConfirmModal; diff --git a/src/components/login_page_body/login_page_body.beta.tsx b/src/components/login_page_body/login_page_body.beta.tsx new file mode 100644 index 000000000..3b579f495 --- /dev/null +++ b/src/components/login_page_body/login_page_body.beta.tsx @@ -0,0 +1,203 @@ +import React from 'react'; +import { signIn, SignInResponse } from 'next-auth/react'; +import Image from 'next/image'; +import { useGlobalCtx } from '@/contexts/global_context'; +import APIHandler from '@/lib/utils/api_handler'; +import { IUser } from '@/interfaces/user'; +import { APIName } from '@/constants/api_connection'; +import { useRouter } from 'next/router'; +import { ISUNFA_ROUTE } from '@/constants/url'; + +enum AuthWith { + GOOGLE = 'google', + APPLE = 'apple', +} + +const LoginPageBody = () => { + const router = useRouter(); + const { + isAgreeWithInfomationConfirmModalVisible, + agreeWithInfomationConfirmModalVisibilityHandler, + TOSNPrivacyPolicyConfirmModalCallbackHandler, + } = useGlobalCtx(); + const { trigger: logInTrigger } = APIHandler<{ + user: IUser; + hasReadAgreement: boolean; + }>(APIName.LOGIN); + const { trigger: agreementTrigger } = APIHandler(APIName.AGREEMENT); + + const handleUserAgree = async (authWith: AuthWith) => { + try { + // 呼叫 API 紀錄用戶已同意條款 + const response = await agreementTrigger({ + body: { authWith, agreement: true }, + }); + + const { success, code } = response; + // eslint-disable-next-line no-console + console.log('紀錄用戶同意條款:', success, code); + router.push(ISUNFA_ROUTE.SELECT_COMPANY); + } catch (error) { + // eslint-disable-next-line no-console + console.error('紀錄用戶同意條款時發生錯誤:', error); + } + }; + + // 新增:呼叫後端 signInAPI + const callLogInAPI = async (authWith: AuthWith, oauthResponse: SignInResponse | undefined) => { + try { + const response = await logInTrigger({ + body: { authWith, oauthResponse }, + }); + + // 處理後端返回的 user 和 hasReadAgreement 資料 + const { success, data } = response; + if (success && data) { + const { user, hasReadAgreement } = data; + if (!hasReadAgreement && !isAgreeWithInfomationConfirmModalVisible) { + // 如果用戶尚未同意條款,顯示同意條款的模態框 + TOSNPrivacyPolicyConfirmModalCallbackHandler(() => handleUserAgree(authWith)); + agreeWithInfomationConfirmModalVisibilityHandler(); + } else { + // 用戶已經同意條款,直接進行登入後的邏輯處理。TODO: (20240812-Tzuhan) route to select-company page + // eslint-disable-next-line no-console + console.log('用戶已登入:', user); + router.push(ISUNFA_ROUTE.SELECT_COMPANY); + } + } else { + // eslint-disable-next-line no-console + console.error('登入 API 錯誤:', response); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('登入 API 錯誤:', error); + } + }; + + // 登入處理函數,呼叫 OAuth 登入並獲取響應後再呼叫 signInAPI + const loginHandler = async (authWith: AuthWith) => { + try { + // eslint-disable-next-line no-console + console.log('loginHandler:', authWith); + const oauthResponse = await signIn(authWith, { redirect: false }); + + if (oauthResponse?.error) { + // eslint-disable-next-line no-console + console.error('OAuth 登入失敗:', oauthResponse.error); + } else { + // 呼叫後端 signInAPI 處理登入 + await callLogInAPI(authWith, oauthResponse); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('登入處理函數錯誤:', error); + } + }; + + // 處理 Apple 登入 + const AuthWithApple = () => loginHandler(AuthWith.APPLE); + + // 處理 Google 登入 + const AuthWithGoogle = () => loginHandler(AuthWith.GOOGLE); + + return ( +
+ {/* 背景圖片 */} +
+ Background + {/* 白色透明遮罩 */} +
+
+
+

Log In

+
+
+
+ {/* 匿名頭像 */} + + + + + + + + + + + +
+ +
+ {/* 匿名頭像 */} + + + + + + + + + + + +
+
+
+
+ + +
+
+
+ ); +}; + +export default LoginPageBody; diff --git a/src/constants/api_connection.ts b/src/constants/api_connection.ts index 1fd74bdce..70ad0ac68 100644 --- a/src/constants/api_connection.ts +++ b/src/constants/api_connection.ts @@ -2,6 +2,8 @@ import { IAPIConfig, IAPIInput, IAPIName, IAPIOutput } from '@/interfaces/api_co const apiVersion = 'v1'; const apiPrefix = `/api/${apiVersion}`; +const apiVersionBeta = 'v2'; +const apiPrefixBeta = `/api/${apiVersionBeta}`; const initialInput: IAPIInput = { header: {}, body: {}, @@ -19,6 +21,8 @@ export enum HttpMethod { } export enum APIName { + LOGIN = 'LOGIN', + AGREEMENT = 'AGREEMENT', CREATE_CHALLENGE = 'CREATE_CHALLENGE', SIGN_UP = 'SIGN_UP', SIGN_IN = 'SIGN_IN', @@ -81,6 +85,8 @@ export enum APIName { } export enum APIPath { + LOGIN = `${apiPrefixBeta}/login`, + AGREEMENT = `${apiPrefixBeta}/agreement`, CREATE_CHALLENGE = `${apiPrefix}/challenge`, SIGN_UP = `${apiPrefix}/sign-up`, SIGN_IN = `${apiPrefix}/sign-in`, @@ -164,6 +170,16 @@ const createConfig = ({ }); export const APIConfig: Record = { + [APIName.LOGIN]: createConfig({ + name: APIName.LOGIN, + method: HttpMethod.POST, + path: APIPath.LOGIN, + }), + [APIName.AGREEMENT]: createConfig({ + name: APIName.AGREEMENT, + method: HttpMethod.POST, + path: APIPath.AGREEMENT, + }), [APIName.CREATE_CHALLENGE]: createConfig({ name: APIName.CREATE_CHALLENGE, method: HttpMethod.GET, diff --git a/src/constants/terms.ts b/src/constants/terms.ts new file mode 100644 index 000000000..806a3aa49 --- /dev/null +++ b/src/constants/terms.ts @@ -0,0 +1,107 @@ +export const term_1 = ` +Dear User, +Thank you for choosing to use our accounting software. During your use of this software, we may collect and process some of your personal information. This statement aims to inform you about the information we collect, how it is used, and how we protect your privacy. + +1. Types of Information Collected +During your use of the accounting software, we may collect the following information: +Personal Identification Information: Including but not limited to your name, email address, phone number, company name, etc. +Usage Data: Such as your operation records within the software, usage duration, access frequency, etc. +Technical Information: Such as IP address, device information, operating system, browser type, etc. + +2. Use of Information +The information we collect will primarily be used for the following purposes: +Providing and Maintaining Services: Ensuring the normal operation of the accounting software and improving the user experience. +Personalized Services: Providing customized features and recommendations based on your needs and preferences. +Customer Support: Addressing your questions, handling your requests and complaints. +Security Assurance: Detecting and preventing potential security threats, protecting your data security. +Data Analysis: Conducting statistical analysis to improve our products and services. + +3. Protection of Information +We commit to taking reasonable technical and organizational measures to protect your personal information from unauthorized access, disclosure, alteration, or destruction. These measures include but are not limited to: +Data Encryption: Encrypting sensitive data. +Access Control: Restricting data access rights to employees and third parties. +Regular Audits: Regularly reviewing our security measures and policies. + +4. Sharing of Information +We will not sell, rent, or trade your personal information to any third party without your consent. However, we may share your information in the following circumstances: +Legal Requirements: When required by law or requested by government authorities. +Service Providers: With third-party service providers who work with us, provided they only use your information to perform our instructions and comply with appropriate confidentiality and security measures. +Business Transfers: In the event of a company merger, acquisition, or asset sale, we may transfer your information. + +5. Your Rights +You have the right to access, correct, or delete your personal information at any time. If you wish to exercise these rights or have any questions, please contact us via: +Email: support@isunfa.com +Phone: +123-456-7890 +We will strive to respond to your request within a reasonable time frame. + +6. Changes to the Statement +We may update this statement from time to time to reflect changes in our information handling practices. When we make significant changes to this statement, we will notify you through in-software notifications or via email. + +Thank you for your trust and support in our accounting software! + +iSunFA August 6, 2024 +`; + +export const term_2 = ` +Dear User, +Welcome to our accounting software. To ensure you fully understand our services and how we protect your personal information, please carefully read the following Terms of Service and Privacy Policy. + +I. Terms of Service +Service Content + +Our accounting software includes features such as bookkeeping, report generation, invoice management, and financial analysis. +We reserve the right to update or modify the service content at any time and will notify you of such changes through appropriate means. + +Eligibility + +You must be at least 18 years old or meet the legal age requirements of your country, and have full legal capacity to use our services. +You agree to use our services in accordance with relevant laws and regulations and not engage in any illegal or improper activities. + +Account Management + +You need to provide accurate and complete registration information and are responsible for maintaining the security of your account. +If you discover any unauthorized account use or security vulnerabilities, please notify us immediately. + +Service Fees + +Some features or services may require payment. Specific fees and payment methods will be detailed on relevant pages or documents. +Payments are non-refundable once confirmed, unless otherwise required by law. + +Limitation of Liability + +We are not liable for any indirect, incidental, special, or consequential damages arising from the use of our services. +We are not responsible for any third-party services or links provided. + +Termination of Service + +We reserve the right to terminate or suspend the service at any time for any reason without liability. +You may cancel the use of our services at any time, but fees paid are non-refundable. + +II. Privacy Policy + +Information Collection + +We collect various types of information related to your use of the accounting software, including but not limited to personal identification information, Usage data, and technical information. For more details, please refer to the "Information Collection Statement." + +Use of Information + +The collected information will be used to provide, maintain, and improve our services, ensure security, and conduct data analysis. We will not use your information for any other purposes without your consent. +Information Protection + +We take reasonable technical and organizational measures to protect your information, including data encryption, access control, and regular security audits. For details, please refer to the "Information Collection Statement." + +Information Sharing + +We will not sell, rent, or trade your personal information to third parties without your consent, except in specific situations such as legal requirements, service provider cooperation, or business transfers. For more details, please refer to the "Information Collection Statement." + +Your Rights + +You have the right to access, correct, or delete your personal information at any time. If needed, please contact us via support@isunfa.com or +123-456-7890. +Changes to the Privacy Policy + +We may update the Privacy Policy from time to time. When significant changes are made, we will notify you through appropriate means and publish the changes in the updated policy. + +Thank you for your trust and support in our accounting software! + +iSunFA August 6, 2024 +`; diff --git a/src/constants/url.ts b/src/constants/url.ts index 7d340bbd0..8956bef82 100644 --- a/src/constants/url.ts +++ b/src/constants/url.ts @@ -13,6 +13,7 @@ export const ISUNFA_ROUTE = { REPORTS: '/reports', CONTACT_US: '/#contact-us', LOGIN: '/users/login', + LOGIN_BETA: '/login-beta', DASHBOARD: '/users/dashboard', KYC: '/users/kyc', SALARY: '/users/salary', diff --git a/src/contexts/global_context.tsx b/src/contexts/global_context.tsx index d0e991818..70ee2f4ff 100644 --- a/src/contexts/global_context.tsx +++ b/src/contexts/global_context.tsx @@ -53,6 +53,8 @@ import EditAccountTitleModal from '@/components/edit_account_title_modal/edit_ac import TeamSettingModal from '@/components/team_setting_modal/team_setting_modal'; import TransferCompanyModal from '@/components/transfer_company_modal/transfer_company_modal'; import { UploadType } from '@/constants/file'; +import LoginConfirmModal from '@/components/login_confirm_modal/login_confirm_modal.beta'; +import { term_1, term_2 } from '@/constants/terms'; interface IGlobalContext { width: number; @@ -140,6 +142,12 @@ interface IGlobalContext { isTransferCompanyModalVisible: boolean; transferCompanyModalVisibilityHandler: () => void; + + isAgreeWithInfomationConfirmModalVisible: boolean; + agreeWithInfomationConfirmModalVisibilityHandler: () => void; + + isTOSNPrivacyPolicyConfirmModalVisible: boolean; + TOSNPrivacyPolicyConfirmModalCallbackHandler: (callback: () => Promise) => void; } export interface IGlobalProvider { @@ -223,6 +231,15 @@ export const GlobalProvider = ({ children }: IGlobalProvider) => { const [isTransferCompanyModalVisible, setIsTransferCompanyModalVisible] = useState(false); + const [isAgreeWithInfomationConfirmModalVisible, setIsAgreeWithInfomationConfirmModalVisible] = + useState(false); + + const [isTOSNPrivacyPolicyConfirmModalVisible, setIsTOSNPrivacyPolicyConfirmModalVisible] = + useState(false); + + const [TOSNPrivacyPolicyConfirmModalCallback, setTOSNPrivacyPolicyConfirmModalCallback] = + useState<() => void>(() => {}); + const { width, height } = windowSize; const layoutAssertion = useMemo(() => { @@ -370,6 +387,18 @@ export const GlobalProvider = ({ children }: IGlobalProvider) => { setFilterOptionsForContract(options); }; + const agreeWithInfomationConfirmModalVisibilityHandler = () => { + setIsAgreeWithInfomationConfirmModalVisible(!isAgreeWithInfomationConfirmModalVisible); + }; + + const TOSNPrivacyPolicyConfirmModalVisibilityHandler = () => { + setIsTOSNPrivacyPolicyConfirmModalVisible(!isTOSNPrivacyPolicyConfirmModalVisible); + }; + + const TOSNPrivacyPolicyConfirmModalCallbackHandler = (callback: () => void) => { + setTOSNPrivacyPolicyConfirmModalCallback(callback); + }; + // Info: (20240509 - Julian) toast handler const toastHandler = useCallback((props: IToastify) => { const { @@ -390,7 +419,7 @@ export const GlobalProvider = ({ children }: IGlobalProvider) => { const position = toastPosition ?? ToastPosition.TOP_CENTER; // Info:(20240513 - Julian) default position 'top-center' // Info:(20240513 - Julian) 如果 closeable 為 false,則 autoClose、closeOnClick、draggable 都會被設為 false - const autoClose = closeable ? (isAutoClose ?? 5000) : false; // Info:(20240513 - Julian) default autoClose 5000ms + const autoClose = closeable ? isAutoClose ?? 5000 : false; // Info:(20240513 - Julian) default autoClose 5000ms const closeOnClick = closeable; // Info:(20240513 - Julian) default closeOnClick true const draggable = closeable; // Info:(20240513 - Julian) default draggable true @@ -625,6 +654,12 @@ export const GlobalProvider = ({ children }: IGlobalProvider) => { isTransferCompanyModalVisible, transferCompanyModalVisibilityHandler, + + isAgreeWithInfomationConfirmModalVisible, + agreeWithInfomationConfirmModalVisibilityHandler, + + isTOSNPrivacyPolicyConfirmModalVisible, + TOSNPrivacyPolicyConfirmModalCallbackHandler, }; return ( @@ -754,6 +789,33 @@ export const GlobalProvider = ({ children }: IGlobalProvider) => { modalVisibilityHandler={transferCompanyModalVisibilityHandler} /> + { + agreeWithInfomationConfirmModalVisibilityHandler(); + TOSNPrivacyPolicyConfirmModalVisibilityHandler(); + }} + onCancel={agreeWithInfomationConfirmModalVisibilityHandler} + /> + + { + TOSNPrivacyPolicyConfirmModalVisibilityHandler(); + TOSNPrivacyPolicyConfirmModalCallback(); + }} + onCancel={TOSNPrivacyPolicyConfirmModalVisibilityHandler} + /> {children} ); diff --git a/src/interfaces/api_connection.ts b/src/interfaces/api_connection.ts index 5a264f12a..e7f413837 100644 --- a/src/interfaces/api_connection.ts +++ b/src/interfaces/api_connection.ts @@ -3,6 +3,8 @@ import { IVoucher } from '@/interfaces/voucher'; import { ICompanyKYCForm } from './company_kyc'; export type IAPIName = + | 'LOGIN' + | 'AGREEMENT' | 'CREATE_CHALLENGE' | 'SIGN_UP' | 'SIGN_IN' diff --git a/src/interfaces/page_query.ts b/src/interfaces/page_query.ts index 855edce6a..50e8540f2 100644 --- a/src/interfaces/page_query.ts +++ b/src/interfaces/page_query.ts @@ -28,3 +28,16 @@ export const pageQueries: IPageQueries = { }, }, }; + +export const loginPageQueries: IPageQueries = { + loginPage: { + options: { + invitation: 'invitation', + action: 'action', + }, + actions: { + GOOGLE: 'google', + APPLE: 'apple', + }, + }, +}; diff --git a/src/locales/cn/common.json b/src/locales/cn/common.json index 58588554b..3f7f07b4b 100644 --- a/src/locales/cn/common.json +++ b/src/locales/cn/common.json @@ -325,7 +325,11 @@ "Y": "年", "V": "版本", "OR": "或", - "ERROR_CODE": "错误代码: {{code}}" + "ERROR_CODE": "错误代码: {{code}}", + "CANCEL": "取消", + "PLEASE_READ_AND_AGREE_THE_FIRST_TIME_YOU_LOGIN": "第一次登录时,请阅读并同意", + "AGREE_WITH_INFORMATION_COLLECTION_STATEMENT": "同意信息收集声明", + "AGREE_WITH_TOS_N_PP": "同意服务条款和隐私政策" }, "JOURNAL": { "FAILED_TO_FETCH_DATA": "获取数据失败", @@ -441,7 +445,6 @@ "LEAVE_HINT": "您的日记帐还未新增", "LEAVE_HINT_CONTENT": "您的日记帐还未新增,您确定要离开吗?", "LEAVE": "离开", - "CANCEL": "取消", "FEE_EXCEEDS_TOTAL": "费用不能大于总金额" }, "PROJECT": { @@ -687,7 +690,6 @@ "UPLOAD_DOCUMENT": "上传文件", "NEXT": "下一步", "SUBMIT": "提交", - "CANCEL": "取消", "CONFIRM": "确认", "BUSINESS_REGISTRATION_CERTIFICATE": "商业登记证书", "TAX_STATUS_CERTIFICATE": "税务状况证明", diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 99d1cf97d..cded319da 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -325,7 +325,11 @@ "Y": "Y", "V": "V", "OR": "OR", - "ERROR_CODE": "Error Code: {{code}}" + "ERROR_CODE": "Error Code: {{code}}", + "CANCEL": "Cancel", + "PLEASE_READ_AND_AGREE_THE_FIRST_TIME_YOU_LOGIN": "Please read and agree the first time you login", + "AGREE_WITH_INFORMATION_COLLECTION_STATEMENT": "Agree with the Information Collection Statement", + "AGREE_WITH_TOS_N_PP": "Agree with Terms of Service & Privacy Policy" }, "JOURNAL": { "FAILED_TO_FETCH_DATA": "Failed to fetch data", @@ -441,7 +445,6 @@ "LEAVE_HINT": "You journal is not added yet", "LEAVE_HINT_CONTENT": "You journal is not added yet. Are you sure you want to leave now?", "LEAVE": "Leave", - "CANCEL": "Cancel", "FEE_EXCEEDS_TOTAL": "Fee cannot exceed total amount" }, "PROJECT": { @@ -687,7 +690,6 @@ "UPLOAD_DOCUMENT": "Upload Document", "NEXT": "Next", "SUBMIT": "Submit", - "CANCEL": "Cancel", "CONFIRM": "Confirm", "BUSINESS_REGISTRATION_CERTIFICATE": "Business Registration Certificate", "TAX_STATUS_CERTIFICATE": "Tax Status Certification", diff --git a/src/locales/tw/common.json b/src/locales/tw/common.json index 3d141f7d1..1f2bfb9e1 100644 --- a/src/locales/tw/common.json +++ b/src/locales/tw/common.json @@ -325,7 +325,11 @@ "Y": "年", "V": "版本", "OR": "或", - "ERROR_CODE": "錯誤代碼: {{code}}" + "ERROR_CODE": "錯誤代碼: {{code}}", + "CANCEL": "取消", + "PLEASE_READ_AND_AGREE_THE_FIRST_TIME_YOU_LOGIN": "首次登錄時,請閱讀並同意", + "AGREE_WITH_INFORMATION_COLLECTION_STATEMENT": "同意《信息收集聲明》", + "AGREE_WITH_TOS_N_PP": "同意《服務條款》和《隱私政策》" }, "JOURNAL": { "FAILED_TO_FETCH_DATA": "獲取數據失敗", @@ -441,7 +445,6 @@ "LEAVE_HINT": "您的日記帳還未新增", "LEAVE_HINT_CONTENT": "您的日記帳還未新增,您確定要離開嗎?", "LEAVE": "離開", - "CANCEL": "取消", "FEE_EXCEEDS_TOTAL": "費用不能大於總金額" }, "PROJECT": { @@ -687,7 +690,6 @@ "UPLOAD_DOCUMENT": "上傳文件", "NEXT": "下一步", "SUBMIT": "提交", - "CANCEL": "取消", "CONFIRM": "確認", "BUSINESS_REGISTRATION_CERTIFICATE": "商業登記證明", "TAX_STATUS_CERTIFICATE": "稅務狀態證明", diff --git a/src/pages/api/auth/next_auth.ts b/src/pages/api/auth/next_auth.ts new file mode 100644 index 000000000..a353192d1 --- /dev/null +++ b/src/pages/api/auth/next_auth.ts @@ -0,0 +1,79 @@ +import NextAuth from 'next-auth'; +import GoogleProvider from 'next-auth/providers/google'; +import AppleProvider from 'next-auth/providers/apple'; +import jwt from 'jsonwebtoken'; +import { ISUNFA_ROUTE } from '@/constants/url'; + +const generateAppleClientSecret = () => { + const privateKey = process.env.APPLE_PRIVATE_KEY?.replace(/\\n/g, '\n'); + if (!privateKey) { + throw new Error('APPLE_PRIVATE_KEY is not defined'); + } + + const clientSecret = jwt.sign( + { + iss: process.env.APPLE_TEAM_ID as string, // Team ID + iat: Math.floor(Date.now() / 1000), // Current time in seconds + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Expiration time (1 months) + aud: 'https://appleid.apple.com', + sub: process.env.APPLE_CLIENT_ID as string, // Service ID + }, + privateKey, + { + algorithm: 'ES256', + header: { + alg: 'ES256', + kid: process.env.APPLE_KEY_ID as string, // Key ID + }, + } + ); + + return clientSecret; +}; + +export default NextAuth({ + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID as string, + clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, + }), + AppleProvider({ + clientId: process.env.APPLE_CLIENT_ID as string, + clientSecret: generateAppleClientSecret(), + }), + ], + pages: { + signIn: ISUNFA_ROUTE.LOGIN_BETA, + }, + session: { + strategy: 'jwt', + }, + callbacks: { + async jwt({ token, account, user }) { + // Deprecated: (20240815 - Tzuhan) Remove console.log + // eslint-disable-next-line no-console + console.log('jwt', token, account, user); + let newToken = token; + if (account) { + newToken = { + ...token, + accessToken: account.access_token, + }; + } + return newToken; + }, + async session({ session, token }) { + // Deprecated: (20240815 - Tzuhan) Remove console.log + // eslint-disable-next-line no-console + console.log('session', session, token); + const newSession = { + ...session, + user: { + ...session.user, + id: token.sub as string, + }, + }; + return newSession; + }, + }, +}); diff --git a/src/pages/users/login-beta.tsx b/src/pages/users/login-beta.tsx new file mode 100644 index 000000000..61632b38c --- /dev/null +++ b/src/pages/users/login-beta.tsx @@ -0,0 +1,66 @@ +import Head from 'next/head'; +import React from 'react'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import NavBar from '@/components/nav_bar/nav_bar'; +import LoginPageBody from '@/components/login_page_body/login_page_body.beta'; +import { useUserCtx } from '@/contexts/user_context'; +import { GetServerSideProps } from 'next'; +import { SkeletonList } from '@/components/skeleton/skeleton'; +import { DEFAULT_SKELETON_COUNT_FOR_PAGE } from '@/constants/display'; +import { useTranslation } from 'next-i18next'; + +const LoginPage = () => { + const { t } = useTranslation('common'); + const { isAuthLoading } = useUserCtx(); + + const displayedBody = isAuthLoading ? ( +
+ +
+ ) : ( +
+ +
+ ); + + return ( + <> + + + + + {t('NAV_BAR.LOGIN')} - iSunFA + + + + + + + +
+ + {displayedBody} +
+ + ); +}; + +export const getServerSideProps: GetServerSideProps = async ({ locale, query }) => { + const { invitation = '', action = '' } = query; + + return { + props: { + invitation: invitation as string, + action: action as string, + ...(await serverSideTranslations(locale as string, ['common'])), + }, + }; +}; + +export default LoginPage; From 9f2ae7e635c6ddde66ba2ec6bf646c79796df5ca Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Mon, 12 Aug 2024 19:31:15 +0800 Subject: [PATCH 2/2] Update .env.example --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index bf29afc8c..066aa482e 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,7 @@ PAYMENT_TOKEN = PAYMENT_ID = PAYMENT_SERVICE = +# OAuth 2.0 for apple login and google login GOOGLE_CLIENT_ID = google-client-id GOOGLE_CLIENT_SECRET = google-client-secret @@ -48,4 +49,4 @@ APPLE_TEAM_ID = apple-team-id APPLE_PRIVATE_KEY = apple-private-key NEXTAUTH_URL = https://isunfa.com/ -NEXTAUTH_SECRET = generated-random-secret \ No newline at end of file +NEXTAUTH_SECRET = generated-random-secret