diff options
| -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 {  |