summaryrefslogtreecommitdiff
path: root/models/user.go
diff options
context:
space:
mode:
Diffstat (limited to 'models/user.go')
-rw-r--r--models/user.go306
1 files changed, 228 insertions, 78 deletions
diff --git a/models/user.go b/models/user.go
index f2b4e22..de440cc 100644
--- a/models/user.go
+++ b/models/user.go
@@ -3,132 +3,282 @@ package models
import (
"auth-proxy/database"
"auth-proxy/types"
- "database/sql"
-
- "golang.org/x/crypto/bcrypt"
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
)
-// HashPassword generates a bcrypt hash of the password
-func HashPassword(password string) (string, error) {
- bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
- return string(bytes), err
+type pbUserRecord struct {
+ ID string `json:"id"`
+ Username string `json:"username"`
+ Role string `json:"role"`
+ IsActive *bool `json:"is_active"`
+ FailedLogins any `json:"failed_logins"`
+ LastLoginAt string `json:"last_login_at"`
+ Created string `json:"created"`
+ Updated string `json:"updated"`
}
-// CheckPasswordHash compares a password with a hash
-func CheckPasswordHash(password, hash string) bool {
- err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
- return err == nil
+func parsePBTime(raw string) (*time.Time, error) {
+ if raw == "" {
+ return nil, nil
+ }
+
+ formats := []string{time.RFC3339Nano, "2006-01-02 15:04:05.000Z", "2006-01-02 15:04:05.000"}
+ for _, format := range formats {
+ if ts, err := time.Parse(format, raw); err == nil {
+ return &ts, nil
+ }
+ }
+
+ return nil, fmt.Errorf("unsupported time format: %s", raw)
}
-// CreateUser creates a new user in the database
-func CreateUser(username, password, role string) (*types.User, error) {
- passwordHash, err := HashPassword(password)
+func parsePBTimeValue(raw string) (time.Time, error) {
+ ts, err := parsePBTime(raw)
if err != nil {
- return nil, err
+ return time.Time{}, err
}
+ if ts == nil {
+ return time.Time{}, nil
+ }
+ return *ts, nil
+}
+
+func parseFailedLogins(value any) int {
+ switch v := value.(type) {
+ case float64:
+ return int(v)
+ case int:
+ return v
+ case int64:
+ return int(v)
+ case string:
+ i, _ := strconv.Atoi(v)
+ return i
+ default:
+ return 0
+ }
+}
+
+func normalizeRole(role string) string {
+ if role == "admin" {
+ return "admin"
+ }
+ return "user"
+}
- query := database.Rebind("INSERT INTO users(username, password_hash, role) VALUES(?, ?, ?)")
- stmt, err := database.DB.Prepare(query)
+func normalizeUser(record pbUserRecord) (*types.User, error) {
+ lastLoginAt, err := parsePBTime(record.LastLoginAt)
if err != nil {
return nil, err
}
- defer stmt.Close() //nolint:errcheck
+ createdAt, err := parsePBTimeValue(record.Created)
+ if err != nil {
+ return nil, err
+ }
+ updatedAt, err := parsePBTimeValue(record.Updated)
+ if err != nil {
+ return nil, err
+ }
+
+ isActive := true
+ if record.IsActive != nil {
+ isActive = *record.IsActive
+ }
- _, err = stmt.Exec(username, passwordHash, role)
+ return &types.User{
+ ID: record.ID,
+ Username: record.Username,
+ Role: normalizeRole(record.Role),
+ IsActive: isActive,
+ FailedLogins: parseFailedLogins(record.FailedLogins),
+ LastLoginAt: lastLoginAt,
+ CreatedAt: createdAt,
+ UpdatedAt: updatedAt,
+ }, nil
+}
+
+func CreateUser(username, password, role string) (*types.User, error) {
+ record := pbUserRecord{}
+ err := database.PB.DoWithSuperuser(context.Background(), http.MethodPost, "/api/collections/"+url.PathEscape(database.PB.Collection())+"/records", nil, map[string]any{
+ "username": username,
+ "email": database.PB.SyntheticEmail(username),
+ "password": password,
+ "passwordConfirm": password,
+ "role": normalizeRole(role),
+ "is_active": true,
+ "failed_logins": 0,
+ "last_login_at": "",
+ "emailVisibility": false,
+ "verified": true,
+ }, &record)
if err != nil {
return nil, err
}
- return GetUserByUsername(username) // Return the full user object
+ return normalizeUser(record)
}
-// baseSelectQuery is the common set of columns for user queries
-const baseSelectQuery = "SELECT id, username, password_hash, role, is_active, failed_logins, last_login_at, created_at, updated_at FROM users"
+func AuthenticateWithPassword(username, password string) (*types.User, error) {
+ resp := struct {
+ Token string `json:"token"`
+ Record pbUserRecord `json:"record"`
+ }{}
-// scanUser scans a row into a User struct
-func scanUser(row *sql.Row) (*types.User, error) {
- user := &types.User{}
- err := row.Scan(&user.ID, &user.Username, &user.PasswordHash, &user.Role, &user.IsActive, &user.FailedLogins, &user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt)
+ err := database.PB.Do(context.Background(), http.MethodPost, "/api/collections/"+url.PathEscape(database.PB.Collection())+"/auth-with-password", "", map[string]any{
+ "identity": username,
+ "password": password,
+ "identityField": database.PB.IdentityField(),
+ }, &resp)
if err != nil {
- if err == sql.ErrNoRows {
- return nil, nil // User not found
- }
return nil, err
}
- return user, nil
+
+ return normalizeUser(resp.Record)
}
-// GetUserByUsername retrieves a user by their username
func GetUserByUsername(username string) (*types.User, error) {
- query := database.Rebind(baseSelectQuery + " WHERE username = ?")
- row := database.DB.QueryRow(query, username)
- return scanUser(row)
+ query := url.Values{}
+ query.Set("filter", fmt.Sprintf("%s=%s", database.PB.IdentityField(), database.QuoteFilterValue(username)))
+ query.Set("perPage", "1")
+
+ resp := apiListResponse[pbUserRecord]{}
+ err := database.PB.DoWithSuperuser(context.Background(), http.MethodGet, "/api/collections/"+url.PathEscape(database.PB.Collection())+"/records", query, nil, &resp)
+ if err != nil {
+ return nil, err
+ }
+ if len(resp.Items) == 0 {
+ return nil, nil
+ }
+
+ return normalizeUser(resp.Items[0])
}
-// GetUserByID retrieves a user by their ID
-func GetUserByID(id int64) (*types.User, error) {
- query := database.Rebind(baseSelectQuery + " WHERE id = ?")
- row := database.DB.QueryRow(query, id)
- return scanUser(row)
+func GetUserByID(id string) (*types.User, error) {
+ record := pbUserRecord{}
+ err := database.PB.DoWithSuperuser(context.Background(), http.MethodGet, "/api/collections/"+url.PathEscape(database.PB.Collection())+"/records/"+url.PathEscape(id), nil, nil, &record)
+ if err != nil {
+ apiErr, ok := err.(*database.APIError)
+ if ok && apiErr.StatusCode == http.StatusNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return normalizeUser(record)
}
-// GetAllUsers retrieves all users from the database
func GetAllUsers() ([]types.User, error) {
- rows, err := database.DB.Query(baseSelectQuery)
+ query := url.Values{}
+ query.Set("perPage", "500")
+ query.Set("sort", "username")
+
+ resp := apiListResponse[pbUserRecord]{}
+ err := database.PB.DoWithSuperuser(context.Background(), http.MethodGet, "/api/collections/"+url.PathEscape(database.PB.Collection())+"/records", query, nil, &resp)
if err != nil {
return nil, err
}
- defer rows.Close() //nolint:errcheck
- var users []types.User
- for rows.Next() {
- var user types.User
- if err := rows.Scan(&user.ID, &user.Username, &user.PasswordHash, &user.Role, &user.IsActive, &user.FailedLogins, &user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt); err != nil {
- return nil, err
+ users := make([]types.User, 0, len(resp.Items))
+ for _, item := range resp.Items {
+ user, convErr := normalizeUser(item)
+ if convErr != nil {
+ return nil, convErr
}
- users = append(users, user)
+ users = append(users, *user)
}
+
return users, nil
}
-// UpdateUserRole updates a user's role
-func UpdateUserRole(id int64, role string) error {
- query := database.Rebind("UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
- _, err := database.DB.Exec(query, role, id)
- return err
+func UpdateUserRole(id, role string) error {
+ return patchUser(id, map[string]any{
+ "role": normalizeRole(role),
+ })
+}
+
+func DeleteUser(id string) error {
+ return database.PB.DoWithSuperuser(context.Background(), http.MethodDelete, "/api/collections/"+url.PathEscape(database.PB.Collection())+"/records/"+url.PathEscape(id), nil, nil, nil)
}
-// DeleteUser deletes a user from the database
-func DeleteUser(id int64) error {
- query := database.Rebind("DELETE FROM users WHERE id = ?")
- _, err := database.DB.Exec(query, id)
- return err
+func ChangeUserPassword(id, currentPassword, newPassword string) error {
+ return patchUser(id, map[string]any{
+ "oldPassword": currentPassword,
+ "password": newPassword,
+ "passwordConfirm": newPassword,
+ })
}
-// UpdateUserPassword updates a user's password hash
-func UpdateUserPassword(id int64, passwordHash string) error {
- query := database.Rebind("UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
- _, err := database.DB.Exec(query, passwordHash, id)
- return err
+func RecordLoginSuccess(id string) error {
+ return patchUser(id, map[string]any{
+ "failed_logins": 0,
+ "last_login_at": time.Now().UTC().Format(time.RFC3339),
+ "is_active": true,
+ })
}
-// RecordLoginSuccess resets failed login attempts and updates last login time
-func RecordLoginSuccess(id int64) error {
- query := database.Rebind("UPDATE users SET failed_logins = 0, last_login_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
- _, err := database.DB.Exec(query, id)
- return err
+func RecordLoginFailure(id string) error {
+ user, err := GetUserByID(id)
+ if err != nil {
+ return err
+ }
+ if user == nil {
+ return nil
+ }
+
+ return patchUser(id, map[string]any{
+ "failed_logins": user.FailedLogins + 1,
+ })
}
-// RecordLoginFailure increments the failed login counter
-func RecordLoginFailure(id int64) error {
- query := database.Rebind("UPDATE users SET failed_logins = failed_logins + 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
- _, err := database.DB.Exec(query, id)
- return err
+func SetUserActiveStatus(id string, isActive bool) error {
+ return patchUser(id, map[string]any{
+ "is_active": isActive,
+ })
}
-// SetUserActiveStatus updates the is_active flag for a user
-func SetUserActiveStatus(id int64, isActive bool) error {
- query := database.Rebind("UPDATE users SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
- _, err := database.DB.Exec(query, isActive, id)
- return err
+func patchUser(id string, body map[string]any) error {
+ return database.PB.DoWithSuperuser(context.Background(), http.MethodPatch, "/api/collections/"+url.PathEscape(database.PB.Collection())+"/records/"+url.PathEscape(id), nil, body, nil)
+}
+
+type apiListResponse[T any] struct {
+ Items []T `json:"items"`
+}
+
+func IsUnauthorized(err error) bool {
+ return database.IsUnauthorized(err)
+}
+
+func IsValidationConflict(err error) bool {
+ apiErr, ok := err.(*database.APIError)
+ if !ok {
+ return false
+ }
+ return apiErr.StatusCode == http.StatusBadRequest || apiErr.StatusCode == http.StatusConflict
+}
+
+func ValidationMessage(err error) string {
+ apiErr, ok := err.(*database.APIError)
+ if !ok || len(apiErr.Details) == 0 {
+ return ""
+ }
+
+ for _, key := range []string{"username", "password", "email", "role"} {
+ raw, exists := apiErr.Details[key]
+ if !exists {
+ continue
+ }
+ if detail, ok := raw.(map[string]any); ok {
+ if msg, ok := detail["message"].(string); ok && strings.TrimSpace(msg) != "" {
+ return msg
+ }
+ }
+ }
+
+ return ""
}