feat: migrate Flask API to Go with JWT auth

This commit is contained in:
Cipher Vance
2025-11-20 19:00:53 -06:00
parent c6e330c063
commit 3bf3a9b24d
34 changed files with 1774 additions and 689 deletions

105
internal/user/model.go Normal file
View File

@@ -0,0 +1,105 @@
package user
import (
"errors"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;not null" json:"username"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
Password string `gorm:"not null" json:"-"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Profile *Profile `gorm:"foreignKey:UserID;constraint:OnDelete:Cascade" json:"profile,omitempty"`
PasswordResets []PasswordReset `gorm:"foreignKey:UserID;constraint:OnDelete:Cascade" json:"password_resets,omitempty"`
Sessions []Session `gorm:"foreignKey:UserID;constraint:OnDelete:Cascade" json:"sessions,omitempty"`
}
type Profile struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;uniqueIndex" json:"user_id"`
FirstName string `gorm:"default:''" json:"first_name"`
LastName string `gorm:"default:''" json:"last_name"`
Bio string `gorm:"default:''" json:"bio"`
ProfilePicture string `gorm:"default:''" json:"profile_picture"`
RestingHR int `gorm:"default:0" json:"resting_hr"`
MaxHR int `gorm:"default:0" json:"max_hr"`
FTP int `gorm:"default:0" json:"ftp"`
Weight float64 `gorm:"default:0" json:"weight"`
TotalRides int `gorm:"default:0" json:"total_rides"`
TotalDistance float64 `gorm:"default:0" json:"total_distance"`
TotalTime int `gorm:"default:0" json:"total_time"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type PasswordReset struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null" json:"user_id"`
Token string `gorm:"uniqueIndex;not null" json:"token"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
UsedAt *time.Time `json:"used_at"`
CreatedAt time.Time `json:"created_at"`
}
type Session struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"`
Token string `gorm:"uniqueIndex;not null" json:"token"`
ExpiresAt time.Time `gorm:"not null;index" json:"expires_at"`
DeviceName string `gorm:"default:''" json:"device_name"`
UserAgent string `gorm:"default:''" json:"user_agent"`
IPAddress string `gorm:"default:''" json:"ip_address"`
CreatedAt time.Time `json:"created_at"`
}
// ===== Methods =====
// SetPassword hashes and sets the password
func (u *User) SetPassword(rawPassword string) error {
if len(rawPassword) < 8 {
return errors.New("password must be at least 8 characters long")
}
hashedPassword, err := bcrypt.GenerateFromPassword(
[]byte(rawPassword),
bcrypt.DefaultCost,
)
if err != nil {
return err
}
u.Password = string(hashedPassword)
return nil
}
// CheckPassword verifies the password
func (u *User) CheckPassword(password string) bool {
return bcrypt.CompareHashAndPassword(
[]byte(u.Password),
[]byte(password),
) == nil
}
// AfterCreate hook: automatically create profile after user insert
func (u *User) AfterCreate(tx *gorm.DB) error {
profile := &Profile{
UserID: u.ID,
}
return tx.Create(profile).Error
}
// IsPasswordResetTokenValid checks if token exists and is not expired
func (prt *PasswordReset) IsValid() bool {
return prt.UsedAt == nil && time.Now().Before(prt.ExpiresAt)
}
// IsSessionValid checks if session is not expired
func (s *Session) IsValid() bool {
return time.Now().Before(s.ExpiresAt)
}