diff --git a/apps/api/src/services/users/emails.py b/apps/api/src/services/users/emails.py index 880be532..95ab0a15 100644 --- a/apps/api/src/services/users/emails.py +++ b/apps/api/src/services/users/emails.py @@ -25,11 +25,12 @@ def send_account_creation_email( def send_password_reset_email( - reset_email_invite_uuid: str, + generated_reset_code: str, user: UserRead, organization: OrganizationRead, email: EmailStr, ): + # send email return send_email( to=email, @@ -38,7 +39,7 @@ def send_password_reset_email(

Hello {user.username}

-

Click here to reset your password.

+

Click here to reset your password.

""", diff --git a/apps/api/src/services/users/password_reset.py b/apps/api/src/services/users/password_reset.py index 4c379fee..e0222d4a 100644 --- a/apps/api/src/services/users/password_reset.py +++ b/apps/api/src/services/users/password_reset.py @@ -99,7 +99,7 @@ async def send_reset_password_code( # Send reset code via email isEmailSent = send_password_reset_email( - reset_email_invite_uuid=reset_email_invite_uuid, + generated_reset_code=generated_reset_code, user=user, organization=org, email=user.email, @@ -132,7 +132,7 @@ async def change_password_with_reset_code( status_code=400, detail="User does not exist", ) - + # Get org statement = select(Organization).where(Organization.id == org_id) org = db_session.exec(statement).first() @@ -142,7 +142,6 @@ async def change_password_with_reset_code( status_code=400, detail="Organization not found", ) - # Redis init LH_CONFIG = get_learnhouse_config() diff --git a/apps/web/app/orgs/[orgslug]/dash/layout.tsx b/apps/web/app/orgs/[orgslug]/dash/layout.tsx index 7c847dcd..ad553272 100644 --- a/apps/web/app/orgs/[orgslug]/dash/layout.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/layout.tsx @@ -1,8 +1,13 @@ import SessionProvider from '@components/Contexts/SessionContext' import LeftMenu from '@components/Dashboard/UI/LeftMenu' import AdminAuthorization from '@components/Security/AdminAuthorization' +import { Metadata } from 'next' import React from 'react' +export const metadata: Metadata = { + title: 'LearnHouse Dashboard', +} + function DashboardLayout({ children, params, diff --git a/apps/web/app/orgs/[orgslug]/forgot/forgot.tsx b/apps/web/app/orgs/[orgslug]/forgot/forgot.tsx new file mode 100644 index 00000000..8b3cb7c2 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/forgot/forgot.tsx @@ -0,0 +1,156 @@ +'use client' +import Image from 'next/image' +import React from 'react' +import learnhouseIcon from 'public/learnhouse_bigicon_1.png' +import FormLayout, { + FormField, + FormLabelAndMessage, + Input, +} from '@components/StyledElements/Form/Form' +import * as Form from '@radix-ui/react-form' +import { getOrgLogoMediaDirectory } from '@services/media/media' +import { AlertTriangle, Info } from 'lucide-react' +import Link from 'next/link' +import { getUriWithOrg } from '@services/config/config' +import { useOrg } from '@components/Contexts/OrgContext' +import { useRouter } from 'next/navigation' +import { useFormik } from 'formik' +import { sendResetLink } from '@services/auth/auth' + +const validate = (values: any) => { + const errors: any = {} + + if (!values.email) { + errors.email = 'Required' + } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) { + errors.email = 'Invalid email address' + } + + + return errors +} + +function ForgotPasswordClient() { + const org = useOrg() as any; + const [isSubmitting, setIsSubmitting] = React.useState(false) + const router = useRouter() + const [error, setError] = React.useState('') + const [message, setMessage] = React.useState('') + + const formik = useFormik({ + initialValues: { + email: '' + }, + validate, + onSubmit: async (values) => { + setIsSubmitting(true) + let res = await sendResetLink(values.email, org?.id) + if (res.status == 200) { + setMessage(res.data + ', please check your email') + setIsSubmitting(false) + } else { + setError(res.data.detail) + setIsSubmitting(false) + } + }, + }) + return ( + +
+
+
+ + + +
+
+
+ +
+ {org?.logo_image ? ( + Learnhouse + ) : ( + + )} +
+
{org?.name}
+
+
+
+
+
+

Forgot Password

+

+ Enter your email address and we will send you a link to reset your + password +

+ + {error && ( +
+ +
{error}
+
+ )} + {message && ( +
+ +
{message}
+
+ )} + + + + + + + +
+ + + +
+
+ +
+
+
+ ) +} + +export default ForgotPasswordClient \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/forgot/page.tsx b/apps/web/app/orgs/[orgslug]/forgot/page.tsx new file mode 100644 index 00000000..bbcc0ad9 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/forgot/page.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import ForgotPasswordClient from './forgot' +import { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'LearnHouse - Forgot Password', +} + +function ForgotPasswordPage() { + return ( + <> + + + ) +} + +export default ForgotPasswordPage \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/login/login.tsx b/apps/web/app/orgs/[orgslug]/login/login.tsx index 4bfb1c35..603c93a4 100644 --- a/apps/web/app/orgs/[orgslug]/login/login.tsx +++ b/apps/web/app/orgs/[orgslug]/login/login.tsx @@ -156,6 +156,15 @@ const LoginClient = (props: LoginClientProps) => { /> +
+ + Forgot password? + +
diff --git a/apps/web/app/orgs/[orgslug]/reset/page.tsx b/apps/web/app/orgs/[orgslug]/reset/page.tsx new file mode 100644 index 00000000..2b206e0b --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/reset/page.tsx @@ -0,0 +1,15 @@ +import { Metadata } from 'next' +import React from 'react' +import ResetPasswordClient from './reset' + +export const metadata: Metadata = { + title: 'LearnHouse - Reset Password', +} + +function ResetPasswordPage() { + return ( + + ) +} + +export default ResetPasswordPage \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/reset/reset.tsx b/apps/web/app/orgs/[orgslug]/reset/reset.tsx new file mode 100644 index 00000000..f29d9a36 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/reset/reset.tsx @@ -0,0 +1,220 @@ +'use client' +import Image from 'next/image' +import React from 'react' +import learnhouseIcon from 'public/learnhouse_bigicon_1.png' +import FormLayout, { + FormField, + FormLabelAndMessage, + Input, +} from '@components/StyledElements/Form/Form' +import * as Form from '@radix-ui/react-form' +import { getOrgLogoMediaDirectory } from '@services/media/media' +import { AlertTriangle, Info } from 'lucide-react' +import Link from 'next/link' +import { getUriWithOrg } from '@services/config/config' +import { useOrg } from '@components/Contexts/OrgContext' +import { useRouter, useSearchParams } from 'next/navigation' +import { useFormik } from 'formik' +import { resetPassword, sendResetLink } from '@services/auth/auth' + +const validate = (values: any) => { + const errors: any = {} + + if (!values.email) { + errors.email = 'Required' + } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) { + errors.email = 'Invalid email address' + } + + if (!values.new_password) { + errors.new_password = 'Required' + } + + if (!values.confirm_password) { + errors.confirm_password = 'Required' + } + + if (values.new_password !== values.confirm_password) { + errors.confirm_password = 'Passwords do not match' + } + + if (!values.reset_code) { + errors.reset_code = 'Required' + } + return errors +} + +function ResetPasswordClient() { + const org = useOrg() as any; + const [isSubmitting, setIsSubmitting] = React.useState(false) + const searchParams = useSearchParams() + const reset_code = searchParams.get('resetCode') || '' + const email = searchParams.get('email') || '' + const router = useRouter() + const [error, setError] = React.useState('') + const [message, setMessage] = React.useState('') + + const formik = useFormik({ + initialValues: { + email: email, + new_password: '', + confirm_password: '', + reset_code: reset_code + }, + validate, + enableReinitialize: true, + onSubmit: async (values) => { + setIsSubmitting(true) + let res = await resetPassword(values.email, values.new_password, org?.id, values.reset_code) + if (res.status == 200) { + setMessage(res.data + ', please login') + setIsSubmitting(false) + } else { + setError(res.data.detail) + setIsSubmitting(false) + } + + }, + }) + return ( + +
+
+
+ + + +
+
+
+ +
+ {org?.logo_image ? ( + Learnhouse + ) : ( + + )} +
+
{org?.name}
+
+
+
+
+
+

Reset Password

+

+ Enter your email and reset code to reset your password +

+ + {error && ( +
+ +
{error}
+
+ )} + {message && ( +
+ +
{message}
+
+ )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+
+ ) +} + +export default ResetPasswordClient \ No newline at end of file diff --git a/apps/web/services/auth/auth.ts b/apps/web/services/auth/auth.ts index f18d6002..bb3e6ef0 100644 --- a/apps/web/services/auth/auth.ts +++ b/apps/web/services/auth/auth.ts @@ -1,4 +1,5 @@ import { getAPIUrl } from '@services/config/config' +import { RequestBody, getResponseMetadata } from '@services/utils/ts/requests' interface LoginAndGetTokenResponse { access_token: 'string' @@ -36,6 +37,29 @@ export async function loginAndGetToken( return response } +export async function sendResetLink(email: string, org_id: number) { + const result = await fetch( + `${getAPIUrl()}users/reset_password/send_reset_code/${email}?org_id=${org_id}`, + RequestBody('POST', null, null) + ) + const res = await getResponseMetadata(result) + return res +} + +export async function resetPassword( + email: string, + new_password: string, + org_id: number, + reset_code: string +) { + const result = await fetch( + `${getAPIUrl()}users/reset_password/change_password/${email}?reset_code=${reset_code}&new_password=${new_password}&org_id=${org_id}`, + RequestBody('POST', null, null) + ) + const res = await getResponseMetadata(result) + return res +} + export async function logout(): Promise { // Request Config