diff --git a/app/(account)/confirm-email/[token]/actions.ts b/app/(account)/confirm-email/[token]/actions.ts new file mode 100644 index 0000000..d555853 --- /dev/null +++ b/app/(account)/confirm-email/[token]/actions.ts @@ -0,0 +1,28 @@ +"use server" + +import {isFieldError} from "../../../../lib/auth/guards"; + +export const postMailConfirmationToken = async (token: string): Promise => { + try { + const response = await fetch(`${process.env.CC_API_URL}/accounts/confirm-account`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({token}), + }); + + if (!response.ok) { + const errorResponse = await response.json(); + if (isFieldError(errorResponse) && errorResponse.error.token) { + return {error: errorResponse.error.token.join(' ')}; + } + + return {error: "An unexpected error occurred."}; + } + + return {success: true}; + } catch (error) { + return {error: "An unexpected error occurred."}; + } +}; diff --git a/app/confirm-email/[token]/page.tsx b/app/(account)/confirm-email/[token]/page.tsx similarity index 84% rename from app/confirm-email/[token]/page.tsx rename to app/(account)/confirm-email/[token]/page.tsx index 771a6de..acb087a 100644 --- a/app/confirm-email/[token]/page.tsx +++ b/app/(account)/confirm-email/[token]/page.tsx @@ -1,13 +1,14 @@ "use client" -import {usePathname} from "next/navigation"; +import {useParams} from "next/navigation"; import {postMailConfirmationToken} from "./actions"; import React, {useEffect, useState} from "react"; -import Panel from "../../common/uiLibrary/panel"; -import ContactInformation from "../../(home)/contactInformation"; +import Panel from "../../../common/uiLibrary/panel"; +import ContactInformation from "../../../(home)/contactInformation"; + const MailConfirmationPage = () => { const [response, setResponse] = useState<{ success?: boolean; error?: string }>({}); - const token = usePathname().split('/').filter(Boolean).pop(); + const {token} = useParams<{token: string}>(); useEffect(() => { const fetchData = async () => { diff --git a/app/(account)/login/page.tsx b/app/(account)/login/page.tsx index bde0fa6..9204921 100644 --- a/app/(account)/login/page.tsx +++ b/app/(account)/login/page.tsx @@ -11,6 +11,7 @@ import {AuthorizerContext} from "../../../context/AuthorizerContextProvider"; import {isFieldError, isGeneralError} from "../../../lib/auth/guards"; import GenericLoading from "../../common/uiLibrary/genericLoading"; import {redirect} from "next/navigation"; +import FullPage from "../../common/uiLibrary/Layouters/fullPage"; const LoginPage = () => { const {state, credentialsLogin} = useContext(AuthorizerContext); @@ -41,7 +42,7 @@ const LoginPage = () => { const loginForm = () => { return ( -
+
{isLoading &&
@@ -89,14 +90,15 @@ const LoginPage = () => { links={ [ {link: '/register', linkText: 'Don\'t have an account?'}, - {link: '/reset-password', linkText: 'Forgot your password?'} + {link: '/reset-password', linkText: 'Forgot your password?'}, + {link: '/resend-confirm-email', linkText: 'Haven\'t received confirmation email yet?'}, ] } />
-
+
) } diff --git a/app/(account)/register/page.tsx b/app/(account)/register/page.tsx index 2060643..f318007 100644 --- a/app/(account)/register/page.tsx +++ b/app/(account)/register/page.tsx @@ -108,12 +108,10 @@ const RegisterPage = () => { placeholder='********' shadow required - helperText={(state.error && isFieldError(state.error) && state.error?.error.password2) ? + helperText={(state.error && isFieldError(state.error) && state.error?.error.password2) &&
{state.error?.error.password2}
- : - usernameHelperText() } /> diff --git a/app/(account)/resend-confirm-email/actions.ts b/app/(account)/resend-confirm-email/actions.ts new file mode 100644 index 0000000..b27851b --- /dev/null +++ b/app/(account)/resend-confirm-email/actions.ts @@ -0,0 +1,53 @@ +"use server" + +import {z} from 'zod'; +import {FieldError, GeneralError, isFieldError} from "../../../lib/auth/guards"; + +export interface ResendConfirmationMailRequest { + success: boolean; + error?: GeneralError | FieldError; +} + +export const postResendConfirmationMail = async (_: ResendConfirmationMailRequest, formData: FormData): Promise => { + const schema = z.object({ + email: z.string().email() + }); + + const parsed = schema.safeParse({ + email: formData.get('email') + }); + + if (!parsed.success) { + const fieldErrors = parsed.error.errors.reduce((acc, error) => { + return {...acc, [error.path[0]]: error.message} + }, {}); + + return {success: false, error: {status: 400, error: fieldErrors}}; + } + + try { + const response = await fetch(`${process.env.CC_API_URL}/accounts/resend-account-confirmation`, { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: parsed.data.email + }) + }); + + if (!response.ok) { + const error = await response.json(); + if (isFieldError(error)) { + return {success: false, error: error}; + } + + return {success: false, error: {error: 'An unexpected error happened', status: 500}}; + } + + } catch (e) { + return {success: false, error: {error: 'An unexpected error happened', status: 500}}; + } + + return {success: true} +} \ No newline at end of file diff --git a/app/(account)/resend-confirm-email/page.tsx b/app/(account)/resend-confirm-email/page.tsx new file mode 100644 index 0000000..86f6777 --- /dev/null +++ b/app/(account)/resend-confirm-email/page.tsx @@ -0,0 +1,70 @@ +"use client" + + +import FormContainer from "../../common/uiLibrary/Layouters/formContainer"; +import TextField from "../../common/uiLibrary/forms/textField"; +import Button from "../../common/uiLibrary/Button"; +import ContactInformation from "../../(home)/contactInformation"; +import React from "react"; +import FullPage from "../../common/uiLibrary/Layouters/fullPage"; +import {useFormState} from "react-dom"; +import {postResendConfirmationMail, ResendConfirmationMailRequest} from "./actions"; + +const ResendConfirmationMail = () => { + const initialState: ResendConfirmationMailRequest = { + success: false, + } + + const [state, formAction] = useFormState(postResendConfirmationMail, initialState); + + const resendForm = () => { + return ( + <> + {!state.success && state.error && errorMessage()} + + + + + ) + } + const errorMessage = () => { + return ( +
+

Oops!

+

There was an unexpected error while trying to resend the confirmation email.

+

Please try again later or contact us.

+
+ ) + } + + const successMessage = () => ( +
+

Success!

+

Your request has been processed successfully. If your account is found in our system and the email + address you provided matches our records, we have sent a confirmation email to that address.

+

Please check your inbox for the confirmation email. If you don't receive it within a few minutes, check + your spam or junk folder. For further assistance, don't hesitate to contact us.

+
+ ); + + return ( + +
+ + {state.success ? successMessage() : resendForm()} + +
+ +
+ ) +}; + +export default ResendConfirmationMail; \ No newline at end of file diff --git a/app/(account)/reset-password/[token]/actions.ts b/app/(account)/reset-password/[token]/actions.ts new file mode 100644 index 0000000..f233447 --- /dev/null +++ b/app/(account)/reset-password/[token]/actions.ts @@ -0,0 +1,75 @@ +"use server" + +import {FieldError, GeneralError, isFieldError, isGeneralError} from "../../../../lib/auth/guards"; +import {z} from "zod"; + +export interface ResetPasswordStep2Response { + success: boolean; + error?: GeneralError | FieldError; +} + +export const postPasswordReset = async (_: ResetPasswordStep2Response, formData: FormData): Promise => { + const schema = z.object({ + password: z.string().min(6), + token: z.string().min(1) + }); + + const parsed = schema.safeParse({ + password: formData.get('password'), + password2: formData.get('password2'), + token: formData.get('token') + }); + + if (!parsed.success) { + const fieldErrors = parsed.error.errors.reduce((acc, error) => { + return {...acc, [error.path[0]]: error.message} + }, {}); + + return {success: false, error: {status: 400, error: fieldErrors}}; + } + + const password2 = formData.get('password2'); + if (password2 !== parsed.data.password) { + return { + success: false, + error: + { + status: 400, + error: { + password2: ['Passwords do not match'] + } + } + } + } + + const body = { + password: parsed.data.password, + token: parsed.data.token + } + + try { + const response = await fetch(`${process.env.CC_API_URL}/accounts/reset-password/${parsed.data.token}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + if (isFieldError(response) && response.error.token) { + return {success: false, error: {status: 400, error: "Your link has expired. Please request a new one."}}; + } + + if (isFieldError(response) || isGeneralError(response)) { + return {success: false, error: response}; + } + + return { success: false, error: {status: 400, error: "An unexpected error occurred"}}; + } + + return { success: true}; + } catch (error) { + return { success: false, error: {status: 400, error: "An unexpected error occurred"}}; + } +} \ No newline at end of file diff --git a/app/(account)/reset-password/[token]/page.tsx b/app/(account)/reset-password/[token]/page.tsx new file mode 100644 index 0000000..b52e520 --- /dev/null +++ b/app/(account)/reset-password/[token]/page.tsx @@ -0,0 +1,93 @@ +"use client" + + +import {useParams} from "next/navigation"; +import React from "react"; +import FormContainer from "../../../common/uiLibrary/Layouters/formContainer"; +import TextField from "../../../common/uiLibrary/forms/textField"; +import Button from "../../../common/uiLibrary/Button"; +import {useFormState} from "react-dom"; +import {postPasswordReset, ResetPasswordStep2Response} from "./actions"; +import {isFieldError} from "../../../../lib/auth/guards"; +import FullPage from "../../../common/uiLibrary/Layouters/fullPage"; + +const ResetPasswordPageStep2 = () => { + const {token} = useParams<{ token: string }>(); + const initialState: ResetPasswordStep2Response = { + success: false, + error: undefined + } + + const [state, formAction] = useFormState(postPasswordReset, initialState); + + const successMessage = () => ( +
+

Success!

+

Your password has been reset successfully.

+

You can now log in using your new password.

+
+ ); + + const errorMessage = () => { + return ( +
+

Oops!

+

There was an error while trying to reset your password. Your password-reset token might be invalid or expired.

+

Please try requesting a new password reset or contact us.

+
+ ) + } + + const resetPasswordForm = () => { + return ( + <> + {state.error && errorMessage()} + + {state.error?.error.password} +
+ } + /> + + + {state.error?.error.password2} + + } + /> + + + + + + ) + } + + return ( + +
+ + {state.success ? successMessage() : resetPasswordForm()} + +
+
+ ) +} + +export default ResetPasswordPageStep2; \ No newline at end of file diff --git a/app/(account)/reset-password/actions.ts b/app/(account)/reset-password/actions.ts index 3ed3af8..675c059 100644 --- a/app/(account)/reset-password/actions.ts +++ b/app/(account)/reset-password/actions.ts @@ -3,14 +3,14 @@ import {revalidatePath} from "next/cache"; import {z} from "zod"; -export interface ResendMailResponse { +export interface ResetPassowrdStep1 { success: boolean; email?: string; message?: string; fieldErrors?: { [key: string]: string }; } -export const resendMailConfirmationToken = async (prevState: ResendMailResponse, formData : FormData): Promise => { +export const requestAPasswordReset = async (_: ResetPassowrdStep1, formData : FormData): Promise => { const schema = z.object({ email: z.string().email(), }); @@ -26,7 +26,7 @@ export const resendMailConfirmationToken = async (prevState: ResendMailResponse, const email = parsed.data.email; try { - const response = await fetch(`${process.env.CC_API_URL}/accounts/resend-account-confirmation/asd`, { + const response = await fetch(`${process.env.CC_API_URL}/accounts/reset-password/`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/app/(account)/reset-password/page.tsx b/app/(account)/reset-password/page.tsx index 6dba675..ff32b60 100644 --- a/app/(account)/reset-password/page.tsx +++ b/app/(account)/reset-password/page.tsx @@ -4,25 +4,25 @@ import Button from "../../common/uiLibrary/Button"; import React from "react"; import FormContainer from "../../common/uiLibrary/Layouters/formContainer"; import TextField from "../../common/uiLibrary/forms/textField"; -import {resendMailConfirmationToken, ResendMailResponse} from "./actions"; +import {requestAPasswordReset, ResetPassowrdStep1} from "./actions"; import {useFormState} from "react-dom"; import ContactInformation from "../../(home)/contactInformation"; -const initialState: ResendMailResponse = { +const initialState: ResetPassowrdStep1 = { success: false, message: undefined, email: '' }; const ResetPasswordPage = () => { - const [state, formAction] = useFormState(resendMailConfirmationToken, initialState); + const [state, formAction] = useFormState(requestAPasswordReset, initialState); const successMessage = () => (

Success!

An email has been sent with instructions to reset your password.

Please check your inbox and follow the instructions to complete the process.

-

Didn't receive the email? Check your Spam folder or try resending the email. Ensure your email address is +

Didn't receive the email? Check your Spam folder or try requesting a password reset again. Ensure your email address is entered correctly.

); diff --git a/app/(home)/page.tsx b/app/(home)/page.tsx index 2207cc2..ba71221 100644 --- a/app/(home)/page.tsx +++ b/app/(home)/page.tsx @@ -49,7 +49,7 @@ const fetchLatestBlogPost = async (): Promise => { return resPage1.results.concat(resPage2.results); } -const HomePage: () => Promise = async () => { +const HomePage = async () => { const latestBlogPosts: BlogPost[] = await fetchLatestBlogPost(); const NoSsrHeroImageClient = dynamic(() => import('./HeroRandomImageClient'), {ssr: false}); diff --git a/app/common/defaultNavbar.tsx b/app/common/defaultNavbar.tsx index d1fb833..14da603 100644 --- a/app/common/defaultNavbar.tsx +++ b/app/common/defaultNavbar.tsx @@ -16,9 +16,10 @@ export default function DefaultNavbar() { const loggedOptions = () => ( <> - My Account + {/* hidden while we work on it */} + {/*My Account*/} - Logout + Logout ) @@ -26,11 +27,11 @@ export default function DefaultNavbar() { const notLoggedOptions = () => ( <> - Login/Register + Login/Register - Reset password + Reset password ) @@ -50,10 +51,10 @@ export default function DefaultNavbar() {

Home

- +

Blog

- +

Changelog

diff --git a/app/common/uiLibrary/Layouters/fullPage.tsx b/app/common/uiLibrary/Layouters/fullPage.tsx new file mode 100644 index 0000000..ef34bfa --- /dev/null +++ b/app/common/uiLibrary/Layouters/fullPage.tsx @@ -0,0 +1,12 @@ +import layoutChildren from "../../../../types/layoutChildren"; + +// makes the view occupy the full page, excluding the header. +const FullPage = (props: layoutChildren) => { + return ( +
+ {props.children} +
+ ) +} + +export default FullPage; \ No newline at end of file diff --git a/app/confirm-email/[token]/actions.ts b/app/confirm-email/[token]/actions.ts deleted file mode 100644 index 0ebe69b..0000000 --- a/app/confirm-email/[token]/actions.ts +++ /dev/null @@ -1,31 +0,0 @@ -"use server" - -export const postMailConfirmationToken = async (token: string): Promise => { - try { - const response = await fetch(`${process.env.CC_API_URL}/accounts/confirm-account`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ token }), - }); - - if (!response.ok) { - const errorResponse = await response.json(); - if (errorResponse.error) { - if (typeof errorResponse.error === 'string') { - return { error: errorResponse.error }; - } else if (errorResponse.error.token) { - return { error: errorResponse.error.token.join(' ') }; - } else if (errorResponse.error.non_field_errors) { - return { error: errorResponse.error.non_field_errors.join(' ') }; - } - } - return { error: "An unexpected error occurred." }; - } - - return { success: true }; - } catch (error) { - return { error: "An unexpected error occurred." }; - } -};