diff options
author | Vidhu Kant Sharma <vidhukant@vidhukant.com> | 2025-10-12 00:05:30 +0530 |
---|---|---|
committer | Vidhu Kant Sharma <vidhukant@vidhukant.com> | 2025-10-12 00:05:30 +0530 |
commit | a356803594ab36fa69e7dbcbd79261d8b46f4262 (patch) | |
tree | 0146efe4c52b1c65c1967ab1f412306c410c10d4 | |
parent | 193be465b21838d2796fafbe1c5d9854038a3f8c (diff) |
-rw-r--r-- | auth/auth.go | 5 | ||||
-rw-r--r-- | auth/controller.go | 36 | ||||
-rw-r--r-- | auth/middleware.go | 3 | ||||
-rw-r--r-- | conf/conf.go | 11 | ||||
-rw-r--r-- | main.go | 4 | ||||
-rw-r--r-- | openbills.toml | 5 | ||||
-rw-r--r-- | user/controller.go | 116 | ||||
-rw-r--r-- | user/router.go | 2 | ||||
-rw-r--r-- | user/service.go | 35 | ||||
-rw-r--r-- | user/user.go | 55 | ||||
-rw-r--r-- | user/validators.go | 60 |
11 files changed, 90 insertions, 242 deletions
diff --git a/auth/auth.go b/auth/auth.go index 4ac6445..0b28b57 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,5 +1,5 @@ /* openbills - Server for web based Libre Billing Software - * Copyright (C) 2023 Vidhu Kant Sharma <vidhukant@vidhukant.com> + * Copyright (C) 2023-2025 Vidhu Kant Sharma <vidhukant@vidhukant.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,7 +23,8 @@ import ( type AuthClaims struct { jwt.RegisteredClaims - UserID uint `json:"userid"` + UserID uint `json:"userid"` + Roles []string `json:"roles"` } type RefreshClaims struct { diff --git a/auth/controller.go b/auth/controller.go index 961518a..8de7370 100644 --- a/auth/controller.go +++ b/auth/controller.go @@ -1,5 +1,5 @@ /* openbills - Server for web based Libre Billing Software - * Copyright (C) 2023 Vidhu Kant Sharma <vidhukant@vidhukant.com> + * Copyright (C) 2023-2025 Vidhu Kant Sharma <vidhukant@vidhukant.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,36 +39,37 @@ func init() { } func handleSignUp (ctx *gin.Context) { - var user user.User - ctx.Bind(&user) + var u user.User + ctx.Bind(&u) var err error // hash password var bytes []byte - bytes, err = bcrypt.GenerateFromPassword([]byte(user.Password), 14) + bytes, err = bcrypt.GenerateFromPassword([]byte(u.Password), 14) if err != nil { // TODO: handle potential errors ctx.Error(err) ctx.Abort() return } - user.Password = string(bytes) + u.Password = string(bytes) + + // for now everyone's an admin + // TODO: fix this shit + u.Roles = []user.Role{ + {0, 0, "admin"}, + } - err = user.Create() + err = u.Create() if err != nil { ctx.Error(err) ctx.Abort() return } - // remove password hash from response - user.Password = "" - - ctx.JSON(http.StatusOK, gin.H{ - "message": "success", - "data": user, - }) + // TODO: email verification and shit before this + ctx.JSON(http.StatusOK, nil) } func handleSignIn (ctx *gin.Context) { @@ -93,6 +94,7 @@ func handleSignIn (ctx *gin.Context) { ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 2)), }, u.ID, + user.RolesToStringList(u.Roles), }, ).SignedString(AUTH_KEY) if err != nil { @@ -125,7 +127,6 @@ func handleSignIn (ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{ "auth_token": authToken, "refresh_token": refreshToken, - "message": "success", "data": u, }) } @@ -147,9 +148,10 @@ func handleRefresh (ctx *gin.Context) { // check token version var u user.User - err := user.GetUser(&u, claims.UserID) + err := user.GetUserById(&u, claims.UserID) if err != nil { if err == errors.ErrNotFound { + // user doesn't exist ctx.Error(errors.ErrUnauthorized) ctx.Abort() return @@ -184,7 +186,8 @@ func handleRefresh (ctx *gin.Context) { IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 2)), }, - claims.UserID, + u.ID, + user.RolesToStringList(u.Roles), }, ).SignedString(AUTH_KEY) if err != nil { @@ -196,6 +199,5 @@ func handleRefresh (ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{ "auth_token": authToken, - "message": "success", }) } diff --git a/auth/middleware.go b/auth/middleware.go index 9ce5e12..80e512e 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -70,6 +70,9 @@ func Authorize() gin.HandlerFunc { return } + ctx.Set("UserID", claims.UserID) + ctx.Set("Roles", claims.Roles) + ctx.Next() } } diff --git a/conf/conf.go b/conf/conf.go index 843bbea..4aa2bcd 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -1,5 +1,5 @@ /* openbills - Server for web based Libre Billing Software - * Copyright (C) 2023-2024 Vidhu Kant Sharma <vidhukant@vidhukant.com> + * Copyright (C) 2023-2025 Vidhu Kant Sharma <vidhukant@vidhukant.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,11 +35,6 @@ func validateConf() { ok = false } - if viper.GetInt("username.min_username_length") < 1 { - log.Println("\x1b[41m\x1b[30m[err]\x1b[0m Minimum username length must be greater than 0.") - ok = false - } - minPassLen := viper.GetInt("security.min_password_length") maxPassLen := viper.GetInt("security.max_password_length") @@ -95,10 +90,6 @@ func init() { viper.SetDefault("instance.description", "Libre Billing Software") viper.SetDefault("instance.url", "https://openbills.vidhukant.com") - viper.SetDefault("username.allowed_characters", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-_") - viper.SetDefault("username.min_username_length", 2) - viper.SetDefault("username.max_username_length", 20) - viper.SetDefault("cryptography.password_hashing_cost", bcrypt.DefaultCost) viper.SetDefault("data.upload_dir", "./data/") @@ -1,5 +1,5 @@ /* openbills - Server for web based Libre Billing Software - * Copyright (C) 2023-2024 Vidhu Kant Sharma <vidhukant@vidhukant.com> + * Copyright (C) 2023-2025 Vidhu Kant Sharma <vidhukant@vidhukant.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,7 +38,7 @@ import ( "log" ) -const OPENBILLS_VERSION = "v0.18.0" +const OPENBILLS_VERSION = "v0.19.0" func init() { if !viper.GetBool("debug_mode") { diff --git a/openbills.toml b/openbills.toml index 0742aaf..c5b569e 100644 --- a/openbills.toml +++ b/openbills.toml @@ -17,11 +17,6 @@ url = "https://openbills.vidhukant.com/" min_password_length = 12 max_password_length = 72 -[username] -allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-_" -min_username_length = 2 -max_username_length = 32 - [cryptography] password_hashing_cost = 14 auth_key = "22ELiOfHn19s0z1WWgsOT9RupghRYrXm" diff --git a/user/controller.go b/user/controller.go index 1dc85da..7dd519a 100644 --- a/user/controller.go +++ b/user/controller.go @@ -1,5 +1,5 @@ /* openbills - Server for web based Libre Billing Software - * Copyright (C) 2023-2024 Vidhu Kant Sharma <vidhukant@vidhukant.com> + * Copyright (C) 2023-2025 Vidhu Kant Sharma <vidhukant@vidhukant.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,8 +20,6 @@ package user import ( e "vidhukant.com/openbills/errors" "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/spf13/viper" "net/http" ) @@ -37,7 +35,7 @@ func handleGetUser (ctx *gin.Context) { userId := uId.(uint) - err := GetUser(&user, userId) + err := GetUserById(&user, userId) if err != nil { ctx.Error(err) ctx.Abort() @@ -48,116 +46,6 @@ func handleGetUser (ctx *gin.Context) { user.Password = "" ctx.JSON(http.StatusOK, gin.H{ - "message": "success", "data": user, }) } - -func handleUploadLogo(ctx *gin.Context) { - var user User - - uId, ok := ctx.Get("UserID") - if !ok { - ctx.Error(e.ErrUnauthorized) - ctx.Abort() - return - } - - userId := uId.(uint) - user.ID = userId - - // TODO: handle potential errors - file, err := ctx.FormFile("logo") - if err != nil { - ctx.Error(err) - ctx.Abort() - return - } - - dest := uuid.New().String() - - // TODO: handle potential errors - err = ctx.SaveUploadedFile(file, viper.GetString("data.upload_dir") + dest) - if err != nil { - ctx.Error(err) - ctx.Abort() - return - } - - // TODO: delete old file (if any) - err = user.update(map[string]interface{}{"logo_file": dest}) - if err != nil { - ctx.Error(err) - ctx.Abort() - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "message": "success", - }) -} - -func handleUploadSignature(ctx *gin.Context) { - var user User - - uId, ok := ctx.Get("UserID") - if !ok { - ctx.Error(e.ErrUnauthorized) - ctx.Abort() - return - } - - userId := uId.(uint) - user.ID = userId - - // TODO: handle potential errors - file, err := ctx.FormFile("signature") - if err != nil { - ctx.Error(err) - ctx.Abort() - return - } - - dest := uuid.New().String() - - // TODO: handle potential errors - err = ctx.SaveUploadedFile(file, viper.GetString("data.upload_dir") + dest) - if err != nil { - ctx.Error(err) - ctx.Abort() - return - } - - // TODO: delete old file (if any) - err = user.update(map[string]interface{}{"signature_file": dest}) - if err != nil { - ctx.Error(err) - ctx.Abort() - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "message": "success", - }) -} - -// TODO: fix this stuff -// also add some kind of 2 factor verification -func handleDelUser (ctx *gin.Context) { - id := uint(1) // get from JWT - - var user User - user.ID = id - - // TODO: add a verification mechanism - err := user.del() - if err != nil { - ctx.Error(err) - ctx.Abort() - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "message": "success", - }) -} diff --git a/user/router.go b/user/router.go index d9fa7e0..eb7270a 100644 --- a/user/router.go +++ b/user/router.go @@ -25,7 +25,5 @@ func Routes(route *gin.RouterGroup) { g := route.Group("/user") { g.GET("/", handleGetUser) - g.POST("/logo", handleUploadLogo) - g.POST("/signature", handleUploadSignature) } } diff --git a/user/service.go b/user/service.go index 222df4a..4dec8bc 100644 --- a/user/service.go +++ b/user/service.go @@ -1,5 +1,5 @@ /* openbills - Server for web based Libre Billing Software - * Copyright (C) 2023-2024 Vidhu Kant Sharma <vidhukant@vidhukant.com> + * Copyright (C) 2023-2025 Vidhu Kant Sharma <vidhukant@vidhukant.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,14 +27,12 @@ func (u *User) Create() error { return res.Error } -func GetUserWithAccountName(user *User, accountName, method string) error { +func GetUserByAccountName(user *User, accountName, method string) error { if method != "username" && method != "email" { return e.ErrInvalidLoginMethod } - res := db.Where(method + " = ?", accountName).Find(&user) - - // TODO: handle potential errors + res := db.Where(method + " = ?", accountName).Preload("Roles").Find(&user) if res.Error != nil { return res.Error } @@ -46,10 +44,8 @@ func GetUserWithAccountName(user *User, accountName, method string) error { return nil } -func GetUser(user *User, id uint) error { - res := db.Find(&user, id) - - // TODO: handle potential errors +func GetUserById(user *User, id uint) error { + res := db.Preload("Roles").Find(&user, id) if res.Error != nil { return res.Error } @@ -63,8 +59,6 @@ func GetUser(user *User, id uint) error { func (u *User) del() error { res := db.Delete(u) - - // TODO: handle potential errors if res.Error != nil { return res.Error } @@ -76,21 +70,4 @@ func (u *User) del() error { return nil } -func (u *User) update(changes map[string]interface{}) error { - res := db.Model(&u). - Omit("email"). - Omit("password"). - Omit("username"). - Updates(changes) - - // TODO: handle potential errors - if res.Error != nil { - return res.Error - } - - if res.RowsAffected == 0 { - return e.ErrNotFound - } - - return nil -} +// TODO: email/password updation (no username changes) with OTP verification or something diff --git a/user/user.go b/user/user.go index dbcbad0..4d0ffcb 100644 --- a/user/user.go +++ b/user/user.go @@ -1,5 +1,5 @@ /* openbills - Server for web based Libre Billing Software - * Copyright (C) 2023-2024 Vidhu Kant Sharma <vidhukant@vidhukant.com> + * Copyright (C) 2023-2025 Vidhu Kant Sharma <vidhukant@vidhukant.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,7 +20,6 @@ package user import ( d "vidhukant.com/openbills/db" e "vidhukant.com/openbills/errors" - u "vidhukant.com/openbills/util" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "github.com/spf13/viper" @@ -32,35 +31,45 @@ var db *gorm.DB func init() { db = d.DB - db.AutoMigrate(&User{}) + db.AutoMigrate(&User{}, &Role{}) COST = viper.GetInt("cryptography.password_hashing_cost") } +var VALID_ROLES []string = []string { + "customer.*", "customer.read", "customer.write", "customer.delete", + "item.*", "item.read", "item.write", "item.delete", + "invoice.*", "invoice.read", "invoice.write", "invoice.delete", + "admin", "*.*", +} + +type Role struct { + ID uint + UserID uint + Name string +} + type User struct { - gorm.Model - u.Address - TokenVersion uint // this can be incremented to disable existing refresh token(s) - FullName string - FirmName string - Gstin string - Phone string - Email string - Website string - Username string - Password string - LogoFile string - SignatureFile string - IsVerified bool // this should be removed and tokens should be issued upon verification - // will be printed with address on the invoice - Details string - // a note is printed on every invoice. - // This is the default that gets automatically set - DefaultInvoiceNote string + ID uint + TokenVersion uint // this can be incremented to disable existing refresh token(s) + Username string + Email string + Password string + Roles []Role `gorm:"constraint:OnDelete:CASCADE;"` +} + +func RolesToStringList(roles []Role) []string { + x := []string{} + + for _, i := range roles { + x = append(x, i.Name) + } + + return x } func CheckPassword(user *User, accountName, method, pass string) error { - err := GetUserWithAccountName(user, accountName, method) + err := GetUserByAccountName(user, accountName, method) if err != nil { return err } diff --git a/user/validators.go b/user/validators.go index e497122..e9a894c 100644 --- a/user/validators.go +++ b/user/validators.go @@ -1,5 +1,5 @@ /* openbills - Server for web based Libre Billing Software - * Copyright (C) 2023-2024 Vidhu Kant Sharma <vidhukant@vidhukant.com> + * Copyright (C) 2023-2025 Vidhu Kant Sharma <vidhukant@vidhukant.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,45 +40,40 @@ func validatePassword(pass string) error { func validateUsername(username string) error { // check if username is too short - if len(username) < viper.GetInt("username.min_username_length") { + if len(username) < 2 { return errors.ErrUsernameTooShort } // check if username is too long - if len(username) > viper.GetInt("username.max_username_length") { + if len(username) > 32 { return errors.ErrUsernameTooLong } - - for _, char := range username { - if !strings.Contains(username, string(char)) { - return errors.ErrInvalidUsername - } - } + + // (11th October 2025) what the fuck even is this + // I'm not even deleting this I can't stop laughing + // + // for _, char := range username { + // if !strings.Contains(username, string(char)) { + // return errors.ErrInvalidUsername + // } + // } return nil } func (u *User) validate() error { - u.Username = strings.TrimSpace(u.Username) u.Email = strings.TrimSpace(u.Email) - u.Phone = strings.TrimSpace(u.Phone) - u.Website = strings.TrimSpace(u.Website) - u.Gstin = strings.TrimSpace(u.Gstin) - u.IsVerified = false - - // don't validate if GSTIN is empty - if u.Gstin != "" && !util.ValidateGstin(u.Gstin) { - return errors.ErrInvalidGSTIN - } - - // don't validate if phone is empty - if u.Phone != "" && !util.ValidatePhone(u.Phone) { - return errors.ErrInvalidPhone - } + u.Username = strings.TrimSpace(u.Username) - // don't validate if website is empty - if u.Website != "" && !util.ValidateWebsite(u.Website) { - return errors.ErrInvalidWebsite + // don't accept empty username + if u.Username == "" { + return errors.ErrEmptyUsername + } else { + // validate username + err := validateUsername(u.Username) + if err != nil { + return err + } } // don't accept empty email @@ -91,17 +86,6 @@ func (u *User) validate() error { } } - // don't accept empty username - if u.Username == "" { - return errors.ErrEmptyUsername - } else { - // validate username - err := validateUsername(u.Username) - if err != nil { - return err - } - } - // validate password err := validatePassword(u.Password) if err != nil { |