Backend structure / login with JWT
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# This is secret key for jwt signature
|
||||||
|
JWT_SECRET=just a random string here
|
||||||
@@ -1 +1,2 @@
|
|||||||
tmp
|
tmp
|
||||||
|
.env
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ func defaultValue(val string, def string) string {
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadEnv() map[string]string {
|
var Env map[string]string
|
||||||
|
|
||||||
|
func LoadEnv() {
|
||||||
// Create object where to store used variables
|
// Create object where to store used variables
|
||||||
env := make(map[string]string, 1)
|
Env = make(map[string]string)
|
||||||
|
|
||||||
// Get env variables that will be used while server is running
|
// Get env variables that will be used while server is running
|
||||||
env["db"] = defaultValue(os.Getenv("POSTGRES_DB"), "postgresql://postgres:postgres@db:5432/cover-letter")
|
Env["db"] = defaultValue(os.Getenv("POSTGRES_DB"), "postgresql://postgres:postgres@db:5432/cover-letter")
|
||||||
|
Env["JWT_SECRET"] = defaultValue(os.Getenv("JWT_SECRET"), "just a random string here")
|
||||||
return env
|
Env["Environment"] = defaultValue(os.Getenv("Environment"), "dev")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
"backend/models/user"
|
||||||
|
"backend/utils/hash"
|
||||||
|
"backend/utils/jwt"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
res "backend/utils/responses"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegisterForm struct {
|
||||||
|
Email string `json:"email" validate:"required,email"`
|
||||||
|
Name string `json:"name" validate:"required,min=2,max=50"`
|
||||||
|
Password string `json:"password" validate:"required,min=8"`
|
||||||
|
RepeatPassword string `json:"repeatPassword" validate:"required,min=8,eqfield=Password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(c *gin.Context) {
|
||||||
|
// Receive data from frontend, check if data is okay, hash password, call model
|
||||||
|
var data RegisterForm
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
res.Error(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate data
|
||||||
|
validate := validator.New()
|
||||||
|
if err := validate.Struct(data); err != nil {
|
||||||
|
// Handle error
|
||||||
|
res.Error(c, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
hash, err := hash.HashPassword(data.Password)
|
||||||
|
if err != nil {
|
||||||
|
res.Error(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert into database
|
||||||
|
if err := user.Create(data.Email, data.Name, hash); err != nil {
|
||||||
|
// Find out postgres error
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
if !errors.As(err, &pgErr) {
|
||||||
|
// Unknown error
|
||||||
|
res.Error(c, fmt.Sprintf("[UNEXPECTED DB ERROR] %v", err.Error()), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Postgres error
|
||||||
|
log.Printf("[ERROR] Postgres code: %s", pgErr.Code)
|
||||||
|
if pgErr.Code == "23505" {
|
||||||
|
// UNIQUE constraint violation (EMAIL TAKEN)
|
||||||
|
res.Error(c, "Email already exists", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown error
|
||||||
|
res.Error(c, fmt.Sprintf("[UNKNOWN ERROR] %v", err.Error()), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success
|
||||||
|
res.Success(c, gin.H{
|
||||||
|
"message": "Successfully registered",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginForm struct {
|
||||||
|
Email string `json:"email" validate:"required,email"`
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Login(c *gin.Context) {
|
||||||
|
// Bind data
|
||||||
|
var data LoginForm
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
res.Error(c, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate data
|
||||||
|
validate := validator.New()
|
||||||
|
if err := validate.Struct(data); err != nil {
|
||||||
|
res.Error(c, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user in database
|
||||||
|
user, err := user.FindByEmail(data.Email)
|
||||||
|
if err != nil {
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
// Check if pg err
|
||||||
|
if errors.As(err, &pgErr) {
|
||||||
|
res.Error(c, pgErr.Message, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email not found
|
||||||
|
res.Error(c, "Email or password are incorrect", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check hash
|
||||||
|
match := hash.CheckPasswordHash(data.Password, user.Password)
|
||||||
|
if !match {
|
||||||
|
res.Error(c, "Email or password are incorrect", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token, and send to client
|
||||||
|
signedToken, err := jwt.GenerateJWT(user)
|
||||||
|
if err != nil {
|
||||||
|
res.Error(c, fmt.Sprintf("[JWT Generation] %s", err.Error()), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return token as cookie
|
||||||
|
secureCookie := config.Env["Environment"] != "dev" // In dev environment cookie wont be secure
|
||||||
|
// 3600S -> 1H * 24H -> 1D * 7 -> 1W
|
||||||
|
c.SetCookie("jwt-token", signedToken, 3600*24*7, "/", "localhost", secureCookie, true)
|
||||||
|
|
||||||
|
// Return successful login
|
||||||
|
res.Success(c, gin.H{"message": "Successfully logged in"})
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"backend/models"
|
|
||||||
"backend/utils"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct{}
|
|
||||||
|
|
||||||
type RegisterForm struct {
|
|
||||||
Email string `json:"email" validate:"required,email"`
|
|
||||||
Name string `json:"name" validate:"required,min=2,max=50"`
|
|
||||||
Password string `json:"password" validate:"required,min=8"`
|
|
||||||
RepeatPassword string `json:"repeatPassword" validate:"required,min=8,eqfield=Password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Register(c *gin.Context) {
|
|
||||||
// Receive data from frontend, check if data is okay, hash password, call model
|
|
||||||
var data RegisterForm
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
utils.Error(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate data
|
|
||||||
validate := validator.New()
|
|
||||||
if err := validate.Struct(data); err != nil {
|
|
||||||
// Handle error
|
|
||||||
utils.Error(c, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash password
|
|
||||||
hash, err := utils.HashPassword(data.Password)
|
|
||||||
if err != nil {
|
|
||||||
utils.Error(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert into database
|
|
||||||
userMod := models.User{}
|
|
||||||
if err := userMod.Create(data.Email, data.Name, hash); err != nil {
|
|
||||||
// Find out postgres error
|
|
||||||
var pgErr *pgconn.PgError
|
|
||||||
if !errors.As(err, &pgErr) {
|
|
||||||
// Unknown error
|
|
||||||
utils.Error(c, fmt.Sprintf("[UNEXPECTED DB ERROR] %v", err.Error()), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Postgres error
|
|
||||||
log.Printf("[ERROR] Postgres code: %s", pgErr.Code)
|
|
||||||
if pgErr.Code == "23505" {
|
|
||||||
// UNIQUE constraint violation (EMAIL TAKEN)
|
|
||||||
utils.Error(c, "Email already exists", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown error
|
|
||||||
utils.Error(c, fmt.Sprintf("[UNKNOWN ERROR] %v", err.Error()), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return success
|
|
||||||
utils.Success(c, gin.H{
|
|
||||||
"message": "Successfully registered",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ go 1.24.4
|
|||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-playground/validator/v10 v10.20.0
|
github.com/go-playground/validator/v10 v10.20.0
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/jackc/pgx/v5 v5.7.5
|
github.com/jackc/pgx/v5 v5.7.5
|
||||||
golang.org/x/crypto v0.37.0
|
golang.org/x/crypto v0.37.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
|
|||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
|||||||
+2
-2
@@ -10,10 +10,10 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load env variables
|
// Load env variables
|
||||||
env := config.LoadEnv()
|
config.LoadEnv()
|
||||||
|
|
||||||
// Connect to database
|
// Connect to database
|
||||||
err := db.Connect(env["db"])
|
err := db.Connect(config.Env["db"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/db"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id int
|
||||||
|
Email string
|
||||||
|
Name string
|
||||||
|
Password string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(email string, name string, hash string) error {
|
||||||
|
// Generate background context for managing timeouts and disconnections
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Build database query
|
||||||
|
query := `INSERT INTO users (email, name, password) VALUES ($1, $2, $3)`
|
||||||
|
_, err := db.Pool.Exec(ctx, query, strings.ToLower(email), name, hash)
|
||||||
|
|
||||||
|
// Return error if any
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symbol * will follow the pointer to its value
|
||||||
|
func FindByEmail(email string) (*User, error) {
|
||||||
|
// bg context
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
query := `SELECT * FROM users WHERE email = $1`
|
||||||
|
row := db.Pool.QueryRow(ctx, query, strings.ToLower(email))
|
||||||
|
|
||||||
|
// Scan will map rows to User struct
|
||||||
|
var u User
|
||||||
|
if err := row.Scan(&u.Id, &u.Email, &u.Name, &u.Password, &u.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// give pointer
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"backend/db"
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
ID int
|
|
||||||
email string
|
|
||||||
name string
|
|
||||||
password string
|
|
||||||
createdAt string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Create(email string, name string, hash string) error {
|
|
||||||
// Generate background context for managing timeouts and disconnections
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Build database query
|
|
||||||
query := `INSERT INTO users (email, name, password) VALUES ($1, $2, $3)`
|
|
||||||
_, err := db.Pool.Exec(ctx, query, email, name, hash)
|
|
||||||
|
|
||||||
// Return error if any
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/controllers"
|
"backend/controllers/user"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -9,11 +9,9 @@ import (
|
|||||||
func SetupRoutes() *gin.Engine {
|
func SetupRoutes() *gin.Engine {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
// Controllers
|
|
||||||
users := controllers.User{}
|
|
||||||
|
|
||||||
// Guest routes (Register, Login, check auth)
|
// Guest routes (Register, Login, check auth)
|
||||||
r.POST("/register", users.Register)
|
r.POST("/register", user.Register)
|
||||||
|
r.POST("/login", user.Login)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package utils
|
package hash
|
||||||
|
|
||||||
import "golang.org/x/crypto/bcrypt"
|
import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
"backend/models/user"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateJWT(u *user.User) (string, error) {
|
||||||
|
// Generate JWT token
|
||||||
|
mySigningKey := []byte(config.Env["JWT_SECRET"])
|
||||||
|
token := jwt.New(jwt.SigningMethodHS256)
|
||||||
|
|
||||||
|
// Add claims (Values)
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["id"] = u.Id
|
||||||
|
claims["name"] = u.Name
|
||||||
|
claims["email"] = u.Email
|
||||||
|
claims["exp"] = time.Now().Add(time.Hour * 24 * 7).Unix() // Expire in 7 days
|
||||||
|
|
||||||
|
// Generate signed token
|
||||||
|
tokenString, err := token.SignedString(mySigningKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package utils
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
+6
-2
@@ -9,11 +9,15 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- "./backend:/app"
|
- "./backend:/app"
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
environment:
|
environment:
|
||||||
- GIN_MODE=debug
|
|
||||||
# - GIN_MODE=release # For production
|
# - GIN_MODE=release # For production
|
||||||
- POSTGRES_DB=postgresql://postgres:postgres@db:5432/cover-letter
|
- GIN_MODE=debug
|
||||||
# - POSTGRES_DB=postgresql://username:password@host:port/database_name
|
# - POSTGRES_DB=postgresql://username:password@host:port/database_name
|
||||||
|
- POSTGRES_DB=postgresql://postgres:postgres@db:5432/cover-letter
|
||||||
|
# - Environment=prod # For production
|
||||||
|
- Environment=dev
|
||||||
networks:
|
networks:
|
||||||
- cover-letter-network
|
- cover-letter-network
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { API_BASE } from "@/consts";
|
|||||||
import { normalizeLink } from "./utils";
|
import { normalizeLink } from "./utils";
|
||||||
import type { ApiResponse } from "@/types/api";
|
import type { ApiResponse } from "@/types/api";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { tryCatch } from "./tryCatch";
|
||||||
|
|
||||||
interface RequestProps<T> {
|
interface RequestProps<T> {
|
||||||
error?: (err: Error) => void;
|
error?: (err: Error) => void;
|
||||||
@@ -34,7 +35,10 @@ class Requests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get response data
|
// Get response data
|
||||||
const data = (await res.json()) as ApiResponse<T>;
|
const { data, error } = await tryCatch<ApiResponse<T>>(res.json());
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Parsing error: ${res.statusText} - ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if data is ok
|
// Check if data is ok
|
||||||
if ("success" in data && !data.success) {
|
if ("success" in data && !data.success) {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Types for the result object with discriminated union
|
||||||
|
type Success<T> = {
|
||||||
|
data: T;
|
||||||
|
error: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Failure<E> = {
|
||||||
|
data: null;
|
||||||
|
error: E;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T, E = Error> = Success<T> | Failure<E>;
|
||||||
|
|
||||||
|
// Main wrapper function
|
||||||
|
export async function tryCatch<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> {
|
||||||
|
try {
|
||||||
|
const data = await promise;
|
||||||
|
return { data, error: null };
|
||||||
|
} catch (error) {
|
||||||
|
return { data: null, error: error as E };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// function for sync
|
||||||
|
export function tryCatchSync<T, E = Error>(callback: () => T): Result<T, E> {
|
||||||
|
try {
|
||||||
|
const data = callback();
|
||||||
|
return { data, error: null };
|
||||||
|
} catch (error) {
|
||||||
|
return { data: null, error: error as E };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,48 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { useAppForm } from "@/hooks/formHook";
|
||||||
import Guest from "@/layouts/Guest";
|
import Guest from "@/layouts/Guest";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import requests from "@/lib/requests";
|
||||||
|
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
export const Route = createFileRoute("/login")({
|
export const Route = createFileRoute("/login")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().nonempty("Password is required"),
|
||||||
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
|
const loading = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
validators: {
|
||||||
|
onBlur: loginSchema,
|
||||||
|
},
|
||||||
|
onSubmit: ({ value }) => {
|
||||||
|
requests.post<{ message: string }>("/login", {
|
||||||
|
data: value,
|
||||||
|
before() {
|
||||||
|
// use state to true loading
|
||||||
|
loading[1](true);
|
||||||
|
},
|
||||||
|
success(data) {
|
||||||
|
navigate({ to: "/" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Guest className="h-screen w-screen grid place-items-center">
|
<Guest className="h-screen w-screen grid place-items-center">
|
||||||
<Card className="w-full max-w-[400px]">
|
<Card className="w-full max-w-[400px]">
|
||||||
@@ -21,10 +55,31 @@ function RouteComponent() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardAction>
|
</CardAction>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-3">
|
||||||
<Input type="email" placeholder="Email address" />
|
<form.AppField
|
||||||
<Input type="password" placeholder="Password" />
|
name="email"
|
||||||
<Button className="w-full">Login</Button>
|
children={(f) => (
|
||||||
|
<f.TextField
|
||||||
|
label="Email address"
|
||||||
|
placeholder="Your email address"
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.AppField
|
||||||
|
name="password"
|
||||||
|
children={(f) => (
|
||||||
|
<f.TextField
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your accounts password"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button onClick={form.handleSubmit} className="w-full">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Guest>
|
</Guest>
|
||||||
|
|||||||
Reference in New Issue
Block a user