summaryrefslogtreecommitdiff
path: root/models
diff options
context:
space:
mode:
Diffstat (limited to 'models')
-rw-r--r--models/token.go145
-rw-r--r--models/user.go306
2 files changed, 261 insertions, 190 deletions
diff --git a/models/token.go b/models/token.go
index de2880a..d0ded38 100644
--- a/models/token.go
+++ b/models/token.go
@@ -1,154 +1,75 @@
package models
import (
- "auth-proxy/database"
"auth-proxy/types"
- "crypto/rand"
- "database/sql"
- "encoding/hex"
+ "auth-proxy/utils"
"time"
)
-// GenerateSecureToken creates a random, secure token.
-func GenerateSecureToken(length int) (string, error) {
- bytes := make([]byte, length)
- if _, err := rand.Read(bytes); err != nil {
- return "", err
- }
- return hex.EncodeToString(bytes), nil
-}
-
-// CreateAuthToken creates a new token for a user, deleting any existing ones.
-func CreateAuthToken(userID int64, duration time.Duration) (*types.AuthToken, error) {
- tokenString, err := GenerateSecureToken(32)
+func CreateAuthToken(user *types.User, duration time.Duration) (*types.AuthToken, error) {
+ tokenString, err := utils.GenerateJWT(user, duration, "session")
if err != nil {
return nil, err
}
- expiresAt := time.Now().Add(duration)
-
- tx, err := database.DB.Begin()
- if err != nil {
- return nil, err
- }
- defer tx.Rollback() //nolint:errcheck
-
- // Delete existing tokens for the user
- deleteQuery := database.Rebind("DELETE FROM auth_tokens WHERE user_id = ?")
- if _, err := tx.Exec(deleteQuery, userID); err != nil {
- return nil, err
- }
-
- // Insert the new token
- insertQuery := database.Rebind("INSERT INTO auth_tokens (user_id, token, expires_at) VALUES (?, ?, ?)")
- if _, err := tx.Exec(insertQuery, userID, tokenString, expiresAt); err != nil {
- return nil, err
- }
-
- if err := tx.Commit(); err != nil {
- return nil, err
- }
-
return &types.AuthToken{
- UserID: userID,
+ UserID: user.ID,
Token: tokenString,
- ExpiresAt: expiresAt,
+ ExpiresAt: time.Now().Add(duration),
}, nil
}
-// GetUserByToken retrieves a user associated with a given token.
-// It returns the user if the token is valid and not expired.
func GetUserByToken(tokenString string) (*types.User, error) {
- query := database.Rebind("SELECT u.id, u.username, u.password_hash, u.role, u.is_active, u.failed_logins, u.last_login_at, u.created_at, u.updated_at FROM users u JOIN auth_tokens t ON u.id = t.user_id WHERE t.token = ? AND t.expires_at > ?")
-
- row := database.DB.QueryRow(query, tokenString, time.Now())
-
- user := &types.User{}
- err := row.Scan(&user.ID, &user.Username, &user.PasswordHash, &user.Role, &user.IsActive, &user.FailedLogins, &user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt)
+ payload, err := utils.ValidateJWT(tokenString)
if err != nil {
- if err == sql.ErrNoRows {
- return nil, nil // Token not found or expired, not a server error
- }
- return nil, err
+ return nil, nil
+ }
+ if payload.TokenType != "session" {
+ return nil, nil
}
- return user, nil
+ return GetUserByID(payload.UserID)
}
-// DeleteAuthToken deletes a token from the database.
-func DeleteAuthToken(tokenString string) error {
- query := database.Rebind("DELETE FROM auth_tokens WHERE token = ?")
- _, err := database.DB.Exec(query, tokenString)
- return err
+func DeleteAuthToken(string) error {
+ return nil
}
-// DeleteExpiredTokens cleans up expired tokens from the database.
func DeleteExpiredTokens() (int64, error) {
- query := database.Rebind("DELETE FROM auth_tokens WHERE expires_at < ?")
- res, err := database.DB.Exec(query, time.Now())
- if err != nil {
- return 0, err
- }
- return res.RowsAffected()
+ return 0, nil
}
-// CreateRefreshToken creates a new refresh token for a user.
-func CreateRefreshToken(userID int64, duration time.Duration) (*types.RefreshToken, error) {
- tokenString, err := GenerateSecureToken(32)
- if err != nil {
- return nil, err
- }
-
- expiresAt := time.Now().Add(duration)
-
- tx, err := database.DB.Begin()
+func CreateRefreshToken(user *types.User, duration time.Duration) (*types.RefreshToken, error) {
+ tokenString, err := utils.GenerateJWT(user, duration, "refresh")
if err != nil {
return nil, err
}
- defer tx.Rollback() //nolint:errcheck
-
- // Delete existing, non-revoked refresh tokens for the user
- deleteQuery := database.Rebind("DELETE FROM refresh_tokens WHERE user_id = ?")
- if _, err := tx.Exec(deleteQuery, userID); err != nil {
- return nil, err
- }
-
- // Insert the new refresh token
- insertQuery := database.Rebind("INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES (?, ?, ?)")
- if _, err := tx.Exec(insertQuery, userID, tokenString, expiresAt); err != nil {
- return nil, err
- }
-
- if err := tx.Commit(); err != nil {
- return nil, err
- }
return &types.RefreshToken{
- UserID: userID,
+ UserID: user.ID,
Token: tokenString,
- ExpiresAt: expiresAt,
+ ExpiresAt: time.Now().Add(duration),
+ IsRevoked: false,
}, nil
}
-// GetRefreshTokenByToken retrieves a refresh token by its token string.
func GetRefreshTokenByToken(tokenString string) (*types.RefreshToken, error) {
- query := database.Rebind("SELECT id, user_id, token, expires_at, is_revoked FROM refresh_tokens WHERE token = ?")
- row := database.DB.QueryRow(query, tokenString)
-
- token := &types.RefreshToken{}
- err := row.Scan(&token.ID, &token.UserID, &token.Token, &token.ExpiresAt, &token.IsRevoked)
+ payload, err := utils.ValidateJWT(tokenString)
if err != nil {
- if err == sql.ErrNoRows {
- return nil, nil // Not found
- }
- return nil, err
+ return nil, nil
}
- return token, nil
+ if payload.TokenType != "refresh" {
+ return nil, nil
+ }
+
+ return &types.RefreshToken{
+ UserID: payload.UserID,
+ Token: tokenString,
+ ExpiresAt: time.Unix(payload.Exp, 0),
+ IsRevoked: false,
+ }, nil
}
-// DeleteRefreshTokenByToken deletes a refresh token from the database by its token string.
-func DeleteRefreshTokenByToken(tokenString string) error {
- query := database.Rebind("DELETE FROM refresh_tokens WHERE token = ?")
- _, err := database.DB.Exec(query, tokenString)
- return err
+func DeleteRefreshTokenByToken(string) error {
+ return nil
}
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 ""
}