summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSanto Cariotti <santo@dcariotti.me>2025-04-03 12:36:34 +0200
committerSanto Cariotti <santo@dcariotti.me>2025-04-03 12:36:34 +0200
commit0d987f5c97cc8c0e205193ef8c67745ac981d5bf (patch)
tree8cee10db15c6b36abee89663fe1c7159a6b4d658
parenta9b84f3f3b1d92335188d43048587e32e0921079 (diff)
Fix login and register
-rw-r--r--api/handlers/handlers.go44
-rw-r--r--cmd/api/main.go12
-rw-r--r--frontend/nuxt.config.ts1
-rw-r--r--frontend/pages/index.vue21
-rw-r--r--frontend/pages/login.vue15
-rw-r--r--frontend/pages/register.vue128
-rw-r--r--go.mod3
-rw-r--r--go.sum6
-rw-r--r--pkg/utils.go24
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>
diff --git a/go.mod b/go.mod
index 882b0b3..b0c1d27 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 5ef9159..9b02a5b 100644
--- a/go.sum
+++ b/go.sum
@@ -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)
+ }
+}