Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve sign in #37

Merged
merged 3 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions internal/controllers/auth_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,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"})
}

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, user, err := ac.authService.VerifyPassword(
body.Email,
isValid, err := ac.authService.VerifyPassword(
body.Password,
user.PasswordHash,
)
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)
Expand Down
30 changes: 11 additions & 19 deletions internal/services/auth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,25 +89,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) {
Expand Down Expand Up @@ -139,3 +125,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
}
11 changes: 9 additions & 2 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/assets/logo/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Keizer Auth</title>

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="app"></div>
Expand Down
10 changes: 10 additions & 0 deletions web/public/assets/logo/logo-full.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions web/public/assets/logo/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion web/public/vite.svg

This file was deleted.

13 changes: 13 additions & 0 deletions web/src/actions/auth/sign-in.ts
Original file line number Diff line number Diff line change
@@ -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<typeof emailPassSignInSchema>,
) =>
await apiClient.post<SignInRes>("auth/sign-in", data).then((res) => res.data);
114 changes: 114 additions & 0 deletions web/src/components/sign-in/form.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>;
type EmailSignInSchema = z.infer<typeof emailPassSignInSchema>;

export function SignInForm({ className, ...props }: UserAuthFormProps) {
const router = useRouter();

const form = useForm<EmailSignInSchema>({
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 (
<div className={cn("grid gap-6", className)} {...props}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-y-4">
<FormField
control={form.control}
name="email"
label="Email"
render={({ field }) => <Input {...field} />}
/>

<FormField
control={form.control}
name="password"
label="Passowrd"
render={({ field }) => <PasswordInput {...field} />}
/>

<Button loading={isPending} type="submit" className="mt-4 w-full">
Sign In
</Button>
</form>
</Form>

<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>

<Button disabled variant="outline" type="button">
<GitHubLogoIcon className="size-4" /> GitHub
</Button>
</div>
);
}
2 changes: 1 addition & 1 deletion web/src/components/sign-up/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function SignUpForm({ className, ...props }: UserAuthFormProps) {
</div>

<Button disabled variant="outline" type="button">
<GitHubLogoIcon className="mr-2 h-4 w-4" /> GitHub
<GitHubLogoIcon className="size-4" /> GitHub
</Button>
</div>
);
Expand Down
Loading
Loading