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 ? (
+

+ ) : (
+
+ )}
+
+
{org?.name}
+
+
+
+
+
+
Forgot Password
+
+ Enter your email address and we will send you a link to reset your
+ password
+
+
+ {error && (
+
+ )}
+ {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 ? (
+

+ ) : (
+
+ )}
+
+
{org?.name}
+
+
+
+
+
+ )
+}
+
+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