diff --git a/apps/consent/app/components/captcha-challenge/index.tsx b/apps/consent/app/components/captcha-challenge/index.tsx index c75527b29b..455c26f9af 100644 --- a/apps/consent/app/components/captcha-challenge/index.tsx +++ b/apps/consent/app/components/captcha-challenge/index.tsx @@ -18,6 +18,7 @@ const CaptchaChallengeComponent: React.FC<{ login_challenge: string phone: string remember: string + channel: string } }> = ({ id, challenge, formData }) => { const captchaHandler = useCallback( diff --git a/apps/consent/app/components/select.tsx b/apps/consent/app/components/select.tsx new file mode 100644 index 0000000000..cf56d517cf --- /dev/null +++ b/apps/consent/app/components/select.tsx @@ -0,0 +1,47 @@ +import React, { SelectHTMLAttributes } from "react" + +interface SelectProps extends SelectHTMLAttributes { + label?: string + id: string + options: string[] +} + +const SelectComponent: React.FC = ({ + label, + id, + options, + ...selectProps +}) => { + return ( +
+ {label ? ( + + ) : null} + +
+ ) +} + +export default SelectComponent diff --git a/apps/consent/app/error-handler.ts b/apps/consent/app/error-handler.ts new file mode 100644 index 0000000000..db1a4574db --- /dev/null +++ b/apps/consent/app/error-handler.ts @@ -0,0 +1,37 @@ +import axios from "axios" + +interface ErrorResponse { + error: boolean + message: string + responsePayload: null +} + +const errorMessages: { [key: string]: string } = { + UserCodeAttemptIpRateLimiterExceededError: + "Your rate limit exceeded, please try after some time.", + UserCodeAttemptIdentifierRateLimiterExceededError: + "Your rate limit exceeded, please try after some time.", +} + +export const handleAxiosError = (err: unknown): ErrorResponse => { + if (axios.isAxiosError(err) && err.response) { + const errorCode = err.response?.data?.error?.name + const errorMessage = + errorCode && Object.prototype.hasOwnProperty.call(errorMessages, errorCode) + ? errorMessages[errorCode as keyof typeof errorMessages] + : errorCode || err?.response?.data?.error + console.error("Error:", errorMessage) + return { + error: true, + message: errorMessage, + responsePayload: null, + } + } + + console.error("An unknown error occurred", err) + return { + error: true, + message: "An unknown error occurred", + responsePayload: null, + } +} diff --git a/apps/consent/app/login/email-login-form.tsx b/apps/consent/app/login/email-login-form.tsx new file mode 100644 index 0000000000..34632f2130 --- /dev/null +++ b/apps/consent/app/login/email-login-form.tsx @@ -0,0 +1,92 @@ +"use client" +import React from "react" +/* eslint @typescript-eslint/ban-ts-comment: "off" */ +// @ts-ignore-next-line error +import { experimental_useFormState as useFormState } from "react-dom" +import Link from "next/link" +import { toast } from "react-toastify" + +import InputComponent from "../components/input-component" +import FormComponent from "../components/form-component" +import Separator from "../components/separator" +import PrimaryButton from "../components/button/primary-button-component" +import SecondaryButton from "../components/button/secondary-button-component" +import { SubmitValue } from "../index.types" + +import { submitForm } from "./email-login-server-action" +interface LoginProps { + login_challenge: string +} + +const EmailLoginForm = ({ login_challenge }: LoginProps) => { + const [state, formAction] = useFormState(submitForm, { + error: null, + message: null, + }) + + if (state.error) { + toast.error(state.message) + state.error = null + } + + return ( + + + +
+ +
+ or +
+
+ +

Sign in with phone

+ +
+
+
+ + Next + + + Cancel + +
+
+ ) +} +export default EmailLoginForm diff --git a/apps/consent/app/login/email-login-server-action.ts b/apps/consent/app/login/email-login-server-action.ts new file mode 100644 index 0000000000..3d0adf0fbb --- /dev/null +++ b/apps/consent/app/login/email-login-server-action.ts @@ -0,0 +1,94 @@ +"use server" +import { redirect } from "next/navigation" +import { cookies, headers } from "next/headers" + +import { hydraClient } from "../../services/hydra" +import { LoginType, SubmitValue } from "../index.types" + +import { LoginEmailResponse } from "./email-login.types" + +import authApi from "@/services/galoy-auth" +import { handleAxiosError } from "@/app/error-handler" + +export async function submitForm( + _prevState: unknown, + formData: FormData, +): Promise { + const headersList = headers() + const customHeaders = { + "x-real-ip": headersList.get("x-real-ip"), + "x-forwarded-for": headersList.get("x-forwarded-for"), + } + + const login_challenge = formData.get("login_challenge") + const submitValue = formData.get("submit") + const email = formData.get("email") + const remember = String(formData.get("remember") === "1") + if ( + !login_challenge || + !submitValue || + !remember || + typeof login_challenge !== "string" || + typeof submitValue !== "string" + ) { + throw new Error("Invalid Value") + } + + if (submitValue === SubmitValue.denyAccess) { + console.log("User denied access") + const response = await hydraClient.rejectOAuth2LoginRequest( + { + loginChallenge: login_challenge, + rejectOAuth2Request: { + error: "access_denied", + error_description: "The resource owner denied the request", + }, + }, + { + headers: { + Cookie: cookies().toString(), + }, + }, + ) + redirect(response.data.redirect_to) + } + + if (!email || typeof email !== "string") { + console.error("Invalid Values for email") + throw new Error("Invalid Email Value") + } + + let emailCodeRequest: string | null + try { + emailCodeRequest = await authApi.requestEmailCode(email, customHeaders) + } catch (err) { + console.error("Error in requestEmailCode", err) + return handleAxiosError(err) + } + + // TODO: manage error on ip rate limit + // TODO: manage error when trying the same email too often + if (!emailCodeRequest) { + return { + error: true, + message: "Internal Server Error", + responsePayload: null, + } + } + + cookies().set( + login_challenge, + JSON.stringify({ + loginType: LoginType.email, + loginId: emailCodeRequest, + value: email, + remember, + }), + { secure: true }, + ) + + const params = new URLSearchParams({ + login_challenge, + }) + redirect(`/login/verification?${params}`) +} diff --git a/apps/consent/app/login/page.tsx b/apps/consent/app/login/page.tsx index 988f55988b..cb5219df3c 100644 --- a/apps/consent/app/login/page.tsx +++ b/apps/consent/app/login/page.tsx @@ -1,109 +1,21 @@ import { redirect } from "next/navigation" import React from "react" -import Link from "next/link" -import { headers, cookies } from "next/headers" +import { cookies } from "next/headers" import { hydraClient } from "../../services/hydra" -import InputComponent from "../components/input-component" -import Card from "../components/card" import MainContent from "../components/main-container" import Logo from "../components/logo" import Heading from "../components/heading" import SubHeading from "../components/sub-heading" -import FormComponent from "../components/form-component" -import Separator from "../components/separator" -import PrimaryButton from "../components/button/primary-button-component" -import SecondaryButton from "../components/button/secondary-button-component" -import { LoginType, SubmitValue } from "../index.types" -import { LoginEmailResponse } from "./email-login.types" +import Card from "../components/card" -import authApi from "@/services/galoy-auth" +import EmailLoginForm from "./email-login-form" -// this page is for login via email interface LoginProps { login_challenge: string } -async function submitForm(formData: FormData): Promise { - "use server" - - const headersList = headers() - const customHeaders = { - "x-real-ip": headersList.get("x-real-ip"), - "x-forwarded-for": headersList.get("x-forwarded-for"), - } - - const login_challenge = formData.get("login_challenge") - const submitValue = formData.get("submit") - const email = formData.get("email") - const remember = String(formData.get("remember") === "1") - - if ( - !login_challenge || - !submitValue || - !remember || - typeof login_challenge !== "string" || - typeof submitValue !== "string" - ) { - throw new Error("Invalid Value") - } - - if (submitValue === SubmitValue.denyAccess) { - console.log("User denied access") - const response = await hydraClient.rejectOAuth2LoginRequest( - { - loginChallenge: login_challenge, - rejectOAuth2Request: { - error: "access_denied", - error_description: "The resource owner denied the request", - }, - }, - { - headers: { - Cookie: cookies().toString(), - }, - }, - ) - redirect(response.data.redirect_to) - } - - if (!email || typeof email !== "string") { - console.error("Invalid Values for email") - throw new Error("Invalid Email Value") - } - - let emailCodeRequest - try { - emailCodeRequest = await authApi.requestEmailCode(email, customHeaders) - } catch (err) { - console.error("error while calling emailRequest Code", err) - } - - if (!emailCodeRequest) { - throw new Error("Request failed to get email code") - } - - // TODO: manage error on ip rate limit - // TODO: manage error when trying the same email too often - - cookies().set( - login_challenge, - JSON.stringify({ - loginType: LoginType.email, - loginId: emailCodeRequest, - value: email, - remember, - }), - { secure: true }, - ) - - const params = new URLSearchParams({ - login_challenge, - }) - redirect(`/login/verification?${params}`) -} - const Login = async ({ searchParams }: { searchParams: LoginProps }) => { const { login_challenge } = searchParams @@ -147,63 +59,7 @@ const Login = async ({ searchParams }: { searchParams: LoginProps }) => { Enter your Blink Account ID to sign in to this application. - - - -
- -
- or -
-
- -

Sign in with phone

- -
-
-
- - Next - - - Cancel - -
-
+ ) diff --git a/apps/consent/app/login/phone/form.tsx b/apps/consent/app/login/phone/form.tsx index 6b565df215..80710dd2e6 100644 --- a/apps/consent/app/login/phone/form.tsx +++ b/apps/consent/app/login/phone/form.tsx @@ -29,6 +29,8 @@ import "react-phone-number-input/style.css" // eslint-disable-next-line import/no-unassigned-import import "./phone-input-styles.css" +import SelectComponent from "@/app/components/select" + interface LoginFormProps { login_challenge: string // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -51,6 +53,7 @@ const LoginForm: React.FC = ({ login_challenge, countryCodes }) login_challenge: null, phone: null, remember: null, + channel: null, }, }, }, @@ -98,6 +101,12 @@ const LoginForm: React.FC = ({ login_challenge, countryCodes }) name="phone" onChange={handlePhoneNumberChange} /> +