package users import ( "clortho/db" "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 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.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") } 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{UserID: user.ID} db.Connection.Create(&session) return &session }