From 3c8cca942aa85219af3e8f730a8d161db9b86e4e Mon Sep 17 00:00:00 2001 From: dappsar Date: Sat, 19 Oct 2024 20:02:27 -0300 Subject: [PATCH 1/8] [feat] :alien: add function to allow edit email (#128) --- src/app/api/_data/data-service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/api/_data/data-service.ts b/src/app/api/_data/data-service.ts index 095791d..7a52940 100644 --- a/src/app/api/_data/data-service.ts +++ b/src/app/api/_data/data-service.ts @@ -102,6 +102,14 @@ export async function updateUser(contact: IAccount): Promise { return result } +export async function updateUserEmail(contact: IAccount): Promise { + const filter = { _id: getObjectId(contact.id) } + const updateData = { email: contact.email } + const setValue = { $set: updateData } + const result: boolean = await updateOneCommon(DB_CHATTERPAY_NAME, SCHEMA_USERS, filter, setValue) + return result +} + export async function getWalletNfts(wallet: string): Promise { const client = await getClientPromise() const db = client.db(DB_CHATTERPAY_NAME) From 99015c21af498bddd651546d18195b836d9d4b53 Mon Sep 17 00:00:00 2001 From: dappsar Date: Sat, 19 Oct 2024 20:03:21 -0300 Subject: [PATCH 2/8] [feat] :alien: add api logic to allow change-email (#128) --- src/app/api/_hooks/api-resolver.ts | 4 +- src/app/api/v1/_common/common.ts | 19 +++++ src/app/api/v1/auth/code/route.ts | 45 +----------- src/app/api/v1/user/[id]/code/route.ts | 85 ++++++++++++++++++++++ src/app/api/v1/user/[id]/email/route.ts | 94 +++++++++++++++++++++++++ src/app/api/v1/user/[id]/route.ts | 17 ++++- 6 files changed, 219 insertions(+), 45 deletions(-) create mode 100644 src/app/api/v1/_common/common.ts create mode 100644 src/app/api/v1/user/[id]/code/route.ts create mode 100644 src/app/api/v1/user/[id]/email/route.ts diff --git a/src/app/api/_hooks/api-resolver.ts b/src/app/api/_hooks/api-resolver.ts index 58d2754..1a9023a 100644 --- a/src/app/api/_hooks/api-resolver.ts +++ b/src/app/api/_hooks/api-resolver.ts @@ -55,7 +55,9 @@ export const endpoints = { root: getFullUIEndpoint('app'), user: { id: (id: string) => getFullUIEndpoint(`user/${id}`), - update: (id: string) => getFullUIEndpoint(`user/${id}`) + update: (id: string) => getFullUIEndpoint(`user/${id}`), + code: (id: string) => getFullUIEndpoint(`user/${id}/code`), + updateEmail: (id: string) => getFullUIEndpoint(`user/${id}/email`) }, wallet: { balance: (id: string) => getFullUIEndpoint(`wallet/${id}/balance`), diff --git a/src/app/api/v1/_common/common.ts b/src/app/api/v1/_common/common.ts new file mode 100644 index 0000000..ea10e1b --- /dev/null +++ b/src/app/api/v1/_common/common.ts @@ -0,0 +1,19 @@ +import { BOT_API_TOKEN } from 'src/config-global' + +import { post, endpoints } from '../../_hooks/api-resolver' + +// ---------------------------------------------------------------------- + +export async function send2FACode(phone: string, code: number, codeMsg: string) { + const botSendMsgEndpoint = endpoints.backend.sendMessage() + const botSendMsgData = { + data_token: BOT_API_TOKEN, + channel_user_id: phone, + message: codeMsg.replace('{2FA_CODE}', code.toString()) + } + console.info('botSendMsgData', botSendMsgData) + const botSendMsgResult = await post(botSendMsgEndpoint, botSendMsgData) + console.info('botSendMsgResult', botSendMsgResult) + + return true +} diff --git a/src/app/api/v1/auth/code/route.ts b/src/app/api/v1/auth/code/route.ts index a3d3689..a214c4f 100644 --- a/src/app/api/v1/auth/code/route.ts +++ b/src/app/api/v1/auth/code/route.ts @@ -1,12 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import { post, endpoints } from 'src/app/api/_hooks/api-resolver' +import { getUserByPhone, updateUserCode } from 'src/app/api/_data/data-service' import { getIpFromRequest, validateRecaptcha } from 'src/app/api/_utils/request-utils' -import { - getUserByPhone, - updateUserCode, - getLastConversacionUserId -} from 'src/app/api/_data/data-service' import { BOT_API_URL, BOT_API_TOKEN, @@ -15,7 +10,8 @@ import { } from 'src/config-global' import { IAccount } from 'src/types/account' -import { LastUserConversation } from 'src/types/chat' + +import { send2FACode } from '../../_common/common' // ---------------------------------------------------------------------- @@ -118,38 +114,3 @@ export async function POST(req: NextRequest) { }) } } - -// ---------------------------------------------------------------------- - -async function send2FACode(phone: string, code: number, codeMsg: string) { - let phoneToMsg = phone - - // Search last conversationIn User in bot with last 8 phone-digits - console.info('entered send2FACode', phone, code) - const lastUserConversation: LastUserConversation = await getLastConversacionUserId(phone) - console.info('lastUserConversation', phone, lastUserConversation) - - if (!lastUserConversation) { - console.info('lastUserConversation NOT_FOUND, using phone:', phone) - } else { - phoneToMsg = lastUserConversation.channel_user_id - console.info( - 'lastUserConversation FOUND, using channel_user_id as phone:', - lastUserConversation.channel_user_id, - lastUserConversation.id - ) - } - - // Send 2FA code by whatsapp with operator-reply endpoint - const botSendMsgEndpoint = endpoints.backend.sendMessage() - const botSendMsgData = { - data_token: BOT_API_TOKEN, - channel_user_id: phoneToMsg, - message: codeMsg.replace('{2FA_CODE}', code.toString()) - } - console.info('botSendMsgData', botSendMsgData) - const botSendMsgResult = await post(botSendMsgEndpoint, botSendMsgData) - console.info('botSendMsgResult', botSendMsgResult) - - return true -} diff --git a/src/app/api/v1/user/[id]/code/route.ts b/src/app/api/v1/user/[id]/code/route.ts new file mode 100644 index 0000000..9160280 --- /dev/null +++ b/src/app/api/v1/user/[id]/code/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from 'next/server' + +import { getUserByPhone, updateUserCode } from 'src/app/api/_data/data-service' +import { BOT_API_URL, BOT_API_TOKEN, botApiWappEnabled } from 'src/config-global' + +import { IAccount } from 'src/types/account' + +import { send2FACode } from '../../../_common/common' + +// ---------------------------------------------------------------------- + +export async function POST(req: NextRequest) { + try { + const { phone, codeMsg }: { phone: string; codeMsg: string; recaptchaToken: string } = + await req.json() + + if (!phone || !codeMsg) { + return new NextResponse( + JSON.stringify({ + code: 'INVALID_REQUEST_PARAMS', + error: 'Missing phone number or codeMsg in request body' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' } + } + ) + } + + if (!BOT_API_URL || !BOT_API_TOKEN) { + return new NextResponse(JSON.stringify({ error: `Backend API or Token not set.` }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }) + } + + const user: IAccount | undefined = await getUserByPhone(phone) + if (!user) { + return new NextResponse( + JSON.stringify({ code: 'USER_NOT_FOUND', error: 'user not found with that phone number' }), + { + status: 404, + headers: { 'Content-Type': 'application/json' } + } + ) + } + + // Generate and store 2FA code + const code: number = Math.floor(Math.random() * (999999 - 100000 + 1)) + 100000 + await updateUserCode(user.id, code) + + // Send 2FA code to user'whatsapp + let botSentCodeResult: boolean = true + if (botApiWappEnabled) { + console.info('calling send2FACode SYNC', phone, code) + botSentCodeResult = await send2FACode(phone, code, codeMsg) + + if (!botSentCodeResult) { + return new NextResponse( + JSON.stringify({ + code: 'USER_NOT_FOUND', + error: 'user not found with that phone number' + }), + { + status: 404, + headers: { 'Content-Type': 'application/json' } + } + ) + } + } + + const finalResult: { phone: string; sent: boolean } = { + phone, + sent: botSentCodeResult + } + + return NextResponse.json(finalResult) + } catch (ex) { + console.error(ex) + return new NextResponse(JSON.stringify({ error: 'Error in authentication' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) + } +} diff --git a/src/app/api/v1/user/[id]/email/route.ts b/src/app/api/v1/user/[id]/email/route.ts new file mode 100644 index 0000000..cd2601e --- /dev/null +++ b/src/app/api/v1/user/[id]/email/route.ts @@ -0,0 +1,94 @@ +import { NextRequest, NextResponse } from 'next/server' + +import { getUserById, updateUserCode, updateUserEmail } from 'src/app/api/_data/data-service' + +import { IAccount } from 'src/types/account' + +// ---------------------------------------------------------------------- + +type IParams = { + id: string +} + +type IBody = { + phone: string + code: string + recaptchaToken: string + email: string +} +export async function POST(req: NextRequest, { params }: { params: IParams }) { + try { + const { id } = params + const { phone, code, email }: IBody = await req.json() + + if (!id) { + return new NextResponse( + JSON.stringify({ + code: 'INVALID_REQUEST_PARAMS', + error: 'Missing parameters in path params' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' } + } + ) + } + + if (!email || !code || !phone) { + return new NextResponse( + JSON.stringify({ + code: 'INVALID_REQUEST_PARAMS', + error: 'Missing email, code and phone in request body' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' } + } + ) + } + + const user: IAccount | undefined = await getUserById(id) + if (!user) { + return new NextResponse( + JSON.stringify({ code: 'USER_NOT_FOUND', error: 'user not found with that id' }), + { + status: 404, + headers: { 'Content-Type': 'application/json' } + } + ) + } + + if (!user.code || code.toString() !== user.code.toString()) { + return new NextResponse(JSON.stringify({ code: 'INVALID_CODE', error: 'invalid code' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) + } + + user.email = email + const reult: boolean = await updateUserEmail(user) + + if (!reult) { + return new NextResponse( + JSON.stringify({ + code: 'USER_UPDATE_ERROR', + error: 'user update error' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' } + } + ) + } + + updateUserCode(user.id, undefined) + + return NextResponse.json({ reult }) + } catch (ex) { + console.error(ex) + return new NextResponse(JSON.stringify({ error: 'Error in update user' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) + } +} diff --git a/src/app/api/v1/user/[id]/route.ts b/src/app/api/v1/user/[id]/route.ts index d9c3596..10cf81b 100644 --- a/src/app/api/v1/user/[id]/route.ts +++ b/src/app/api/v1/user/[id]/route.ts @@ -62,11 +62,24 @@ export async function POST(req: NextRequest, { params }: { params: IParams }) { const { id } = params const { name }: { name: string } = await req.json() - if (!name || !id) { + if (!id) { + return new NextResponse( + JSON.stringify({ + code: 'INVALID_REQUEST_PARAMS', + error: 'Missing parameters in path params' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' } + } + ) + } + + if (!name) { return new NextResponse( JSON.stringify({ code: 'INVALID_REQUEST_PARAMS', - error: 'Missing id or name in request body' + error: 'Missing name in request body' }), { status: 400, From 4c4b65bd0cbdd1353cf6614e08c8df3bf6e2cf3e Mon Sep 17 00:00:00 2001 From: dappsar Date: Sat, 19 Oct 2024 20:03:56 -0300 Subject: [PATCH 3/8] [feat] :sparkles add change-email page (#128) --- src/app/dashboard/user/account/email/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/dashboard/user/account/email/page.tsx diff --git a/src/app/dashboard/user/account/email/page.tsx b/src/app/dashboard/user/account/email/page.tsx new file mode 100644 index 0000000..90fe84e --- /dev/null +++ b/src/app/dashboard/user/account/email/page.tsx @@ -0,0 +1,11 @@ +import { EmailEditView } from 'src/sections/account/view' + +// ---------------------------------------------------------------------- + +export const metadata = { + title: 'Account - Email' +} + +export default function AccountEmailPage() { + return +} From 9c5bfc45f3bd579cbb4f0ad7d90b47aa65d6d5ae Mon Sep 17 00:00:00 2001 From: dappsar Date: Sat, 19 Oct 2024 20:04:30 -0300 Subject: [PATCH 4/8] [feat] :sparkles add change-email functions in context (#128) --- src/auth/context/jwt/auth-provider.tsx | 92 +++++++++++++++++++++----- src/auth/types.ts | 10 ++- 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/auth/context/jwt/auth-provider.tsx b/src/auth/context/jwt/auth-provider.tsx index ede90b1..02922ee 100644 --- a/src/auth/context/jwt/auth-provider.tsx +++ b/src/auth/context/jwt/auth-provider.tsx @@ -14,7 +14,9 @@ import { AuthUserType, ActionMapType, AuthStateType, AuthUserCodeType } from '.. enum Types { INITIAL = 'INITIAL', LOGIN = 'LOGIN', - GENERATE_CODE = 'GENERATE_CODE', + UPDATE_EMAIL = 'UPDATE_EMAIL', + GENERATE_CODE_LOGIN = 'GENERATE_CODE_LOGIN', + GENERATE_CODE_EMAIL = 'GENERATE_CODE_EMAIL', REGISTER = 'REGISTER', LOGOUT = 'LOGOUT', UPDATE_USER = 'UPDATE_USER' @@ -27,7 +29,13 @@ type Payload = { [Types.LOGIN]: { user: AuthUserType } - [Types.GENERATE_CODE]: { + [Types.GENERATE_CODE_LOGIN]: { + user: AuthUserCodeType + } + [Types.GENERATE_CODE_EMAIL]: { + user: AuthUserCodeType + } + [Types.UPDATE_EMAIL]: { user: AuthUserCodeType } [Types.REGISTER]: { @@ -60,11 +68,21 @@ const reducer = (state: AuthStateType, action: ActionsType) => { ...state, user: action.payload.user } - case Types.GENERATE_CODE: + case Types.GENERATE_CODE_LOGIN: return { ...state, user: null } + case Types.GENERATE_CODE_EMAIL: + return { + ...state, + user: action.payload.user + } + case Types.UPDATE_EMAIL: + return { + ...state, + user: action.payload.user + } case Types.REGISTER: return { ...state, @@ -135,7 +153,6 @@ export function AuthProvider({ children }: Props) { initialize() }, [initialize]) - // LOGIN const login = useCallback(async (email: string, password: string) => { const data = { email, @@ -144,9 +161,8 @@ export function AuthProvider({ children }: Props) { const res = await axios.post(endpoints.auth.login(), data) - const user = res.data - - const accessToken = 'dummyToken' + const { user } = res.data + const { accessToken } = res.data setSession(accessToken) @@ -161,8 +177,7 @@ export function AuthProvider({ children }: Props) { }) }, []) - // Generate whatsapp Code - const generateCode = useCallback( + const generate2faCodeLogin = useCallback( async (phone: string, codeMsg: string, recaptchaToken: string) => { const data = { phone, @@ -171,7 +186,7 @@ export function AuthProvider({ children }: Props) { } await post(endpoints.auth.code(), data) dispatch({ - type: Types.GENERATE_CODE, + type: Types.GENERATE_CODE_LOGIN, payload: { user: null } @@ -180,7 +195,23 @@ export function AuthProvider({ children }: Props) { [] ) - // LOGIN With Code + const generate2faCodeEmail = useCallback( + async (id: string, phone: string, codeMsg: string) => { + const data = { + phone, + codeMsg + } + await post(endpoints.dashboard.user.code(id), data) + dispatch({ + type: Types.GENERATE_CODE_EMAIL, + payload: { + user: state.user + } + }) + }, + [state] + ) + const loginWithCode = useCallback(async (phone: string, code: string, recaptchaToken: string) => { const data = { phone, @@ -203,7 +234,25 @@ export function AuthProvider({ children }: Props) { }) }, []) - // REGISTER + const updateEmail = useCallback( + async (phone: string, code: string, email: string, id: string) => { + const data = { + phone, + code, + email + } + await post(endpoints.dashboard.user.updateEmail(id), data) + + const updatedUser = { ...state.user, email } + dispatch({ + type: Types.UPDATE_EMAIL, + payload: { + user: updatedUser + } + }) + }, + [state] + ) const register = useCallback( async (email: string, password: string, firstName: string, lastName: string) => { @@ -250,7 +299,7 @@ export function AuthProvider({ children }: Props) { dispatch({ type: Types.UPDATE_USER, payload: { - user + user: updateUser } }) }, []) @@ -271,12 +320,25 @@ export function AuthProvider({ children }: Props) { // login, loginWithCode, - generateCode, + generate2faCodeLogin, + generate2faCodeEmail, + updateEmail, register, logout, updateUser }), - [generateCode, login, loginWithCode, logout, register, state.user, status, updateUser] + [ + login, + loginWithCode, + generate2faCodeLogin, + generate2faCodeEmail, + updateEmail, + register, + logout, + updateUser, + state.user, + status + ] ) return {children} diff --git a/src/auth/types.ts b/src/auth/types.ts index 8530a44..2ce3881 100644 --- a/src/auth/types.ts +++ b/src/auth/types.ts @@ -25,8 +25,10 @@ export type AuthStateType = { type CanRemove = { login?: (email: string, password: string) => Promise + generate2faCodeLogin?: (phone: string, codeMsg: string, recaptchaToken: string) => Promise + generate2faCodeEmail?: (id: string, phone: string, codeMsg: string) => Promise loginWithCode?: (phone: string, code: string, recaptchaToken: string) => Promise - generateCode?: (email: string, codeMsg: string, recaptchaToken: string) => Promise + updateEmail?: (phone: string, code: string, email: string, id: string) => Promise register?: (email: string, password: string, firstName: string, lastName: string) => Promise // loginWithGoogle?: () => Promise @@ -47,8 +49,10 @@ export type JWTContextType = CanRemove & { authenticated: boolean unauthenticated: boolean login: (email: string, password: string) => Promise - loginWithCode: (phone: string, code: string, recaptchaToken: string) => Promise - generateCode: (email: string, codeMsg: string, recaptchaToken: string) => Promise + loginWithCode: (phone: string, code: string, recaptchaToken: string, id: string) => Promise + updateEmail: (phone: string, code: string, email: string, id: string) => Promise + generate2faCodeLogin: (phone: string, codeMsg: string, recaptchaToken: string) => Promise + generate2faCodeEmail: (id: string, phone: string, codeMsg: string) => Promise register: (email: string, password: string, firstName: string, lastName: string) => Promise logout: () => Promise updateUser: (user: AuthUserType) => void From 68252b75115fe1c224061efe89acac6f08349989 Mon Sep 17 00:00:00 2001 From: dappsar Date: Sat, 19 Oct 2024 20:05:12 -0300 Subject: [PATCH 5/8] [feat] :globe_with_meridians: add i18n message for change-email (#128) --- src/locales/langs/br.json | 18 ++++++++++++++++-- src/locales/langs/en.json | 18 ++++++++++++++++-- src/locales/langs/es.json | 18 ++++++++++++++++-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/locales/langs/br.json b/src/locales/langs/br.json index 98c936a..d39d57b 100644 --- a/src/locales/langs/br.json +++ b/src/locales/langs/br.json @@ -14,6 +14,8 @@ "must-be-valid-email": "O email deve ser um endereço de email válido", "must-be-min": "Deve ter no mínimo {MIN_CHARS} caracteres", "nodata": "Sem Dados", + "save": "Salvar Alterações", + "cancel": "Cancelar", "close": "Fechar", "view": "Ver", "share": "Compartilhar", @@ -30,7 +32,8 @@ "management": "Gestão", "user": "Usuário", "account": "Conta", - "logout": "Sair" + "logout": "Sair", + "email": "Alterar Email" }, "terms": { "title": "Termos e Condições", @@ -324,6 +327,17 @@ }, "account": { "title": "Conta", - "save": "Salvar Alterações" + "change-email": "Alterar", + "email": { + "old": "Endereço de Email Antigo", + "new": "Novo Endereço de Email", + "new2": "Repita o Novo Endereço de Email", + "send-code": "Enviar código para o WhatsApp para validar", + "resend-code": "Reenviar código", + "code-sent": "Enviamos um código de 6 dígitos para esse número de telefone", + "code-info": "Por favor, insira o código de 6 dígitos que enviamos para o seu WhatsApp (pode demorar cerca de 2 minutos para chegar).", + "invalid-code": "Código inválido", + "code-bot": "Insira este código {2FA_CODE} para validar a alteração do seu e-mail no Chatterpay. É muito importante que você não compartilhe esta informação com ninguém. Se você não solicitou o código, desconsidere esta mensagem e entre em contato conosco para que possamos ajudá-lo!" + } } } diff --git a/src/locales/langs/en.json b/src/locales/langs/en.json index a4d6b34..14fd701 100644 --- a/src/locales/langs/en.json +++ b/src/locales/langs/en.json @@ -14,7 +14,9 @@ "must-be-valid-email": "Email must be a valid email address", "must-be-min": "Must have a minimum of {MIN_CHARS} chars", "nodata": "No Data", + "save": "Save Changes", "close": "Close", + "cancel": "Cancel", "view": "View", "share": "Share", "metadata": "View Metadata", @@ -30,7 +32,8 @@ "management": "Management", "user": "User", "account": "Account", - "logout": "Logout" + "logout": "Logout", + "email": "Email Change" }, "home": { "header": { @@ -324,6 +327,17 @@ }, "account": { "title": "Account", - "save": "Save Changes" + "change-email": "Change", + "email": { + "old": "Old Email Address", + "new": "New Email Address", + "new2": "Repeat New Email Address", + "send-code": "Send Code to Whatsapp to valiate", + "resend-code": "Resend Code", + "code-sent": "We sent a 6-digit code to the phone number", + "code-info": "Please enter the 6-digit code we sent to your WhatsApp (it may take around 2 minutes to arrive).", + "invalid-code": "Invalid code", + "code-bot": "Enter this code {2FA_CODE} to validate your email change in Chatterpay. It is very important that you do not share this information with anyone. If you did not request the code, disregard this message and contact us so we can help you!" + } } } \ No newline at end of file diff --git a/src/locales/langs/es.json b/src/locales/langs/es.json index f43c407..a9b06b4 100644 --- a/src/locales/langs/es.json +++ b/src/locales/langs/es.json @@ -15,6 +15,8 @@ "must-be-min": "Debe tener un mínimo de {MIN_CHARS} caracteres", "nodata": "Sin Datos", "close": "Cerrar", + "save": "Guardar Cambios", + "cancel": "Cancelar", "view": "Ver", "share": "Compartir", "metadata": "Ver Metadata", @@ -30,7 +32,8 @@ "management": "Gestión", "user": "Usuario", "account": "Cuenta", - "logout": "Cerrar Sesión" + "logout": "Cerrar Sesión", + "email": "Cambiar Correo" }, "terms": { "title": "Términos y Condiciones", @@ -325,6 +328,17 @@ }, "account": { "title": "Cuenta", - "save": "Guardar Cambios" + "change-email": "Cambiar", + "email": { + "old": "Dirección de Correo Electrónico Antiguo", + "new": "Nueva Dirección de Correo Electrónico", + "new2": "Repetir Nueva Dirección de Correo Electrónico", + "send-code": "Enviar código a whatsapp para validar", + "resend-code": "Reenviar código", + "code-sent": "Enviamos un código de 6 dígitos al teléfono", + "code-info": "Por favor, ingresa el código de 6 dígitos que enviamos a tu WhatsApp (puede demorarse alrededor de 2 minutos en llegar).", + "invalid-code": "Código inválido", + "code-bot": "Ingresá este código *{2FA_CODE}* para validar tu cambio de correo electrónico en Chatterpay. Es muy importante que no compartas esta información con nadie. Si no solicitaste el código, desestimá este mensaje y contactate con nosotros para que podamos ayudarte!" + } } } From 5a57bdb1635b9278f24f145b180e98d4f63cc3ce Mon Sep 17 00:00:00 2001 From: dappsar Date: Sat, 19 Oct 2024 20:06:04 -0300 Subject: [PATCH 6/8] [feat] :sparkles: update user context with change-email funcitons (#128) --- src/sections/auth/jwt/jwt-login-view.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sections/auth/jwt/jwt-login-view.tsx b/src/sections/auth/jwt/jwt-login-view.tsx index e7c9198..99f6ca6 100644 --- a/src/sections/auth/jwt/jwt-login-view.tsx +++ b/src/sections/auth/jwt/jwt-login-view.tsx @@ -36,7 +36,7 @@ import FormProvider, { RHFCode, RHFTextField } from 'src/components/hook-form' export default function JwtLoginView() { const { t } = useTranslate() - const { generateCode, loginWithCode } = useAuthContext() + const { generate2faCodeLogin, loginWithCode } = useAuthContext() const router = useRouter() const [errorMsg, setErrorMsg] = useState('') const [codeSent, setCodeSent] = useState(false) @@ -93,7 +93,7 @@ export default function JwtLoginView() { startCountdown() setErrorMsg('') setValue('code', '') - await generateCode?.( + await generate2faCodeLogin?.( `${selectedCountry}${data.phone}`, t('login.msg.code-bot'), recaptchaToken || '' @@ -111,7 +111,7 @@ export default function JwtLoginView() { } } }, - [enqueueSnackbar, generateCode, phone, selectedCountry, setValue, startCountdown, t] + [enqueueSnackbar, generate2faCodeLogin, phone, selectedCountry, setValue, startCountdown, t] ) const onSubmit = handleSubmit(async (data) => { From eff8444d35495c2304d2e20b9a61d7228fc2e8c0 Mon Sep 17 00:00:00 2001 From: dappsar Date: Sat, 19 Oct 2024 20:06:55 -0300 Subject: [PATCH 7/8] [feat] :sparkles: add sections to allow change-email (#128) --- src/routes/paths.ts | 3 +- src/sections/account/account-general.tsx | 25 +- src/sections/account/change-email.tsx | 216 ++++++++++++++++++ .../account/view/account-edit-view.tsx | 35 +-- src/sections/account/view/email-edit-view.tsx | 38 +++ src/sections/account/view/index.ts | 1 + 6 files changed, 278 insertions(+), 40 deletions(-) create mode 100644 src/sections/account/change-email.tsx create mode 100644 src/sections/account/view/email-edit-view.tsx diff --git a/src/routes/paths.ts b/src/routes/paths.ts index ecbfb2c..fb5ec8d 100644 --- a/src/routes/paths.ts +++ b/src/routes/paths.ts @@ -25,9 +25,8 @@ export const paths = { }, user: { root: `${ROOTS.DASHBOARD}/user/account`, - list: `${ROOTS.DASHBOARD}/user/list`, - profile: `${ROOTS.DASHBOARD}/user/profile`, account: `${ROOTS.DASHBOARD}/user/account`, + email: `${ROOTS.DASHBOARD}/user/account/email`, wallet: { root: (walletId: string) => `${ROOTS.DASHBOARD}/user/wallet/${walletId}`, nfts: { diff --git a/src/sections/account/account-general.tsx b/src/sections/account/account-general.tsx index 8669aed..a080236 100644 --- a/src/sections/account/account-general.tsx +++ b/src/sections/account/account-general.tsx @@ -6,9 +6,13 @@ import { yupResolver } from '@hookform/resolvers/yup' import Box from '@mui/material/Box' import Card from '@mui/material/Card' import Stack from '@mui/material/Stack' +import Button from '@mui/material/Button' import Grid from '@mui/material/Unstable_Grid2' import LoadingButton from '@mui/lab/LoadingButton' +import { paths } from 'src/routes/paths' +import { useRouter } from 'src/routes/hooks' + import { useTranslate } from 'src/locales' import { useAuthContext } from 'src/auth/hooks' import { updateContact } from 'src/app/api/_hooks/use-contact' @@ -32,6 +36,7 @@ export default function AccountGeneral() { const { enqueueSnackbar } = useSnackbar() const { t } = useTranslate() const { user, updateUser } = useAuthContext() + const router = useRouter() const UpdateUserSchema = Yup.object().shape({ displayName: Yup.string() @@ -66,6 +71,8 @@ export default function AccountGeneral() { formState: { isSubmitting } } = methods + // ---------------------------------------------------------------------- + const onSubmit = handleSubmit(async (data) => { const confirmMsg = t('common.msg.update-success') const errorMsg = t('common.msg.update-error') @@ -82,19 +89,23 @@ export default function AccountGeneral() { } await updateContact(user!.id, userData) - // TODO: Actualizar el contexto (para que se actualice el form) updateUser({ ...user, // Mantiene los campos que no cambian displayName: formData.displayName }) - console.info('DATA', data) enqueueSnackbar(confirmMsg) } catch (error) { enqueueSnackbar(errorMsg, { variant: 'error' }) } }) + const handleChangeEmail = () => { + router.push(paths.dashboard.user.email) + } + + // ---------------------------------------------------------------------- + return ( @@ -110,14 +121,20 @@ export default function AccountGeneral() { }} > - + + + + + - {t('account.save')} + {t('common.save')} diff --git a/src/sections/account/change-email.tsx b/src/sections/account/change-email.tsx new file mode 100644 index 0000000..23cac85 --- /dev/null +++ b/src/sections/account/change-email.tsx @@ -0,0 +1,216 @@ +import * as Yup from 'yup' +import { useForm } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers/yup' +import { useMemo, useState, useEffect, useCallback } from 'react' + +import Box from '@mui/material/Box' +import Card from '@mui/material/Card' +import Stack from '@mui/material/Stack' +import Alert from '@mui/material/Alert' +import Button from '@mui/material/Button' +import Grid from '@mui/material/Unstable_Grid2' +import LoadingButton from '@mui/lab/LoadingButton' + +import { paths } from 'src/routes/paths' +import { useRouter } from 'src/routes/hooks' + +import { useCountdownSeconds } from 'src/hooks/use-countdown' + +import { useTranslate } from 'src/locales' +import { useAuthContext } from 'src/auth/hooks' + +import { useSnackbar } from 'src/components/snackbar' +import FormProvider, { RHFCode, RHFTextField } from 'src/components/hook-form' + +import { IAccount } from 'src/types/account' + +// ---------------------------------------------------------------------- + +export default function ChangeEmail() { + const { enqueueSnackbar } = useSnackbar() + const { t } = useTranslate() + const { generate2faCodeEmail, updateEmail } = useAuthContext() + const { user } = useAuthContext() + const router = useRouter() + + const [codeSent, setCodeSent] = useState(false) + const [errorMsg, setErrorMsg] = useState('') + const [contextUser, setContextUser] = useState(null) + + const { counting, countdown, startCountdown } = useCountdownSeconds(60) + + const ChangeEmailSchema = Yup.object().shape({ + oldEmail: Yup.string().email(t('common.must-be-valid-email')).required(t('common.required')), + newEmail: Yup.string().email(t('common.must-be-valid-email')).required(t('common.required')), + confirmEmail: Yup.string() + .oneOf([Yup.ref('newEmail')], t('common.emails-must-match')) + .required(t('common.required')), + // @ts-ignore + code: Yup.string().when('codeSent', { + is: true, + then: Yup.string() + .matches(/^[0-9]{6}$/, t('common.must-be-numeric')) + .required(t('common.required')) + }) + }) + + const defaultValues = useMemo( + () => ({ + oldEmail: user?.email || '', + newEmail: '', + confirmEmail: '', + code: '' + }), + [user?.email] + ) + + const methods = useForm({ + // @ts-ignore + resolver: yupResolver(ChangeEmailSchema), + defaultValues + }) + + const { + watch, + setValue, + handleSubmit, + formState: { isSubmitting, isValid } + } = methods + + const newEmail = watch('newEmail') + const confirmEmail = watch('confirmEmail') + const codeValue = watch('code') + + // ---------------------------------------------------------------------- + + // guardar el telefono en estado, por cambios en el contexdto del user + useEffect(() => { + if (!user) return + // @ts-ignore + setContextUser(user) + }, [user]) + + const handleSendCode = useCallback(async () => { + try { + startCountdown() + setErrorMsg('') + + // @ts-ignore + await generate2faCodeEmail?.( + contextUser!.id, + // @ts-ignore + contextUser!.phoneNumber, + t('account.email.code-bot') + ) + + setCodeSent(true) + setValue('code', '') + enqueueSnackbar(`${t('account.email.code-sent')} ${contextUser}`, { + variant: 'info' + }) + } catch (ex) { + console.error(ex) + if (typeof ex === 'string') { + setErrorMsg(ex) + } else if (ex.code === 'USER_NOT_FOUND') { + setErrorMsg(t('login.msg.invalid-user')) + } else { + setErrorMsg(ex.error) + } + } + }, [startCountdown, contextUser, generate2faCodeEmail, t, setValue, enqueueSnackbar]) + + const handleCancel = () => { + router.push(paths.dashboard.user.account) + } + + const onSubmit = async (data: any) => { + try { + // @ts-ignore + await updateEmail?.(contextUser!.phoneNumber, data.code || '', confirmEmail, contextUser!.id) + router.push(paths.dashboard.user.account) + setErrorMsg('') + enqueueSnackbar(t('common.msg.update-success')) + } catch (ex) { + console.error(ex) + if (typeof ex === 'string') { + setErrorMsg(ex) + } else if (ex.code === 'USER_NOT_FOUND') { + setErrorMsg(t('login.msg.invalid-user')) + } else if (ex.code === 'INVALID_CODE') { + setErrorMsg(t('account.email.invalid-code')) + } else { + setErrorMsg(ex.error) + } + } + } + + const isSendCodeDisabled = !newEmail || !confirmEmail || newEmail !== confirmEmail || counting + const isSaveDisabled = !codeSent || !codeValue || codeValue.length !== 6 || !isValid + + // ---------------------------------------------------------------------- + + const renderForm = ( + + + + + + + + + {codeSent && ( + <> + {t('account.email.code-info')} + + + )} + + {errorMsg && {errorMsg}} + + + + {counting + ? `${t('account.email.resend-code')} (${countdown}s)` + : t('account.email.send-code')} + + + + + + + + {t('common.save')} + + + + + + ) + + return ( + + {renderForm} + + ) +} diff --git a/src/sections/account/view/account-edit-view.tsx b/src/sections/account/view/account-edit-view.tsx index f83be7a..7dc8bfa 100644 --- a/src/sections/account/view/account-edit-view.tsx +++ b/src/sections/account/view/account-edit-view.tsx @@ -1,16 +1,11 @@ 'use client' -import { useState, useCallback } from 'react' - -import Tab from '@mui/material/Tab' -import Tabs from '@mui/material/Tabs' import Container from '@mui/material/Container' import { paths } from 'src/routes/paths' import { useTranslate } from 'src/locales' -import Iconify from 'src/components/iconify' import { useSettingsContext } from 'src/components/settings' import CustomBreadcrumbs from 'src/components/custom-breadcrumbs' @@ -18,24 +13,9 @@ import AccountGeneral from '../account-general' // ---------------------------------------------------------------------- -const TABS = [ - { - value: 'general', - label: 'General', - icon: - } -] - -// ---------------------------------------------------------------------- - export default function AccountEditView() { const settings = useSettingsContext() const { t } = useTranslate() - const [currentTab, setCurrentTab] = useState('general') - - const handleChangeTab = useCallback((event: React.SyntheticEvent, newValue: string) => { - setCurrentTab(newValue) - }, []) return ( @@ -50,20 +30,7 @@ export default function AccountEditView() { mb: { xs: 3, md: 5 } }} /> - - - {TABS.map((tab) => ( - - ))} - - - {currentTab === 'general' && } + ) } diff --git a/src/sections/account/view/email-edit-view.tsx b/src/sections/account/view/email-edit-view.tsx new file mode 100644 index 0000000..8e453dc --- /dev/null +++ b/src/sections/account/view/email-edit-view.tsx @@ -0,0 +1,38 @@ +'use client' + +import Container from '@mui/material/Container' + +import { paths } from 'src/routes/paths' + +import { useTranslate } from 'src/locales' + +import { useSettingsContext } from 'src/components/settings' +import CustomBreadcrumbs from 'src/components/custom-breadcrumbs' + +import ChangeEmail from '../change-email' + +// ---------------------------------------------------------------------- + +export default function EmailEditView() { + const settings = useSettingsContext() + const { t } = useTranslate() + + return ( + + + + + + ) +} diff --git a/src/sections/account/view/index.ts b/src/sections/account/view/index.ts index c828d75..06e71bb 100644 --- a/src/sections/account/view/index.ts +++ b/src/sections/account/view/index.ts @@ -1 +1,2 @@ +export { default as EmailEditView } from './email-edit-view' export { default as AccountEditView } from './account-edit-view' From 727bc10eb1c6f65f0eacefd351e74aebdedf1330 Mon Sep 17 00:00:00 2001 From: dappsar Date: Sat, 19 Oct 2024 20:07:18 -0300 Subject: [PATCH 8/8] [chore] :wastebasket: remove deprecated code --- .../user/wallet/[walletId]/nfts/[nftId]/page_ | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/app/dashboard/user/wallet/[walletId]/nfts/[nftId]/page_ diff --git a/src/app/dashboard/user/wallet/[walletId]/nfts/[nftId]/page_ b/src/app/dashboard/user/wallet/[walletId]/nfts/[nftId]/page_ deleted file mode 100644 index 3f0c136..0000000 --- a/src/app/dashboard/user/wallet/[walletId]/nfts/[nftId]/page_ +++ /dev/null @@ -1,25 +0,0 @@ -/* -'use client' - -import { useParams } from 'next/navigation' - -import { NftView } from 'src/sections/nfts/view' -*/ - -// ---------------------------------------------------------------------- - -/* -export const metadata = { - title: 'user - NFTs' -} -*/ -/* -export default function NftIdPage() { - const params = useParams() - - // Asegurar que los parámetros sean string - const walletId = Array.isArray(params.walletId) ? params.walletId[0] : params.walletId - const nftId = Array.isArray(params.nftId) ? params.nftId[0] : params.nftId - return -} -*/ \ No newline at end of file