@@ -62,7 +58,7 @@
diff --git a/front/src/api.js b/front/src/api.js
index c8acbf8..b4dbe68 100644
--- a/front/src/api.js
+++ b/front/src/api.js
@@ -19,21 +19,43 @@ export function getBooks() {
}
export function postBook(book) {
- return fetch(baseUrl + '/book', {
+ return genericPostCall('/book', book.value)
+}
+
+export function postLogin(user) {
+ return genericPostCall('/auth/login', user.value)
+}
+
+export function postSignUp(user) {
+ return genericPostCall('/auth/signup', user.value)
+}
+
+export function genericPostCall(apiRoute, object) {
+ return fetch(baseUrl + apiRoute, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
- body: JSON.stringify(book.value)
+ body: JSON.stringify(object)
})
}
-export function postSignup(user) {
- return fetch(baseUrl + '/auth/signup', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(user.value)
- })
+export function extractFromErrorFromField(fieldName, errors) {
+ if (errors === null || !('field' in errors)) {
+ return "";
+ }
+ const titleErr = errs.find((e) => e["field"] === fieldName);
+ if (typeof titleErr !== 'undefined') {
+ return titleErr.error;
+ } else {
+ return "";
+ }
+}
+
+export function extractGlobalFormError(errors) {
+ if (errors !== null && "error" in errors) {
+ return errors["error"];
+ } else {
+ return "";
+ }
}
diff --git a/front/src/auth.store.js b/front/src/auth.store.js
new file mode 100644
index 0000000..e718dea
--- /dev/null
+++ b/front/src/auth.store.js
@@ -0,0 +1,21 @@
+import { defineStore } from 'pinia';
+import { useRouter } from 'vue-router'
+
+export const useAuthStore = defineStore('auth', {
+ state: () => ({
+ // initialize state from local storage to enable user to stay logged in
+ user: JSON.parse(localStorage.getItem('user')),
+ returnUrl: null
+ }),
+ actions: {
+ login(user) {
+ this.user = user;
+ localStorage.setItem('user', JSON.stringify(user));
+ },
+ logout() {
+ this.user = null;
+ localStorage.removeItem('user');
+ useRouter().push('/');
+ }
+ }
+});
diff --git a/front/src/main.js b/front/src/main.js
index c9c2219..99a86a0 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -1,15 +1,18 @@
import { createApp } from 'vue'
+import { createPinia } from 'pinia'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import BooksBrowser from './BooksBrowser.vue'
import AddBook from './AddBook.vue'
import SignUp from './SignUp.vue'
+import LogIn from './LogIn.vue'
const routes = [
{ path: '/', component: BooksBrowser },
{ path: '/add', component: AddBook },
{ path: '/signup', component: SignUp },
+ { path: '/login', component: LogIn },
]
export const router = createRouter({
@@ -17,4 +20,6 @@ export const router = createRouter({
routes,
})
-createApp(App).use(router).mount('#app')
+const pinia = createPinia()
+
+createApp(App).use(pinia).use(router).mount('#app')
diff --git a/go.mod b/go.mod
index cd44c45..ceb2877 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
diff --git a/go.sum b/go.sum
index 4a42af1..9ed2b7f 100644
--- a/go.sum
+++ b/go.sum
@@ -28,6 +28,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
diff --git a/internal/api/dto.go b/internal/api/dto.go
index 0ae5657..0d65765 100644
--- a/internal/api/dto.go
+++ b/internal/api/dto.go
@@ -6,7 +6,12 @@ type bookPostCreate struct {
Rating int `json:"rating" binding:"min=0,max=10"`
}
-type userPostCreate struct {
+type userSignup struct {
+ Username string `json:"username" binding:"required,min=2,max=20"`
+ Password string `json:"password" binding:"required,min=6,max=100"`
+}
+
+type userLogin struct {
Username string `json:"username" binding:"required,min=2,max=20"`
Password string `json:"password" binding:"required,min=6,max=100"`
}
diff --git a/internal/api/mapper.go b/internal/api/mapper.go
index 7b602a6..a2ca2c9 100644
--- a/internal/api/mapper.go
+++ b/internal/api/mapper.go
@@ -13,7 +13,7 @@ func (b bookPostCreate) toBook() model.Book {
}
}
-func (u userPostCreate) toUser() (model.User, error) {
+func (u userSignup) toUser() (model.User, error) {
user := model.User{
Name: u.Username,
Password: "",
diff --git a/internal/api/routes.go b/internal/api/routes.go
index fbcecda..d2881f2 100644
--- a/internal/api/routes.go
+++ b/internal/api/routes.go
@@ -2,11 +2,14 @@ package api
import (
"errors"
+ "fmt"
"net/http"
+ "git.artlef.fr/PersonalLibraryManager/internal/jwtauth"
"git.artlef.fr/PersonalLibraryManager/internal/model"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
+ "golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
@@ -32,8 +35,8 @@ func PostBookHandler(c *gin.Context, db *gorm.DB) {
c.String(200, "Success")
}
-func PostUserHandler(c *gin.Context, db *gorm.DB) {
- var user userPostCreate
+func PostSignupHandler(c *gin.Context, db *gorm.DB) {
+ var user userSignup
err := c.ShouldBindJSON(&user)
if err != nil {
manageBindingError(c, err)
@@ -52,6 +55,37 @@ func PostUserHandler(c *gin.Context, db *gorm.DB) {
c.String(200, "Success")
}
+func PostLoginHandler(c *gin.Context, db *gorm.DB) {
+ var user userLogin
+ err := c.ShouldBindJSON(&user)
+ if err != nil {
+ manageBindingError(c, err)
+ return
+ }
+
+ if !isUserAndPasswordOk(db, user.Username, user.Password) {
+ c.JSON(http.StatusInternalServerError,
+ gin.H{"error": "Invalid credentials."})
+ return
+ }
+
+ var jwtToken string
+ jwtToken, err = jwtauth.GenerateJwtToken(user.Username)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized,
+ gin.H{"error": fmt.Errorf("Error when generating JWT token: %w", err)})
+ return
+ }
+ c.JSON(200, gin.H{"message": "Authentication was a success.", "token": jwtToken})
+}
+
+func isUserAndPasswordOk(db *gorm.DB, username string, password string) bool {
+ var user model.User
+ db.Where("name = ?", username).First(&user)
+ err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
+ return err == nil
+}
+
func manageBindingError(c *gin.Context, err error) {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
diff --git a/internal/jwtauth/jwt.go b/internal/jwtauth/jwt.go
new file mode 100644
index 0000000..70ab423
--- /dev/null
+++ b/internal/jwtauth/jwt.go
@@ -0,0 +1,22 @@
+package jwtauth
+
+import (
+ "encoding/base64"
+ "os"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+func GenerateJwtToken(username string) (string, error) {
+ var s string
+ key, err := base64.URLEncoding.DecodeString(os.Getenv(getKeyVariableName()))
+ if err != nil {
+ return s, err
+ }
+ t := jwt.NewWithClaims(jwt.SigningMethodHS256,
+ jwt.MapClaims{
+ "iss": "PersonalLibraryManager",
+ "sub": username,
+ })
+ return t.SignedString(key)
+}
diff --git a/internal/jwtauth/key.go b/internal/jwtauth/key.go
new file mode 100644
index 0000000..2c277e1
--- /dev/null
+++ b/internal/jwtauth/key.go
@@ -0,0 +1,39 @@
+package jwtauth
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "os"
+)
+
+func generateRandomBytes(n int) ([]byte, error) {
+ b := make([]byte, n)
+ _, err := rand.Read(b)
+ if err != nil {
+ return nil, err
+ }
+ return b, nil
+}
+
+func generateSecureToken(n int) (string, error) {
+ bytes, err := generateRandomBytes(n)
+ if err != nil {
+ return "", err
+ }
+ return base64.URLEncoding.EncodeToString(bytes), nil
+}
+
+func getKeyVariableName() string {
+ return "PLM_JWT_KEY"
+}
+
+func InitKey() error {
+ var err error
+ keyName := getKeyVariableName()
+ key := os.Getenv(keyName)
+ if key == "" {
+ key, err = generateSecureToken(64)
+ os.Setenv(keyName, key)
+ }
+ return err
+}
diff --git a/main.go b/main.go
index 7c480c2..82048bc 100644
--- a/main.go
+++ b/main.go
@@ -7,6 +7,7 @@ import (
"git.artlef.fr/PersonalLibraryManager/internal/api"
"git.artlef.fr/PersonalLibraryManager/internal/config"
"git.artlef.fr/PersonalLibraryManager/internal/db"
+ "git.artlef.fr/PersonalLibraryManager/internal/jwtauth"
)
func main() {
@@ -17,6 +18,10 @@ func main() {
func setup(config *config.Config) *gin.Engine {
db := db.Initdb(config.DatabaseFilePath, config.DemoDataPath)
+ err := jwtauth.InitKey()
+ if err != nil {
+ panic(err)
+ }
r := gin.Default()
r.Use(cors.Default()) // All origins allowed by default
r.GET("/books", func(c *gin.Context) {
@@ -26,7 +31,10 @@ func setup(config *config.Config) *gin.Engine {
api.PostBookHandler(c, db)
})
r.POST("/auth/signup", func(c *gin.Context) {
- api.PostUserHandler(c, db)
+ api.PostSignupHandler(c, db)
+ })
+ r.POST("/auth/login", func(c *gin.Context) {
+ api.PostLoginHandler(c, db)
})
return r
}