clortho/lib/users/users.go
Maxime Duchene-Savard ea2bb235a2 work
2025-04-14 23:50:22 -04:00

158 lines
3.9 KiB
Go

package users
import (
"clortho/lib/db"
"clortho/lib/utils"
"crypto/rand"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"math/big"
"os"
"strconv"
)
var secretKey = []byte("your-secret-key")
// GetPepper retrieves the pepper from the environment variable.
func getPepper() string {
return os.Getenv("CLORTHO_PEPPER") // Example: Set PASSWORD_PEPPER in your .env or system environment
}
// HashPassword hashes a password with bcrypt and an optional pepper.
func HashPassword(password string) (string, error) {
pepper := getPepper()
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password+pepper), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedBytes), nil
}
// CheckPasswordHash verifies if a plaintext password + pepper matches a stored hash.
func CheckPasswordHash(password, hash string) bool {
pepper := getPepper()
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+pepper))
return err == nil
}
func GetUser(username string) *db.User {
var user db.User
result := db.Connection.Where("username = ?", username).First(&user)
if result.RowsAffected != 0 {
return &user
} else {
return nil
}
}
func CreateUser(username string) (*db.User, error) {
user := db.User{Username: username}
result := db.Connection.Create(&user)
if result.Error != nil {
return nil, result.Error
}
return &user, nil
}
func GetUsers() []db.User {
var users []db.User
db.Connection.Find(&users)
return users
}
func GetSession(id uint) *db.UserSession {
var session db.UserSession
result := db.Connection.Joins("User").First(&session, id)
if result.RowsAffected != 0 {
return &session
} else {
return nil
}
}
func InitAdminUser() (*string, error) {
username := "admin"
user := GetUser(username)
if user != nil {
return nil, errors.New("Admin user already exists")
}
var pass string
if utils.IsDev() {
pass = "admin"
} else {
pass, _ = GenerateSecurePassword(32)
}
hash, _ := HashPassword(pass)
admin := db.User{Username: username, DisplayName: &username, Admin: true, PasswordHash: &hash}
result := db.Connection.Create(&admin)
if result.RowsAffected == 0 || result.Error != nil {
return nil, errors.New(fmt.Sprintf("Unable to create admin user: %s", result.Error))
}
return &pass, nil
}
const (
lowercase = "abcdefghijklmnopqrstuvwxyz"
uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
digits = "0123456789"
specialChars = "!@#$%^&*()-_=+[]{}|;:,.<>?/~"
allChars = lowercase + uppercase + digits + specialChars
)
// GenerateSecurePassword generates a cryptographically secure password
func GenerateSecurePassword(length int) (string, error) {
if length < 1 {
return "", fmt.Errorf("password length must be at least 1")
}
password := make([]byte, length)
for i := range password {
index, _ := rand.Int(rand.Reader, big.NewInt(int64(len(allChars))))
password[i] = allChars[index.Int64()]
}
return string(password), nil
}
func GetSessionFromCookie(authCookie string) (*db.UserSession, error) {
token, err := jwt.Parse(authCookie, func(token *jwt.Token) (interface{}, error) {
return secretKey, nil
})
if err != nil || !token.Valid {
return nil, errors.New("invalid or expired token")
}
subject, err := token.Claims.GetSubject()
if err != nil {
return nil, errors.New("invalid or expired token")
}
sessionId, err := strconv.ParseInt(subject, 10, 64)
if err != nil {
return nil, errors.New("invalid or expired token")
}
session := GetSession(uint(sessionId))
if session == nil {
return nil, errors.New("invalid or expired token")
}
return session, nil
}
func GenerateJwt(sessionId uint) (string, error) {
claims := jwt.RegisteredClaims{Subject: fmt.Sprint(sessionId)}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secretKey)
}
func NewSession(user db.User) *db.UserSession {
session := db.UserSession{User: &user}
db.Connection.Create(&session)
return &session
}