Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: error handling, dark mode, consent bug fixes #3408

Closed
wants to merge 12 commits into from
Closed
1 change: 1 addition & 0 deletions apps/consent/app/components/captcha-challenge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const CaptchaChallengeComponent: React.FC<{
login_challenge: string
phone: string
remember: string
channel: string
}
}> = ({ id, challenge, formData }) => {
const captchaHandler = useCallback(
Expand Down
47 changes: 47 additions & 0 deletions apps/consent/app/components/select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { SelectHTMLAttributes } from "react"

interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
label?: string
id: string
options: string[]
}

const SelectComponent: React.FC<SelectProps> = ({
label,
id,
options,
...selectProps
}) => {
return (
<div className="mb-4">
{label ? (
<label
htmlFor={id}
className="block mb-2 text-sm font-medium text-[var(--inputColor)]"
>
{label}
</label>
) : null}
<select
{...selectProps}
id={id}
className="p-2
border-1
border-solid
border-gray-300
rounded-sm
w-full
bg-[var(--inputBackground)]
focus:border-blue-500 "
>
{options.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
</div>
)
}

export default SelectComponent
37 changes: 37 additions & 0 deletions apps/consent/app/error-handler.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
92 changes: 92 additions & 0 deletions apps/consent/app/login/email-login-form.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<FormComponent action={formAction}>
<input type="hidden" name="login_challenge" value={login_challenge} />
<InputComponent
data-testid="email_id_input"
label="Email"
type="email"
id="email"
name="email"
required
placeholder="Email Id"
/>
<div className="flex items-center mb-4">
<label className="text-[var(--inputColor)] text-sm flex items-center">
<input
type="checkbox"
id="remember"
name="remember"
value="1"
className="mr-2"
style={{ width: "14px", height: "14px" }}
/>
Remember me
</label>
</div>
<Separator>or</Separator>
<div className="flex justify-center mb-4">
<div className="text-center text-sm w-60">
<Link
data-testid="sign_in_with_phone_text"
href={`/login/phone?login_challenge=${login_challenge}`}
replace
>
<p className="font-semibold text-sm">Sign in with phone</p>
</Link>
</div>
</div>
<div className="flex flex-col md:flex-row-reverse w-full gap-2">
<PrimaryButton
type="submit"
id="accept"
name="submit"
value="Log in"
data-testid="email_login_next_btn"
>
Next
</PrimaryButton>
<SecondaryButton
type="button"
id="reject"
name="submit"
value={SubmitValue.denyAccess}
formNoValidate
>
Cancel
</SecondaryButton>
</div>
</FormComponent>
)
}
export default EmailLoginForm
94 changes: 94 additions & 0 deletions apps/consent/app/login/email-login-server-action.ts
Original file line number Diff line number Diff line change
@@ -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<LoginEmailResponse | void> {
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}`)
}
Loading
Loading