diff options
author | Santo Cariotti <santo@dcariotti.me> | 2025-04-03 12:36:34 +0200 |
---|---|---|
committer | Santo Cariotti <santo@dcariotti.me> | 2025-04-03 12:36:34 +0200 |
commit | 0d987f5c97cc8c0e205193ef8c67745ac981d5bf (patch) | |
tree | 8cee10db15c6b36abee89663fe1c7159a6b4d658 | |
parent | a9b84f3f3b1d92335188d43048587e32e0921079 (diff) |
Fix login and register
-rw-r--r-- | api/handlers/handlers.go | 44 | ||||
-rw-r--r-- | cmd/api/main.go | 12 | ||||
-rw-r--r-- | frontend/nuxt.config.ts | 1 | ||||
-rw-r--r-- | frontend/pages/index.vue | 21 | ||||
-rw-r--r-- | frontend/pages/login.vue | 15 | ||||
-rw-r--r-- | frontend/pages/register.vue | 128 | ||||
-rw-r--r-- | go.mod | 3 | ||||
-rw-r--r-- | go.sum | 6 | ||||
-rw-r--r-- | pkg/utils.go | 24 |
9 files changed, 213 insertions, 41 deletions
diff --git a/api/handlers/handlers.go b/api/handlers/handlers.go index 7d5fd10..cc7a9d9 100644 --- a/api/handlers/handlers.go +++ b/api/handlers/handlers.go @@ -2,44 +2,66 @@ package handlers import ( "encoding/json" + "log/slog" "net/http" "github.com/boozec/rahanna/api/auth" "github.com/boozec/rahanna/api/database" + utils "github.com/boozec/rahanna/pkg" "golang.org/x/crypto/bcrypt" ) func RegisterUser(w http.ResponseWriter, r *http.Request) { + slog.Info("POST /register") var user database.User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + utils.JsonError(&w, err.Error()) + return + } + + if len(user.Password) < 4 { + utils.JsonError(&w, "password too short") + return + } + + var storedUser database.User + db, _ := database.GetDb() + result := db.Where("username = ?", user.Username).First(&storedUser) + + if result.Error == nil { + utils.JsonError(&w, "user with this username already exists") return } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + utils.JsonError(&w, err.Error()) return } user.Password = string(hashedPassword) - db, _ := database.GetDb() - - result := db.Create(&user) + result = db.Create(&user) if result.Error != nil { - http.Error(w, result.Error.Error(), http.StatusInternalServerError) + utils.JsonError(&w, result.Error.Error()) + return + } + + token, err := auth.GenerateJWT(user.ID) + if err != nil { + utils.JsonError(&w, err.Error()) return } - w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{"token": token}) } func LoginUser(w http.ResponseWriter, r *http.Request) { + slog.Info("POST /login") var inputUser database.User err := json.NewDecoder(r.Body).Decode(&inputUser) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + utils.JsonError(&w, err.Error()) return } @@ -48,19 +70,19 @@ func LoginUser(w http.ResponseWriter, r *http.Request) { db, _ := database.GetDb() result := db.Where("username = ?", inputUser.Username).First(&storedUser) if result.Error != nil { - http.Error(w, "Invalid credentials", http.StatusUnauthorized) + utils.JsonError(&w, "invalid credentials") return } err = bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(inputUser.Password)) if err != nil { - http.Error(w, "Invalid credentials", http.StatusUnauthorized) + utils.JsonError(&w, "invalid credentials") return } token, err := auth.GenerateJWT(storedUser.ID) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + utils.JsonError(&w, err.Error()) return } diff --git a/cmd/api/main.go b/cmd/api/main.go index 2120d41..ff80c17 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,22 +1,26 @@ package main import ( + "log/slog" + "net/http" "os" "github.com/boozec/rahanna/api/database" "github.com/boozec/rahanna/api/handlers" "github.com/gorilla/mux" - "net/http" + "github.com/rs/cors" ) func main() { database.InitDb(os.Getenv("DATABASE_URL")) r := mux.NewRouter() - r.HandleFunc("/register", handlers.RegisterUser).Methods(http.MethodPost) - r.HandleFunc("/login", handlers.LoginUser).Methods(http.MethodPost) + r.HandleFunc("/auth/register", handlers.RegisterUser).Methods(http.MethodPost) + r.HandleFunc("/auth/login", handlers.LoginUser).Methods(http.MethodPost) - if err := http.ListenAndServe(":8080", r); err != nil { + slog.Info("Serving on :8080") + handler := cors.AllowAll().Handler(r) + if err := http.ListenAndServe(":8080", handler); err != nil { panic(err) } } diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 3e1df8c..df48718 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -2,6 +2,7 @@ export default defineNuxtConfig({ compatibilityDate: "2024-11-01", devtools: { enabled: false }, + ssr: false, modules: ["@nuxt/ui"], css: ["~/assets/css/main.css"], runtimeConfig: { diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index d097617..110db25 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -1,20 +1,7 @@ -<script setup lang="ts"> -const props = defineProps<{ - title: string; -}>(); - -const toast = useToast(); - -function showToast() { - toast.add(props); -} -</script> +<script setup lang="ts"></script> <template> - <UButton - label="Show toast" - color="neutral" - variant="outline" - @click="showToast" - /> + <div> + <h1>Hello</h1> + </div> </template> diff --git a/frontend/pages/login.vue b/frontend/pages/login.vue index 0be0372..5fdb689 100644 --- a/frontend/pages/login.vue +++ b/frontend/pages/login.vue @@ -83,7 +83,7 @@ const handleSubmit = async (event) => { try { error.value = null; isLoading.value = true; - fetch(`${config.public.apiBase}/auth/login`, { + await fetch(`${config.public.apiBase}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json", @@ -92,23 +92,28 @@ const handleSubmit = async (event) => { username: username.value, password: password.value, }), - }).then((response) => { + }).then(async (response) => { + const body = await response.json(); if (response.status != 200) { toast.add({ title: "Login Failed", - description: response.body, + description: body.error, color: "error", }); } else { toast.add({ title: "Login Successful", - description: "You have been successfully logged in.", + description: "You have been successfully logged in", color: "success", }); + + localStorage.setItem("token", body.token); + setTimeout(() => { + window.location.href = "/play"; + }, 1000); } }); } catch (err) { - console.error("Login failed:", err); error.value = err.response?.data?.message || "An error occurred during login"; diff --git a/frontend/pages/register.vue b/frontend/pages/register.vue new file mode 100644 index 0000000..824b53b --- /dev/null +++ b/frontend/pages/register.vue @@ -0,0 +1,128 @@ +<template> + <div + class="flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8" + > + <UCard class="w-full max-w-md bg-gray-900"> + <div class="flex flex-col items-center"> + <h1 + class="text-center text-2xl font-bold tracking-tight text-white" + > + Create a new account + </h1> + <p class="mt-2 text-center text-sm text-gray-200"> + Or + <NuxtLink + to="/login" + class="font-medium text-primary hover:text-primary-dark underline" + > + sign in to your account + </NuxtLink> + </p> + </div> + + <div class="mt-8"> + <form + @submit.prevent="handleSubmit" + class="space-y-6" + method="POST" + > + <UFormField label="Username" name="username"> + <UInput + v-model="username" + name="username" + autocomplete="username" + required + placeholder="mario.rossi" + class="w-full" + /> + </UFormField> + + <UFormField label="Password" name="password"> + <UInput + v-model="password" + type="password" + name="password" + autocomplete="current-password" + required + placeholder="*****" + class="w-full" + /> + </UFormField> + + <div> + <UButton + type="submit" + block + :loading="isLoading" + color="primary" + variant="solid" + class="cursor-pointer" + > + Sign up + </UButton> + </div> + </form> + </div> + </UCard> + </div> +</template> + +<script setup> +const username = ref(""); +const password = ref(""); +const error = ref(""); +const isLoading = ref(false); + +const toast = useToast(); + +const config = useRuntimeConfig(); + +const handleSubmit = async (event) => { + event.preventDefault(); + + try { + error.value = null; + isLoading.value = true; + await fetch(`${config.public.apiBase}/auth/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: username.value, + password: password.value, + }), + }).then(async (response) => { + const body = await response.json(); + if (response.status != 200) { + toast.add({ + title: "Login Failed", + description: body.error, + color: "error", + }); + } else { + toast.add({ + title: "Register Successful", + description: "You have been successfully signed up", + color: "success", + }); + localStorage.setItem("token", body.token); + setTimeout(() => { + window.location.href = "/play"; + }, 1000); + } + }); + } catch (err) { + error.value = + err.response?.data?.message || "An error occurred during login"; + + toast.add({ + title: "Login Failed", + description: error.value, + color: "error", + }); + } finally { + isLoading.value = false; + } +}; +</script> @@ -24,9 +24,10 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.6.1 // indirect + github.com/rs/cors v1.11.1 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect @@ -38,6 +38,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -65,8 +67,8 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= diff --git a/pkg/utils.go b/pkg/utils.go index f5a443e..9246854 100644 --- a/pkg/utils.go +++ b/pkg/utils.go @@ -1,6 +1,11 @@ package utils -import "golang.org/x/crypto/bcrypt" +import ( + "encoding/json" + "net/http" + + "golang.org/x/crypto/bcrypt" +) func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) @@ -11,3 +16,20 @@ func CheckPasswordHash(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } + +// Set a JSON response with status code 400 +func JsonError(w *http.ResponseWriter, error string) { + payloadMap := map[string]string{"error": error} + + (*w).Header().Set("Content-Type", "application/json") + (*w).WriteHeader(http.StatusBadRequest) + + payload, err := json.Marshal(payloadMap) + + if err != nil { + (*w).WriteHeader(http.StatusBadGateway) + (*w).Write([]byte(err.Error())) + } else { + (*w).Write(payload) + } +} |