From 5174666befb5ab7051a6981c855677f117987e80 Mon Sep 17 00:00:00 2001 From: "M. E. Abdelsalam" Date: Tue, 28 Oct 2025 07:42:15 +0300 Subject: [PATCH] modif: now tutors can register on the app. --- apps/web/src/components/Auth/Login/Form.tsx | 7 +--- .../src/components/Auth/Register/Content.tsx | 2 +- .../web/src/components/Auth/Register/Form.tsx | 4 +- apps/web/src/hooks/google.ts | 20 +++++++++- services/server/src/handlers/auth.ts | 39 ++++++++++--------- services/server/src/handlers/user.ts | 7 +--- services/server/src/lib/user.ts | 25 +++++++++++- 7 files changed, 67 insertions(+), 37 deletions(-) diff --git a/apps/web/src/components/Auth/Login/Form.tsx b/apps/web/src/components/Auth/Login/Form.tsx index 18603e51c..df3a78ae2 100644 --- a/apps/web/src/components/Auth/Login/Form.tsx +++ b/apps/web/src/components/Auth/Login/Form.tsx @@ -48,12 +48,7 @@ const LoginForm: React.FC<{ return router.generic({ route, query: omit(query, "redirect") }); }, [searchParams]); - const google = useGoogle({ - // TODO: this should cover both tutors and students. - // should be implemented once the tutor onboarding is finalized. - role: IUser.Role.Student, - redirect, - }); + const google = useGoogle({ redirect }); // ========== manual login ============ const onSuccess = useCallback( diff --git a/apps/web/src/components/Auth/Register/Content.tsx b/apps/web/src/components/Auth/Register/Content.tsx index 0c2ed1a15..cec9ddcb4 100644 --- a/apps/web/src/components/Auth/Register/Content.tsx +++ b/apps/web/src/components/Auth/Register/Content.tsx @@ -18,7 +18,7 @@ const Content: React.FC = () => { }, [params.role]); useEffect(() => { - if (!role) return navigate(Web.Root); + if (!role) return navigate(Web.Login); }, [navigate, role]); return ( diff --git a/apps/web/src/components/Auth/Register/Form.tsx b/apps/web/src/components/Auth/Register/Form.tsx index ada346180..517825b82 100644 --- a/apps/web/src/components/Auth/Register/Form.tsx +++ b/apps/web/src/components/Auth/Register/Form.tsx @@ -37,9 +37,7 @@ const RegisterForm: React.FC<{ role?: Role }> = ({ role }) => { const verifyEmailDialog = useRender(); // ======== Google Registeration ============ - const google = useGoogle({ - role, - }); + const google = useGoogle({ role }); // ========== manual registeration ============ const onSuccess = useCallback( diff --git a/apps/web/src/hooks/google.ts b/apps/web/src/hooks/google.ts index 3bfac8ca6..695e2e9d8 100644 --- a/apps/web/src/hooks/google.ts +++ b/apps/web/src/hooks/google.ts @@ -2,7 +2,7 @@ import { useApi } from "@litespace/headless/api"; import { useUser } from "@litespace/headless/context/user"; import { useFormatMessage } from "@litespace/ui/hooks/intl"; import { useToast } from "@litespace/ui/Toast"; -import { safe } from "@litespace/utils/error"; +import { ResponseError, safe } from "@litespace/utils/error"; import { IUser } from "@litespace/types"; import { useGoogleLogin, useGoogleOneTapLogin } from "@react-oauth/google"; import { useCallback, useMemo, useState } from "react"; @@ -46,11 +46,27 @@ export function useGoogle({ api.auth.google({ token, type, role }) ); - if (info instanceof Error) + if (info instanceof ResponseError) { + if (info.statusCode === 404) { + return navigate( + router.web({ + route: Web.Register, + role: "student", + }) + ); + } return toast.error({ title: intl("login.error"), description: intl(getErrorMessageId(info)), }); + } + + if (info instanceof Error) { + return toast.error({ + title: intl("login.error"), + description: intl(getErrorMessageId(info)), + }); + } const regularUser = isRegularUser(info.user); if (info.user && !regularUser) { diff --git a/services/server/src/handlers/auth.ts b/services/server/src/handlers/auth.ts index bbda490ad..6a5af3b8c 100644 --- a/services/server/src/handlers/auth.ts +++ b/services/server/src/handlers/auth.ts @@ -4,12 +4,16 @@ import { forbidden, noPassword, notfound, - serviceUnavailable, wrongPassword, } from "@/lib/error/api"; import { users } from "@litespace/models"; import { NextFunction, Request, Response } from "express"; -import { isSamePassword, registerNewStudent, withImageUrl } from "@/lib/user"; +import { + isSamePassword, + registerNewStudent, + registerNewTutor, + withImageUrl, +} from "@/lib/user"; import { IUser } from "@litespace/types"; import { email, password, string } from "@/validation/utils"; import { googleConfig, jwtSecret } from "@/constants"; @@ -121,22 +125,21 @@ async function loginWithGoogle( if (user && role && role !== user.role) return next(bad()); if (user && (!role || role === user.role)) return await success(user); - const register = !user; - if (register && !role) return next(bad()); - if (register) { - // TODO: remove this condition once the turor onboarding is finalized. - if (role === IUser.Role.Tutor) return next(serviceUnavailable()); - - const { user } = await registerNewStudent({ - email: data.email, - verifiedEmail: data.verified, - role, - }); - - return await success(user); - } - - return next(notfound.user()); + const canRegister = !user && role !== undefined; + if (!canRegister) return next(notfound.user()); + + const { user: registeredUser } = + role === IUser.Role.Student + ? await registerNewStudent({ + email: data.email, + verifiedEmail: data.verified, + }) + : await registerNewTutor({ + email: data.email, + verifiedEmail: data.verified, + }); + + return await success(registeredUser); } async function loginWithAuthToken( diff --git a/services/server/src/handlers/user.ts b/services/server/src/handlers/user.ts index 7842e18d6..b71fffecf 100644 --- a/services/server/src/handlers/user.ts +++ b/services/server/src/handlers/user.ts @@ -186,11 +186,8 @@ export async function create(req: Request, res: Response, next: NextFunction) { const payload: IUser.CreateApiPayload = createUserPayload.parse(req.body); const creator = req.user; const admin = isAdmin(creator); - // both students and tutors can create/register account on the application, - // hover, currently only students can register. (that's temporary) - // TODO: check if its a regular user rather than just a student, once the tutor - // on-boarding is finalized. - if (payload.role !== IUser.Role.Student && !admin) return next(forbidden()); + if (!admin && ![IUser.Role.Student, IUser.Role.Tutor].includes(payload.role)) + return next(forbidden()); const userObject = await users.findByEmail(payload.email); if (userObject) return next(exists.user()); diff --git a/services/server/src/lib/user.ts b/services/server/src/lib/user.ts index fa4d2a9da..0d724218c 100644 --- a/services/server/src/lib/user.ts +++ b/services/server/src/lib/user.ts @@ -1,8 +1,8 @@ import crypto from "node:crypto"; import s3 from "@/lib/s3"; import { isValidPhone } from "@litespace/utils"; -import { knex, users, students } from "@litespace/models"; -import { IStudent, IUser } from "@litespace/types"; +import { knex, users, students, tutors } from "@litespace/models"; +import { IStudent, ITutor, IUser } from "@litespace/types"; import { InvalidPhoneNumber, MissingPhoneNumber } from "@/lib/error/local"; export function hashPassword(password: string): string { @@ -108,3 +108,24 @@ export async function registerNewStudent( return { user, student }; }); } + +export async function registerNewTutor( + payload: Partial & + Partial & { verifiedEmail?: boolean } +) { + return await knex.transaction(async (tx) => { + const user = await users.create( + { + role: IUser.Role.Tutor, + email: payload.email, + password: payload.password ? hashPassword(payload.password) : "", + verifiedEmail: payload.verifiedEmail, + }, + tx + ); + + const tutor = await tutors.create(user.id, tx); + + return { user, tutor }; + }); +}