diff --git a/backend/.gitignore b/backend/.gitignore index 5e63467..056934b 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ go.sum backend +wallet.sqlite diff --git a/backend/api/Accessible.go b/backend/api/Accessible.go deleted file mode 100644 index a3f8e86..0000000 --- a/backend/api/Accessible.go +++ /dev/null @@ -1,17 +0,0 @@ -package api - -import ( - "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt" -) - -func Accessible(c *fiber.Ctx) error { - return c.SendString("Accessible") -} - -func Restricted(c *fiber.Ctx) error { - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - name := claims["name"].(string) - return c.SendString("Welcome " + name) -} diff --git a/backend/api/GetAllUsers.go b/backend/api/GetAllUsers.go new file mode 100644 index 0000000..6d00338 --- /dev/null +++ b/backend/api/GetAllUsers.go @@ -0,0 +1,56 @@ +package api + +import ( + "backend/database" + "backend/model" + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +func GetAllUsers(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + userid := claims["id"].(string) + + dbuser, e := database.GetUserById(userid) + if e != nil { + return c.Status( + fiber.StatusInternalServerError, + ).JSON( + fiber.Map{ + "error": e.Error(), + }, + ) + } + + if !dbuser.IsSuperuser { + return c.Status( + fiber.StatusForbidden, + ).JSON( + fiber.Map{ + "error": "you are not superuser", + }, + ) + } + + users, e := database.GetAllUsers() + if e != nil { + return fmt.Errorf("Error: %v", e) + } + var responseUsers []model.UserResponse + for i := range users { + responseUsers = append( + responseUsers, + model.UserResponse{ + ID: users[i].ID, + Username: users[i].Username, + Email: users[i].Email, + Group: users[i].Group, + IsSuperuser: users[i].IsSuperuser, + }, + ) + } + return c.JSON(responseUsers) +} diff --git a/backend/api/GetUserData.go b/backend/api/GetUserData.go new file mode 100644 index 0000000..5d51504 --- /dev/null +++ b/backend/api/GetUserData.go @@ -0,0 +1,34 @@ +package api + +import ( + "backend/database" + "backend/model" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +func GetUserData(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + userid := claims["id"].(string) + + dbuser, e := database.GetUserById(userid) + if e != nil { + return c.Status( + fiber.StatusInternalServerError, + ).JSON( + fiber.Map{ + "error": e.Error(), + }, + ) + } + + return c.JSON(model.UserResponse{ + ID: dbuser.ID, + Username: dbuser.Username, + Email: dbuser.Email, + Group: dbuser.Group, + IsSuperuser: dbuser.IsSuperuser, + }) +} diff --git a/backend/api/Login.go b/backend/api/Login.go index e5e20d5..6b5eeba 100644 --- a/backend/api/Login.go +++ b/backend/api/Login.go @@ -1,13 +1,14 @@ package api import ( + "backend/database" "backend/model" "fmt" "os" "time" "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" ) func Login(c *fiber.Ctx) error { @@ -16,13 +17,18 @@ func Login(c *fiber.Ctx) error { if err != nil { return fmt.Errorf("Error parse body, %s", err) } - if requestdata.Username != "tola" || requestdata.Password != "2808" { - return c.SendStatus(fiber.StatusUnauthorized) + dbuser, e := database.GetUserByUsername(requestdata.Username) + if e != nil { + return fiber.ErrInternalServerError + } + if dbuser.Password != requestdata.Password { + return fiber.ErrForbidden } claims := jwt.MapClaims{ - "username": "tola", - "is_superuser": true, + "id": dbuser.ID, + "username": dbuser.Username, + "is_superuser": dbuser.IsSuperuser, "exp": time.Now().Add(time.Hour * 72).Unix(), } diff --git a/backend/database/db.go b/backend/database/db.go new file mode 100644 index 0000000..c1d4726 --- /dev/null +++ b/backend/database/db.go @@ -0,0 +1,46 @@ +package database + +import ( + "backend/model" + "os" + + "github.com/glebarez/sqlite" + "github.com/gofiber/fiber/v2/log" + "gorm.io/gorm" +) + +var Db *gorm.DB + +func Init() error { + if os.Getenv("DATABASE") == "" { + log.Fatal("Error no DATABASE path or url") + } + var err error + Db, err = gorm.Open(sqlite.Open(os.Getenv("DATABASE")), &gorm.Config{}) + if err != nil { + log.Error("Failed to connect database") + return err + } + return nil +} + +func Migrate() { + Db.AutoMigrate( + &model.User{}, + &model.Category{}, + &model.Subcategory{}, + &model.Operation{}, + ) +} + +func InitDatabase() { + Init() + Migrate() + + if os.Getenv("USERS_FILE") != "" { + allusers := GetUsersFromFile(os.Getenv("USERS_FILE")) + for i := range allusers { + CreateUser(&allusers[i]) + } + } +} diff --git a/backend/database/users.go b/backend/database/users.go new file mode 100644 index 0000000..d6b36e5 --- /dev/null +++ b/backend/database/users.go @@ -0,0 +1,85 @@ +package database + +import ( + "backend/model" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + "gorm.io/gorm" +) + +func CreateUser(user *model.User) error { + e := Db.Where(model.User{ + Username: user.Username, + }).FirstOrCreate(&user).Error + if e != nil { + return e + } + return nil +} + +func GetUserById(id string) (*model.User, error) { + var user model.User + e := Db.First(&user, "id = ?", id).Error + if e != nil { + if errors.Is(e, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, e + } + return &user, nil +} + +func GetUserByUsername(username string) (*model.User, error) { + var user model.User + e := Db.First(&user, "username = ?", username).Error + if e != nil { + if errors.Is(e, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, e + } + return &user, nil +} + +func DeleteUser(id string) error { + var user model.User + e := Db.First(&user, "id = ?", id).Error + if e != nil { + if errors.Is(e, gorm.ErrRecordNotFound) { + return fmt.Errorf("user not found") + } + return e + } + e = Db.Unscoped().Delete(&user).Error + if e != nil { + if errors.Is(e, gorm.ErrRecordNotFound) { + return fmt.Errorf("user not found") + } + return e + } + return nil +} + +func GetUsersFromFile(filename string) []model.User { + data, e := os.ReadFile(filename) + if e != nil { + panic(e) + } + decoder := json.NewDecoder(strings.NewReader(string(data))) + var res []model.User + decoder.Decode(&res) + return res +} + +func GetAllUsers() ([]model.User, error) { + var users []model.User + e := Db.Find(&users).Error + if e != nil { + return nil, e + } + return users, nil +} diff --git a/backend/go.mod b/backend/go.mod index 1ca3944..57778cc 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,24 +3,36 @@ module backend go 1.24.5 require ( + github.com/glebarez/sqlite v1.11.0 github.com/gofiber/contrib/jwt v1.1.2 github.com/gofiber/fiber/v2 v2.52.9 github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 + gorm.io/gorm v1.30.1 ) require ( github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.20.0 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) diff --git a/backend/internal/category/GetAllCategories.go b/backend/internal/category/GetAllCategories.go index e58bff1..0f04b30 100644 --- a/backend/internal/category/GetAllCategories.go +++ b/backend/internal/category/GetAllCategories.go @@ -5,41 +5,34 @@ import "backend/model" func GetAllCategories() ([]model.Category, error) { return []model.Category{ { - ID: "1", Title: "Electronics", Icon: "😀", Type: "expense", }, { - ID: "2", Title: "Clothing", Icon: "😁", Type: "expense", }, { - ID: "3", Title: "Books", Icon: "😂", Subcategories: []model.Subcategory{ { - ID: "1", Title: "Fiction", }, { - ID: "2", Title: "Non-Fiction", }, }, Type: "expense", }, { - ID: "4", Title: "Salary", Icon: "😀", Type: "income", }, { - ID: "5", Title: "asdf", Icon: "😀", Type: "income", diff --git a/backend/internal/internaljwt/JwtFromBody.go b/backend/internal/internaljwt/JwtFromBody.go new file mode 100644 index 0000000..8da28e6 --- /dev/null +++ b/backend/internal/internaljwt/JwtFromBody.go @@ -0,0 +1,36 @@ +package internaljwt + +import ( + "backend/model" + + "github.com/gofiber/fiber/v2" +) + +func JwtFromBody(c *fiber.Ctx) error { + req := new(model.TokenRequest) + if err := c.BodyParser(req); err != nil { + return c.Status( + fiber.StatusBadRequest, + ).JSON( + fiber.Map{ + "error": "Cannot parse request body", + }, + ) + } + + if req.Token == "" { + return c.Status( + fiber.StatusBadRequest, + ).JSON( + fiber.Map{ + "error": "Token not found in request body", + }, + ) + } + + c.Request().Header.Set( + "Authorization", "Bearer "+req.Token, + ) + + return c.Next() +} diff --git a/backend/internal/operation/GetAllOperation.go b/backend/internal/operation/GetAllOperation.go index 152f404..ffec57b 100644 --- a/backend/internal/operation/GetAllOperation.go +++ b/backend/internal/operation/GetAllOperation.go @@ -8,12 +8,13 @@ import ( func GetAllOperation() ([]model.Operation, error) { return []model.Operation{ { - ID: "1", - Title: "Operation 1", - Amount: 100, - Category: "Category 1", - SubCategory: model.Subcategory{ - ID: "1", + Title: "Operation 1", + Amount: 100, + Category: model.Category{ + Title: "category 1", + Type: "income", + }, + Subcategory: model.Subcategory{ Title: "subcategory", }, Date: "2022-01-01T00:00:00Z", @@ -21,30 +22,35 @@ func GetAllOperation() ([]model.Operation, error) { Icon: "😀", }, { - ID: "2", - Title: "Operation 2", - Amount: 200, - Category: "Category 2", - Date: "2022-01-02T00:00:00Z", - Type: "expense", - Icon: "😂", + Title: "Operation 2", + Amount: 200, + Category: model.Category{ + Title: "Category 2", + Type: "expense", + }, + Date: "2022-01-02T00:00:00Z", + Type: "expense", + Icon: "😂", }, { - ID: "2", - Title: "Operation 2", - Amount: 200, - Category: "Category 2", - Date: time.Now().Format(time.RFC3339), - Type: "expense", - Icon: "😂", + Title: "Operation 2", + Amount: 200, + Category: model.Category{ + Title: "Category 2", + Type: "expense", + }, + Date: time.Now().Format(time.RFC3339), + Type: "expense", + Icon: "😂", }, { - ID: "1", - Title: "Operation 1", - Amount: 100, - Category: "Category 1", - SubCategory: model.Subcategory{ - ID: "1", + Title: "Operation 1", + Amount: 100, + Category: model.Category{ + Title: "Category 2", + Type: "expense", + }, + Subcategory: model.Subcategory{ Title: "subcategory", }, Date: "2022-01-02T00:00:00Z", @@ -52,22 +58,26 @@ func GetAllOperation() ([]model.Operation, error) { Icon: "😀", }, { - ID: "2", - Title: "Operation 2", - Amount: 200, - Category: "Category 2", - Date: "2022-06-02T00:00:00Z", - Type: "expense", - Icon: "😂", + Title: "Operation 2", + Amount: 200, + Category: model.Category{ + Title: "Category 2", + Type: "expense", + }, + Date: "2022-06-02T00:00:00Z", + Type: "expense", + Icon: "😂", }, { - ID: "2", - Title: "Operation 2", - Amount: 200, - Category: "Category 2", - Date: "2022-06-22T00:00:00Z", - Type: "expense", - Icon: "😂", + Title: "Operation 2", + Amount: 200, + Category: model.Category{ + Title: "Category 2", + Type: "expense", + }, + Date: "2022-06-22T00:00:00Z", + Type: "expense", + Icon: "😂", }, }, nil } diff --git a/backend/model/BaseModel.go b/backend/model/BaseModel.go new file mode 100644 index 0000000..c15368b --- /dev/null +++ b/backend/model/BaseModel.go @@ -0,0 +1,18 @@ +package model + +import ( + "github.com/google/uuid" + "gorm.io/gorm" +) + +type BaseModel struct { + ID string `json:"id" gorm:"primaryKey"` +} + +func (base *BaseModel) BeforeCreate(tx *gorm.DB) (err error) { + // generate new uuid if ID not created yet + if base.ID == "" { + base.ID = uuid.New().String() + } + return +} diff --git a/backend/model/category.go b/backend/model/Category.go similarity index 59% rename from backend/model/category.go rename to backend/model/Category.go index 52abbfa..975fbd8 100644 --- a/backend/model/category.go +++ b/backend/model/Category.go @@ -1,9 +1,10 @@ package model type Category struct { - ID string `json:"id"` + BaseModel Icon string `json:"icon"` Title string `json:"title"` - Subcategories []Subcategory `json:"subcategories"` + Subcategories []Subcategory `json:"subcategories" gorm:"foreignKey:CategoryID"` Type string `json:"type"` + UserID string } diff --git a/backend/model/Operation.go b/backend/model/Operation.go new file mode 100644 index 0000000..f6bf85b --- /dev/null +++ b/backend/model/Operation.go @@ -0,0 +1,16 @@ +package model + +type Operation struct { + BaseModel + Title string `json:"title"` + Amount float64 `json:"amount"` + Note string `json:"note,omitempty"` + Icon string `json:"icon"` + Type string `json:"type"` + Date string `json:"date"` + UserID string + CategoryID string + SubcategoryID string + Category Category `gorm:"foreignKey:CategoryID"` + Subcategory Subcategory `gorm:"foreignKey:SubcategoryID"` +} diff --git a/backend/model/Subcategory.go b/backend/model/Subcategory.go new file mode 100644 index 0000000..736d670 --- /dev/null +++ b/backend/model/Subcategory.go @@ -0,0 +1,8 @@ +package model + +type Subcategory struct { + BaseModel + Title string `json:"title"` + CategoryID string + UserID string +} diff --git a/backend/model/Token.go b/backend/model/Token.go new file mode 100644 index 0000000..b35c13c --- /dev/null +++ b/backend/model/Token.go @@ -0,0 +1,5 @@ +package model + +type TokenRequest struct { + Token string `json:"token"` +} diff --git a/backend/model/User.go b/backend/model/User.go new file mode 100644 index 0000000..17c5917 --- /dev/null +++ b/backend/model/User.go @@ -0,0 +1,26 @@ +package model + +type User struct { + BaseModel + Username string `json:"username" gorm:"unique"` + Email string `json:"email" gorm:"unique"` + Password string `json:"password"` + Group string `json:"group"` + IsSuperuser bool `json:"is_superuser"` + Categories []Category `json:"categories" gorm:"foreignKey:UserID"` + Subcategories []Subcategory `json:"subcategories" gorm:"foreignKey:UserID"` + Operations []Operation `json:"operations" gorm:"foreignKey:UserID"` +} + +type AuthRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type UserResponse struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Group string `json:"group"` + IsSuperuser bool `json:"is_superuser"` +} diff --git a/backend/model/operation.go b/backend/model/operation.go deleted file mode 100644 index 1e0e4c5..0000000 --- a/backend/model/operation.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -type Operation struct { - ID string `json:"id"` - Title string `json:"title"` - Amount float64 `json:"amount"` - Category string `json:"category"` - SubCategory Subcategory `json:"subcategory,omitzero"` - Note string `json:"note,omitempty"` - Icon string `json:"icon"` - Type string `json:"type"` - Date string `json:"date"` -} diff --git a/backend/model/subcategory.go b/backend/model/subcategory.go deleted file mode 100644 index 73bf613..0000000 --- a/backend/model/subcategory.go +++ /dev/null @@ -1,6 +0,0 @@ -package model - -type Subcategory struct { - ID string `json:"id"` - Title string `json:"title"` -} diff --git a/backend/model/user.go b/backend/model/user.go deleted file mode 100644 index bedcc61..0000000 --- a/backend/model/user.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -type AuthRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type User struct { - Username string `json:"username"` - Password string `json:"password"` - Email string `json:"email"` - Group string `json:"group"` - IsSuperuser bool `json:"is_superuser"` -} diff --git a/backend/server/configure.go b/backend/server/configure.go index d4b462a..0639528 100644 --- a/backend/server/configure.go +++ b/backend/server/configure.go @@ -2,6 +2,8 @@ package server import ( "backend/api" + "backend/database" + "backend/internal/internaljwt" "backend/utils" "os" @@ -13,6 +15,7 @@ import ( func Configure(app *fiber.App) { utils.LoadEnv("wallet.conf") + database.InitDatabase() log.SetLevel(log.LevelDebug) app.Use(cors.New(cors.Config{ AllowOrigins: "http://localhost:3000", @@ -22,13 +25,39 @@ func Configure(app *fiber.App) { apiGroup := app.Group("/api") apiGroup.Post("/token", api.Login) - apiGroup.Get("/accessible", api.Accessible) - app.Use(jwtware.New(jwtware.Config{ + app.Use(internaljwt.JwtFromBody, jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{ Key: []byte(os.Getenv("JWT_SECRET_KEY")), }, + ErrorHandler: func(c *fiber.Ctx, err error) error { + if err.Error() == "Missing or malformed JWT" { + return c.Status( + fiber.StatusBadRequest, + ).JSON( + fiber.Map{ + "status": "error", + "message": "Missing or malformed JWT", + "data": nil, + }, + ) + } + return c.Status( + fiber.StatusUnauthorized, + ).JSON( + fiber.Map{ + "status": "error", + "message": "Invalid or expired JWT", + "data": nil, + "error": err.Error(), + }, + ) + }, })) - apiGroup.Get("/rescticted", api.Restricted) + + usersGroup := apiGroup.Group("/users") + usersGroup.Get("/", api.GetAllUsers) + usersGroup.Get("/user", api.GetUserData) + operationGroup := apiGroup.Group("/operation") operationGroup.Get("/", api.GetAllOperation) diff --git a/backend/users.json b/backend/users.json new file mode 100644 index 0000000..6ad4817 --- /dev/null +++ b/backend/users.json @@ -0,0 +1,16 @@ +[ + { + "username": "tola", + "email": "mail@example.com", + "password": "2808", + "group": "root", + "is_superuser": true + }, + { + "username": "lisa", + "email": "lisa@example.com", + "password": "asdf", + "group": "users", + "is_superuser": false + } +] diff --git a/backend/wallet.conf b/backend/wallet.conf index c9dd73b..10b43d1 100644 --- a/backend/wallet.conf +++ b/backend/wallet.conf @@ -3,3 +3,5 @@ HTTP_ADDRESS=0.0.0.0:43243 ENABLE_TLS=true CERT_FILE=/home/tola/certs/localhost/localhost.crt KEY_FILE=/home/tola/certs/localhost/localhost.key +USERS_FILE=users.json +DATABASE=wallet.sqlite