From cac8debfb5371129fb2507bcccdf49905f082599 Mon Sep 17 00:00:00 2001 From: Sudarsh1010 Date: Thu, 14 Nov 2024 08:54:39 +0530 Subject: [PATCH 1/3] user should be verified to login and improved error messages --- internal/controllers/auth_controller.go | 40 ++++++++++++++++++++----- internal/services/auth_service.go | 33 ++++++++------------ 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index ead1750c..a45c3895 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -3,7 +3,6 @@ package controllers import ( "errors" "fmt" - "keizer-auth/internal/models" "keizer-auth/internal/services" "keizer-auth/internal/utils" @@ -26,27 +25,54 @@ func (ac *AuthController) SignIn(c *fiber.Ctx) error { body := new(validators.SignInUser) if err := c.BodyParser(body); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return c. + Status(fiber.StatusBadRequest). + JSON(fiber.Map{"error": "Invalid request body"}) } - isValid, user, err := ac.authService.VerifyPassword( - body.Email, + user, err := ac.authService.GetUser(body.Email) + if err != nil { + return c. + Status(fiber.StatusInternalServerError). + JSON(fiber.Map{ + "error": "Unable to retrieve user information. Please try again later.", + }) + } + if user.ID.String() == "" { + return c. + Status(fiber.StatusInternalServerError). + JSON(fiber.Map{ + "error": "User not found. Please check your email and try again.", + }) + } + if !user.IsVerified { + return c. + Status(fiber.StatusInternalServerError). + JSON(fiber.Map{ + "error": "User is not verified. Please verify your account before signing in.", + }) + } + + isValid, err := ac.authService.VerifyPassword( + user.PasswordHash, body.Password, ) if err != nil { return c. Status(fiber.StatusInternalServerError). - JSON(fiber.Map{"error": "Internal Server Error"}) + JSON(fiber.Map{"error": "Unable to verify password. Please try again later."}) } if !isValid { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid email or password"}) + return c. + Status(fiber.StatusUnauthorized). + JSON(fiber.Map{"error": "Invalid email or password. Please try again."}) } sessionId, err := ac.sessionService.CreateSession(user) if err != nil { return c. Status(fiber.StatusInternalServerError). - JSON(fiber.Map{"error": "Failed to create session"}) + JSON(fiber.Map{"error": "Something went wrong, Failed to create session"}) } utils.SetSessionCookie(c, sessionId) diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 50fc099e..433e397d 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -4,12 +4,11 @@ import ( "encoding/base64" "encoding/json" "fmt" - "time" - "keizer-auth/internal/models" "keizer-auth/internal/repositories" "keizer-auth/internal/utils" "keizer-auth/internal/validators" + "time" "github.com/nrednav/cuid2" "github.com/redis/go-redis/v9" @@ -89,25 +88,11 @@ func (as *AuthService) RegisterUser( return otpCacheKey, nil } -func (as *AuthService) VerifyPassword(email string, password string) (bool, *models.User, error) { - user := models.User{Email: email} - err := as.userRepo.GetUserByStruct(&user) - if err != nil { - return false, nil, err - } - if user.ID.String() == "" { - return false, nil, err - } - - isValid, err := utils.VerifyPassword(password, user.PasswordHash) - if err != nil { - return false, nil, err - } - if !isValid { - return false, nil, nil - } - - return true, &user, nil +func (as *AuthService) VerifyPassword( + password string, + passwordHash string, +) (bool, error) { + return utils.VerifyPassword(password, passwordHash) } func (as *AuthService) VerifyOTP(verifyOtpBody *validators.VerifyOTP) (string, bool, error) { @@ -139,3 +124,9 @@ func (as *AuthService) SetIsVerified(id string) (*models.User, error) { err := as.userRepo.UpdateUser(id, &user) return &user, err } + +func (as *AuthService) GetUser(email string) (*models.User, error) { + user := models.User{Email: email} + err := as.userRepo.GetUserByStruct(&user) + return &user, err +} From c889852ab15b1a9cd147036237596f88c27def0e Mon Sep 17 00:00:00 2001 From: Sudarsh1010 Date: Thu, 14 Nov 2024 09:07:05 +0530 Subject: [PATCH 2/3] add sign-in page to the dashboard --- internal/controllers/auth_controller.go | 2 +- web/src/actions/auth/sign-in.ts | 13 +++ web/src/components/sign-in/form.tsx | 114 ++++++++++++++++++++++++ web/src/route-tree.gen.ts | 36 +++++++- web/src/routes/_auth/sign-in.tsx | 52 +++++++++++ 5 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 web/src/actions/auth/sign-in.ts create mode 100644 web/src/components/sign-in/form.tsx create mode 100644 web/src/routes/_auth/sign-in.tsx diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index a45c3895..a4353488 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -54,8 +54,8 @@ func (ac *AuthController) SignIn(c *fiber.Ctx) error { } isValid, err := ac.authService.VerifyPassword( - user.PasswordHash, body.Password, + user.PasswordHash, ) if err != nil { return c. diff --git a/web/src/actions/auth/sign-in.ts b/web/src/actions/auth/sign-in.ts new file mode 100644 index 00000000..31a8a57b --- /dev/null +++ b/web/src/actions/auth/sign-in.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +import apiClient from "~/axios"; +import type { emailPassSignInSchema } from "~/schema/auth"; + +interface SignInRes { + message: string; +} + +export const signInMutationFn = async ( + data: z.infer, +) => + await apiClient.post("auth/sign-in", data).then((res) => res.data); diff --git a/web/src/components/sign-in/form.tsx b/web/src/components/sign-in/form.tsx new file mode 100644 index 00000000..3d5d2dd2 --- /dev/null +++ b/web/src/components/sign-in/form.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { GitHubLogoIcon } from "@radix-ui/react-icons"; +import { useMutation } from "@tanstack/react-query"; +import { useRouter } from "@tanstack/react-router"; +import { AxiosError } from "axios"; +import * as React from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +import { signInMutationFn } from "~/actions/auth/sign-in"; +import { cn } from "~/lib/utils"; +import { emailPassSignInSchema } from "~/schema/auth"; + +import { Button } from "../ui/button"; +import { Form, FormField } from "../ui/form"; +import { Input } from "../ui/input"; +import { PasswordInput } from "../ui/password-input"; + +type UserAuthFormProps = React.HTMLAttributes; +type EmailSignInSchema = z.infer; + +export function SignInForm({ className, ...props }: UserAuthFormProps) { + const router = useRouter(); + + const form = useForm({ + resolver: zodResolver(emailPassSignInSchema), + }); + + const { mutate, isPending } = useMutation({ + mutationFn: signInMutationFn, + onSuccess: (res) => { + toast.success(res.message); + router.navigate({ to: "/" }); + }, + onError: (err) => { + if (err instanceof AxiosError) { + if (err.response?.data?.errors) { + let shouldFocus = true; + const validationErrors = err.response.data.errors; + + return Object.keys(validationErrors).forEach((field) => { + const fieldErrors = validationErrors[field]; + const errorMessage = Object.values(fieldErrors).find( + (message) => message !== "", + ) as string; + + if (errorMessage) { + form.setError( + field as keyof EmailSignInSchema, + { message: errorMessage }, + { shouldFocus: shouldFocus }, + ); + shouldFocus = false; + } + }); + } + + return toast.error( + err.response?.data?.error || "An unknown error occurred.", + ); + } + + return toast.error("An unknown error occurred."); + }, + }); + + async function onSubmit(data: EmailSignInSchema) { + mutate(data); + } + + return ( +
+
+ + } + /> + + } + /> + + + + + +
+
+ +
+
+ + Or continue with + +
+
+ + +
+ ); +} diff --git a/web/src/route-tree.gen.ts b/web/src/route-tree.gen.ts index 9c401288..03f4cade 100644 --- a/web/src/route-tree.gen.ts +++ b/web/src/route-tree.gen.ts @@ -14,6 +14,7 @@ import { Route as rootRoute } from './routes/__root' import { Route as AuthImport } from './routes/_auth' import { Route as IndexImport } from './routes/index' import { Route as AuthSignUpImport } from './routes/_auth/sign-up' +import { Route as AuthSignInImport } from './routes/_auth/sign-in' import { Route as AuthVerifyOtpIdImport } from './routes/_auth/verify-otp/$id' // Create/Update Routes @@ -35,6 +36,12 @@ const AuthSignUpRoute = AuthSignUpImport.update({ getParentRoute: () => AuthRoute, } as any) +const AuthSignInRoute = AuthSignInImport.update({ + id: '/sign-in', + path: '/sign-in', + getParentRoute: () => AuthRoute, +} as any) + const AuthVerifyOtpIdRoute = AuthVerifyOtpIdImport.update({ id: '/verify-otp/$id', path: '/verify-otp/$id', @@ -59,6 +66,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthImport parentRoute: typeof rootRoute } + '/_auth/sign-in': { + id: '/_auth/sign-in' + path: '/sign-in' + fullPath: '/sign-in' + preLoaderRoute: typeof AuthSignInImport + parentRoute: typeof AuthImport + } '/_auth/sign-up': { id: '/_auth/sign-up' path: '/sign-up' @@ -79,11 +93,13 @@ declare module '@tanstack/react-router' { // Create and export the route tree interface AuthRouteChildren { + AuthSignInRoute: typeof AuthSignInRoute AuthSignUpRoute: typeof AuthSignUpRoute AuthVerifyOtpIdRoute: typeof AuthVerifyOtpIdRoute } const AuthRouteChildren: AuthRouteChildren = { + AuthSignInRoute: AuthSignInRoute, AuthSignUpRoute: AuthSignUpRoute, AuthVerifyOtpIdRoute: AuthVerifyOtpIdRoute, } @@ -93,6 +109,7 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) export interface FileRoutesByFullPath { '/': typeof IndexRoute '': typeof AuthRouteWithChildren + '/sign-in': typeof AuthSignInRoute '/sign-up': typeof AuthSignUpRoute '/verify-otp/$id': typeof AuthVerifyOtpIdRoute } @@ -100,6 +117,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '': typeof AuthRouteWithChildren + '/sign-in': typeof AuthSignInRoute '/sign-up': typeof AuthSignUpRoute '/verify-otp/$id': typeof AuthVerifyOtpIdRoute } @@ -108,16 +126,23 @@ export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/_auth': typeof AuthRouteWithChildren + '/_auth/sign-in': typeof AuthSignInRoute '/_auth/sign-up': typeof AuthSignUpRoute '/_auth/verify-otp/$id': typeof AuthVerifyOtpIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '' | '/sign-up' | '/verify-otp/$id' + fullPaths: '/' | '' | '/sign-in' | '/sign-up' | '/verify-otp/$id' fileRoutesByTo: FileRoutesByTo - to: '/' | '' | '/sign-up' | '/verify-otp/$id' - id: '__root__' | '/' | '/_auth' | '/_auth/sign-up' | '/_auth/verify-otp/$id' + to: '/' | '' | '/sign-in' | '/sign-up' | '/verify-otp/$id' + id: + | '__root__' + | '/' + | '/_auth' + | '/_auth/sign-in' + | '/_auth/sign-up' + | '/_auth/verify-otp/$id' fileRoutesById: FileRoutesById } @@ -151,10 +176,15 @@ export const routeTree = rootRoute "/_auth": { "filePath": "_auth.tsx", "children": [ + "/_auth/sign-in", "/_auth/sign-up", "/_auth/verify-otp/$id" ] }, + "/_auth/sign-in": { + "filePath": "_auth/sign-in.tsx", + "parent": "/_auth" + }, "/_auth/sign-up": { "filePath": "_auth/sign-up.tsx", "parent": "/_auth" diff --git a/web/src/routes/_auth/sign-in.tsx b/web/src/routes/_auth/sign-in.tsx new file mode 100644 index 00000000..a586af5d --- /dev/null +++ b/web/src/routes/_auth/sign-in.tsx @@ -0,0 +1,52 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; + +import { SignInForm } from "~/components/sign-in/form"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card"; + +export const Route = createFileRoute("/_auth/sign-in")({ + component: RouteComponent, +}); + +function RouteComponent() { + return ( + + + Sign In to Your Dashboard + + Please enter your email and password to access your dashboard + + + + + + + + + + By clicking continue, you agree to our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + . + + + + ); +} From 46c9b9732c7b132031de333cf322acd4fd372473 Mon Sep 17 00:00:00 2001 From: Sudarsh1010 Date: Thu, 14 Nov 2024 09:52:15 +0530 Subject: [PATCH 3/3] add fonts, basic designs, logo --- internal/controllers/auth_controller.go | 1 + internal/services/auth_service.go | 3 ++- web/index.html | 11 +++++++++-- web/public/assets/logo/logo-full.svg | 10 ++++++++++ web/public/assets/logo/logo.svg | 20 ++++++++++++++++++++ web/public/vite.svg | 1 - web/src/components/sign-in/form.tsx | 2 +- web/src/components/sign-up/form.tsx | 2 +- web/src/routes/_auth.tsx | 14 +------------- web/src/styles/index.css | 22 +++++++++++++++------- web/tailwind.config.js | 4 ++++ 11 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 web/public/assets/logo/logo-full.svg create mode 100644 web/public/assets/logo/logo.svg delete mode 100644 web/public/vite.svg diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index a4353488..8f5edf2e 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -3,6 +3,7 @@ package controllers import ( "errors" "fmt" + "keizer-auth/internal/models" "keizer-auth/internal/services" "keizer-auth/internal/utils" diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 433e397d..9f04e664 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -4,11 +4,12 @@ import ( "encoding/base64" "encoding/json" "fmt" + "time" + "keizer-auth/internal/models" "keizer-auth/internal/repositories" "keizer-auth/internal/utils" "keizer-auth/internal/validators" - "time" "github.com/nrednav/cuid2" "github.com/redis/go-redis/v9" diff --git a/web/index.html b/web/index.html index 990e82da..3f4cde3f 100644 --- a/web/index.html +++ b/web/index.html @@ -2,9 +2,16 @@ - + - Vite + React + TS + Keizer Auth + + + +
diff --git a/web/public/assets/logo/logo-full.svg b/web/public/assets/logo/logo-full.svg new file mode 100644 index 00000000..b2ad1af8 --- /dev/null +++ b/web/public/assets/logo/logo-full.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/public/assets/logo/logo.svg b/web/public/assets/logo/logo.svg new file mode 100644 index 00000000..6b2c95b6 --- /dev/null +++ b/web/public/assets/logo/logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/vite.svg b/web/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/web/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/components/sign-in/form.tsx b/web/src/components/sign-in/form.tsx index 3d5d2dd2..a7c51651 100644 --- a/web/src/components/sign-in/form.tsx +++ b/web/src/components/sign-in/form.tsx @@ -107,7 +107,7 @@ export function SignInForm({ className, ...props }: UserAuthFormProps) { ); diff --git a/web/src/components/sign-up/form.tsx b/web/src/components/sign-up/form.tsx index 7905ee23..f8fd20ce 100644 --- a/web/src/components/sign-up/form.tsx +++ b/web/src/components/sign-up/form.tsx @@ -126,7 +126,7 @@ export function SignUpForm({ className, ...props }: UserAuthFormProps) { ); diff --git a/web/src/routes/_auth.tsx b/web/src/routes/_auth.tsx index ddc9de90..4307fc96 100644 --- a/web/src/routes/_auth.tsx +++ b/web/src/routes/_auth.tsx @@ -11,19 +11,7 @@ function RouteComponent() {
- - - - Keizer Auth +
diff --git a/web/src/styles/index.css b/web/src/styles/index.css index 7d864cf1..819a4ec9 100644 --- a/web/src/styles/index.css +++ b/web/src/styles/index.css @@ -82,7 +82,7 @@ @layer base { * { - @apply border-border; + @apply border-border font-sans box-border antialiased; } body { @@ -90,26 +90,34 @@ } h1 { - @apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl; + @apply scroll-m-20 text-4xl font-bold font-serif tracking-tight lg:text-5xl; } h2 { - @apply scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight; + @apply scroll-m-20 border-b pb-2 text-3xl font-serif font-semibold tracking-tight; } h3 { - @apply scroll-m-20 text-2xl font-semibold tracking-tight; + @apply scroll-m-20 text-2xl font-semibold font-serif tracking-tight; } h4 { - @apply scroll-m-20 text-xl font-semibold tracking-tight; + @apply scroll-m-20 text-xl font-semibold font-serif tracking-tight; } p { - @apply leading-7; + @apply leading-7 font-sans; + } + + label { + @apply font-serif; + } + + button { + @apply font-serif; } blockquote { - @apply mt-6 border-l-2 pl-6 italic; + @apply mt-6 border-l-2 pl-6 font-serif italic; } } diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 9a79c94d..261a99e6 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -6,6 +6,10 @@ export default { content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], theme: { extend: { + fontFamily: { + sans: ["Open Sans", "sans-serif"], + serif: ["Lora", "serif"], + }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)",