diff --git a/frontend/desktop/src/pages/api/account/enterpriseRealName.ts b/frontend/desktop/src/pages/api/account/enterpriseRealName.ts index e818e7fca..8f99a5242 100644 --- a/frontend/desktop/src/pages/api/account/enterpriseRealName.ts +++ b/frontend/desktop/src/pages/api/account/enterpriseRealName.ts @@ -106,7 +106,10 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse, userUid: str }); if (!info || !info.additionalInfo) { - return jsonRes(res, { code: 200, data: { authTimes: realNameAuthProvider.maxFailedTimes } }); + return jsonRes(res, { + code: 200, + data: { remainingAttempts: realNameAuthProvider.maxFailedTimes } + }); } const additionalInfo = info.additionalInfo as unknown as AdditionalInfo; diff --git a/frontend/desktop/src/pages/api/account/enterpriseRealNameVerify.ts b/frontend/desktop/src/pages/api/account/enterpriseRealNameVerify.ts index cf1e7bbd1..bf0057224 100644 --- a/frontend/desktop/src/pages/api/account/enterpriseRealNameVerify.ts +++ b/frontend/desktop/src/pages/api/account/enterpriseRealNameVerify.ts @@ -6,6 +6,9 @@ import { globalPrisma } from '@/services/backend/db/init'; import { z } from 'zod'; import { PAYMENTSTATUS } from '@/types/response/enterpriseRealName'; import { AdditionalInfo } from '@/types/response/enterpriseRealName'; +import { RealNameAuthProvider } from '@/pages/api/account/faceIdRealNameAuthCallback'; +import { TencentCloudFaceAuthConfig } from '@/pages/api/account/faceIdRealNameAuthCallback'; +import { getInviterInfo } from '@/utils/getInviteInfo'; const schema = z.object({ transAmt: z.string().min(1) @@ -25,7 +28,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const payload = await verifyAccessToken(req.headers); if (!payload) return jsonRes(res, { code: 401, message: 'Token is invalid' }); + const userUid = payload.userUid; + const userId = payload.userId; + + const inviteInfo = await getInviterInfo(userId); + try { + const realNameAuthProvider: RealNameAuthProvider | null = + await globalPrisma.realNameAuthProvider.findFirst({ + where: { + backend: 'TENCENTCLOUD', + authType: 'tcloudFaceAuth' + } + }); + + if (!realNameAuthProvider) { + throw new Error('faceidRealNameAuth: Real name authentication provider not found'); + } + + const config: TencentCloudFaceAuthConfig = + realNameAuthProvider.config as TencentCloudFaceAuthConfig; + + if (!config) { + throw new Error('faceidRealNameAuth: Real name authentication configuration not found'); + } + + const realNameAuthReward = config.realNameAuthReward; + const validationResult = schema.safeParse(req.body); if (!validationResult.success) { return jsonRes(res, { @@ -60,17 +89,130 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const additionalInfo = info.additionalInfo as unknown as AdditionalInfo; - if (additionalInfo.transAmt === transAmt) { - await globalPrisma.enterpriseRealNameInfo.update({ - where: { userUid: payload.userUid }, + if (additionalInfo.transAmt !== transAmt) { + return jsonRes(res, { + code: 200, + message: 'transAmt_not_match', data: { - isVerified: true, - additionalInfo: { - ...additionalInfo, - paymentStatus: PAYMENTSTATUS.SUCCESS - } + authState: 'failed' } }); + } + + if (realNameAuthReward) { + await globalPrisma.$transaction(async (globalPrisma) => { + const currentAccount = await globalPrisma.account.findUniqueOrThrow({ + where: { userUid } + }); + + if (!currentAccount.balance) { + throw new Error('enterpriseRealNameVerify: Account balance not found'); + } + + const currentActivityBonus = currentAccount.activityBonus || BigInt(0); + const realnameReward = BigInt(realNameAuthReward); + + const newActivityBonus = currentActivityBonus + realnameReward; + const newBalance = currentAccount.balance + realnameReward; + + const updatedAccount = await globalPrisma.account.update({ + where: { userUid }, + data: { + activityBonus: newActivityBonus, + balance: newBalance + } + }); + + const enterpriseRealNameInfo = await globalPrisma.enterpriseRealNameInfo.update({ + where: { userUid: payload.userUid }, + data: { + isVerified: true, + additionalInfo: { + ...additionalInfo, + paymentStatus: PAYMENTSTATUS.SUCCESS + } + } + }); + + const accountTransaction = await globalPrisma.accountTransaction.create({ + data: { + type: 'REALNAME_AUTH_REWARD', + userUid: userUid, + balance: realnameReward, + balance_before: currentAccount.balance, + deduction_balance: 0, // No deduction in this case + deduction_balance_before: currentAccount.deduction_balance, + message: 'Real name authentication reward', + billing_id: enterpriseRealNameInfo.id // You'll need to implement this function + } + }); + + await globalPrisma.userTask.updateMany({ + where: { + userUid, + task: { + taskType: 'REAL_NAME_AUTH' + }, + status: 'NOT_COMPLETED' + }, + data: { + rewardStatus: 'COMPLETED', + status: 'COMPLETED', + completedAt: new Date() + } + }); + + if (inviteInfo.inviterId && inviteInfo.amount) { + const inviterUser = await globalPrisma.user.findUniqueOrThrow({ + where: { id: inviteInfo.inviterId } + }); + + if (!inviterUser) { + throw new Error('enterpriseRealNameVerify: Inviter user not found'); + } + + const inviterAccount = await globalPrisma.account.findUniqueOrThrow({ + where: { userUid: inviterUser.uid } + }); + + if (!inviterAccount.balance) { + throw new Error('enterpriseRealNameVerify: Inviter account balance not found'); + } + + const currentActivityBonus = inviterAccount.activityBonus || BigInt(0); + const realnameInviteReward = inviteInfo.amount; + + const newActivityBonus = currentActivityBonus + realnameInviteReward; + const newBalance = inviterAccount.balance + realnameInviteReward; + + await globalPrisma.account.update({ + where: { userUid: inviterUser.uid }, + data: { + activityBonus: newActivityBonus, + balance: newBalance + } + }); + + await globalPrisma.accountTransaction.create({ + data: { + type: 'REALNAME_AUTH_INVITE_REWARD', + userUid: inviterUser.uid, + balance: realnameInviteReward, + balance_before: inviterAccount.balance, + deduction_balance: 0, // No deduction in this case + deduction_balance_before: inviterAccount.deduction_balance, + message: 'Real name authentication invite reward', + billing_id: enterpriseRealNameInfo.id // You'll need to implement this function + } + }); + } + + return { + account: updatedAccount, + transaction: accountTransaction, + enterpriseRealNameInfo: enterpriseRealNameInfo + }; + }); return jsonRes(res, { code: 200, @@ -81,11 +223,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } + await globalPrisma.enterpriseRealNameInfo.update({ + where: { userUid: payload.userUid }, + data: { + isVerified: true, + additionalInfo: { + ...additionalInfo, + paymentStatus: PAYMENTSTATUS.SUCCESS + } + } + }); + return jsonRes(res, { code: 200, - message: 'transAmt_not_match', data: { - authState: 'failed' + authState: 'success', + enterpriseRealName: additionalInfo.keyName } }); } catch (error) { diff --git a/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts b/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts index 03e6f1155..40a1d93de 100644 --- a/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts +++ b/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts @@ -7,8 +7,9 @@ import { globalPrisma } from '@/services/backend/db/init'; import { GetDetectInfoEnhancedResponse } from 'tencentcloud-sdk-nodejs/tencentcloud/services/faceid/v20180301/faceid_models'; import { RealNameOSSConfigType } from '@/types'; import { Client, ClientOptions } from 'minio'; +import { getInviterInfo } from '@/utils/getInviteInfo'; -type TencentCloudFaceAuthConfig = { +export type TencentCloudFaceAuthConfig = { secretId: string; secretKey: string; ruleId: string; @@ -17,7 +18,7 @@ type TencentCloudFaceAuthConfig = { type JsonValue = string | number | boolean | object | null; -type RealNameAuthProvider = { +export type RealNameAuthProvider = { id: string; backend: string; authType: string; @@ -95,6 +96,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const isFaceRecognitionSuccess = userRealNameFaceAuthInfo.Text?.ErrCode === 0; const userUid = payload.userUid; + const userId = payload.userId; const timestamp = Date.now(); // Fetch existing user real name info @@ -110,6 +112,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return jsonRes(res, { code: 400, message: 'User real name info already verified' }); } + const inviteInfo = await getInviterInfo(userId); + let additionalInfo: AdditionalInfo = userRealNameInfo.additionalInfo; additionalInfo.faceRecognition.callback.isUsed = true; @@ -151,114 +155,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) additionalInfo.userMaterials.push(videoPath); } - if (isFaceRecognitionSuccess) { - if (realNameAuthReward) { - await globalPrisma.$transaction(async (globalPrisma) => { - const currentAccount = await globalPrisma.account.findUniqueOrThrow({ - where: { userUid } - }); - - if (!currentAccount.balance) { - throw new Error('faceidRealNameAuth: Account balance not found'); - } - - const currentActivityBonus = currentAccount.activityBonus || BigInt(0); - const rewardBigInt = BigInt(realNameAuthReward); - - const newActivityBonus = currentActivityBonus + rewardBigInt; - const newBalance = currentAccount.balance + rewardBigInt; - - const updatedAccount = await globalPrisma.account.update({ - where: { userUid }, - data: { - activityBonus: newActivityBonus, - balance: newBalance - } - }); - - const userRealNameInfo = await globalPrisma.userRealNameInfo.update({ - where: { userUid }, - data: { - realName: userRealNameFaceAuthInfo.Text?.Name, - idCard: userRealNameFaceAuthInfo.Text?.IdCard, - isVerified: true, - additionalInfo: additionalInfo - } - }); - - const accountTransaction = await globalPrisma.accountTransaction.create({ - data: { - type: 'REALNAME_AUTH_REWARD', - userUid: userUid, - balance: rewardBigInt, - balance_before: currentAccount.balance, - deduction_balance: 0, // No deduction in this case - deduction_balance_before: currentAccount.deduction_balance, - message: 'Real name authentication reward', - billing_id: userRealNameInfo.id // You'll need to implement this function - } - }); - - await globalPrisma.userTask.updateMany({ - where: { - userUid, - task: { - taskType: 'REAL_NAME_AUTH' - }, - status: 'NOT_COMPLETED' - }, - data: { - rewardStatus: 'COMPLETED', - status: 'COMPLETED', - completedAt: new Date() - } - }); - - return { - account: updatedAccount, - transaction: accountTransaction, - userRealNameInfo: userRealNameInfo - }; - }); - } else { - await globalPrisma.userRealNameInfo.update({ - where: { userUid }, - data: { - realName: userRealNameFaceAuthInfo.Text?.Name, - idCard: userRealNameFaceAuthInfo.Text?.IdCard, - isVerified: true, - additionalInfo: additionalInfo - } - }); - } - - res.setHeader('Content-Type', 'text/html'); - return res.send(` - - - - - - Real Name Authentication - - - -

Real Name Authentication Successful

- - - `); - } else { + if (!isFaceRecognitionSuccess) { await globalPrisma.userRealNameInfo.update({ where: { userUid }, data: { @@ -296,6 +193,185 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) `); } + + if (realNameAuthReward) { + await globalPrisma.$transaction(async (globalPrisma) => { + const currentAccount = await globalPrisma.account.findUniqueOrThrow({ + where: { userUid } + }); + + if (!currentAccount.balance) { + throw new Error('faceidRealNameAuth: Account balance not found'); + } + + const currentActivityBonus = currentAccount.activityBonus || BigInt(0); + const realnameReward = BigInt(realNameAuthReward); + + const newActivityBonus = currentActivityBonus + realnameReward; + const newBalance = currentAccount.balance + realnameReward; + + const updatedAccount = await globalPrisma.account.update({ + where: { userUid }, + data: { + activityBonus: newActivityBonus, + balance: newBalance + } + }); + + const userRealNameInfo = await globalPrisma.userRealNameInfo.update({ + where: { userUid }, + data: { + realName: userRealNameFaceAuthInfo.Text?.Name, + idCard: userRealNameFaceAuthInfo.Text?.IdCard, + isVerified: true, + additionalInfo: additionalInfo + } + }); + + const accountTransaction = await globalPrisma.accountTransaction.create({ + data: { + type: 'REALNAME_AUTH_REWARD', + userUid: userUid, + balance: realnameReward, + balance_before: currentAccount.balance, + deduction_balance: 0, // No deduction in this case + deduction_balance_before: currentAccount.deduction_balance, + message: 'Real name authentication reward', + billing_id: userRealNameInfo.id // You'll need to implement this function + } + }); + + await globalPrisma.userTask.updateMany({ + where: { + userUid, + task: { + taskType: 'REAL_NAME_AUTH' + }, + status: 'NOT_COMPLETED' + }, + data: { + rewardStatus: 'COMPLETED', + status: 'COMPLETED', + completedAt: new Date() + } + }); + + if (inviteInfo.inviterId && inviteInfo.amount) { + const inviterUser = await globalPrisma.user.findUniqueOrThrow({ + where: { id: inviteInfo.inviterId } + }); + + if (!inviterUser) { + throw new Error('faceidRealNameAuth: Inviter user not found'); + } + + const inviterAccount = await globalPrisma.account.findUniqueOrThrow({ + where: { userUid: inviterUser.uid } + }); + + if (!inviterAccount.balance) { + throw new Error('faceidRealNameAuth: Inviter account balance not found'); + } + + const currentActivityBonus = inviterAccount.activityBonus || BigInt(0); + const realnameInviteReward = inviteInfo.amount; + + const newActivityBonus = currentActivityBonus + realnameInviteReward; + const newBalance = inviterAccount.balance + realnameInviteReward; + + await globalPrisma.account.update({ + where: { userUid: inviterUser.uid }, + data: { + activityBonus: newActivityBonus, + balance: newBalance + } + }); + + await globalPrisma.accountTransaction.create({ + data: { + type: 'REALNAME_AUTH_INVITE_REWARD', + userUid: inviterUser.uid, + balance: realnameInviteReward, + balance_before: inviterAccount.balance, + deduction_balance: 0, // No deduction in this case + deduction_balance_before: inviterAccount.deduction_balance, + message: 'Real name authentication invite reward', + billing_id: userRealNameInfo.id // You'll need to implement this function + } + }); + } + + return { + account: updatedAccount, + transaction: accountTransaction, + userRealNameInfo: userRealNameInfo + }; + }); + + res.setHeader('Content-Type', 'text/html'); + return res.send(` + + + + + + Real Name Authentication + + + +

Real Name Authentication Successful

+ + + `); + } + + await globalPrisma.userRealNameInfo.update({ + where: { userUid }, + data: { + realName: userRealNameFaceAuthInfo.Text?.Name, + idCard: userRealNameFaceAuthInfo.Text?.IdCard, + isVerified: true, + additionalInfo: additionalInfo + } + }); + + res.setHeader('Content-Type', 'text/html'); + return res.send(` + + + + + + Real Name Authentication + + + +

Real Name Authentication Successful

+ + + `); } catch (error) { console.error('faceidRealNameAuth: Internal error'); if (error instanceof Error) { diff --git a/frontend/desktop/src/utils/getInviteInfo.ts b/frontend/desktop/src/utils/getInviteInfo.ts new file mode 100644 index 000000000..9b2e4b88f --- /dev/null +++ b/frontend/desktop/src/utils/getInviteInfo.ts @@ -0,0 +1,51 @@ +import { AuthConfigType } from '@/types/system'; +interface InviterResponse { + code: number; + message: string; + data?: { + inviterId: string; + reward: { + amount: number; + }; + }; +} + +export async function getInviterInfo( + inviteeId: string +): Promise<{ inviterId?: string; amount?: bigint }> { + const conf = global.AppConfig?.desktop.auth as AuthConfigType; + const inviteEnabled = conf.invite?.enabled || false; + const secretKey = conf.invite?.lafSecretKey || ''; + const baseUrl = conf.invite?.lafBaseURL || ''; + + if (!inviteEnabled || !secretKey || !baseUrl) { + return {}; + } + + try { + const response = await fetch(`https://${baseUrl}/getInviter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + inviteeId, + secretKey + }) + }); + + const result: InviterResponse = await response.json(); + + if (result.code !== 200) { + return {}; + } + + return { + inviterId: result.data?.inviterId, + amount: result.data?.reward?.amount ? BigInt(result.data.reward.amount * 1000000) : BigInt(0) + }; + } catch (error) { + console.error('getInviteInfo error:', error); + return {}; + } +}