Initial commit: MyCrib API in Go

Complete rewrite of Django REST API to Go with:
- Gin web framework for HTTP routing
- GORM for database operations
- GoAdmin for admin panel
- Gorush integration for push notifications
- Redis for caching and job queues

Features implemented:
- User authentication (login, register, logout, password reset)
- Residence management (CRUD, sharing, share codes)
- Task management (CRUD, kanban board, completions)
- Contractor management (CRUD, specialties)
- Document management (CRUD, warranties)
- Notifications (preferences, push notifications)
- Subscription management (tiers, limits)

Infrastructure:
- Docker Compose for local development
- Database migrations and seed data
- Admin panel for data management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-26 20:07:16 -06:00
commit 1f12f3f62a
78 changed files with 13821 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
package repositories
import (
"gorm.io/gorm"
"github.com/treytartt/mycrib-api/internal/models"
)
// ContractorRepository handles database operations for contractors
type ContractorRepository struct {
db *gorm.DB
}
// NewContractorRepository creates a new contractor repository
func NewContractorRepository(db *gorm.DB) *ContractorRepository {
return &ContractorRepository{db: db}
}
// FindByID finds a contractor by ID with preloaded relations
func (r *ContractorRepository) FindByID(id uint) (*models.Contractor, error) {
var contractor models.Contractor
err := r.db.Preload("CreatedBy").
Preload("Specialties").
Preload("Tasks").
Where("id = ? AND is_active = ?", id, true).
First(&contractor).Error
if err != nil {
return nil, err
}
return &contractor, nil
}
// FindByResidence finds all contractors for a residence
func (r *ContractorRepository) FindByResidence(residenceID uint) ([]models.Contractor, error) {
var contractors []models.Contractor
err := r.db.Preload("CreatedBy").
Preload("Specialties").
Where("residence_id = ? AND is_active = ?", residenceID, true).
Order("is_favorite DESC, name ASC").
Find(&contractors).Error
return contractors, err
}
// FindByUser finds all contractors accessible to a user
func (r *ContractorRepository) FindByUser(residenceIDs []uint) ([]models.Contractor, error) {
var contractors []models.Contractor
err := r.db.Preload("CreatedBy").
Preload("Specialties").
Preload("Residence").
Where("residence_id IN ? AND is_active = ?", residenceIDs, true).
Order("is_favorite DESC, name ASC").
Find(&contractors).Error
return contractors, err
}
// Create creates a new contractor
func (r *ContractorRepository) Create(contractor *models.Contractor) error {
return r.db.Create(contractor).Error
}
// Update updates a contractor
func (r *ContractorRepository) Update(contractor *models.Contractor) error {
return r.db.Save(contractor).Error
}
// Delete soft-deletes a contractor
func (r *ContractorRepository) Delete(id uint) error {
return r.db.Model(&models.Contractor{}).
Where("id = ?", id).
Update("is_active", false).Error
}
// ToggleFavorite toggles the favorite status of a contractor
func (r *ContractorRepository) ToggleFavorite(id uint) (bool, error) {
var contractor models.Contractor
if err := r.db.First(&contractor, id).Error; err != nil {
return false, err
}
newStatus := !contractor.IsFavorite
err := r.db.Model(&models.Contractor{}).
Where("id = ?", id).
Update("is_favorite", newStatus).Error
return newStatus, err
}
// GetTasksForContractor gets all tasks associated with a contractor
func (r *ContractorRepository) GetTasksForContractor(contractorID uint) ([]models.Task, error) {
var tasks []models.Task
err := r.db.Preload("Category").
Preload("Priority").
Preload("Status").
Where("contractor_id = ?", contractorID).
Order("due_date ASC NULLS LAST").
Find(&tasks).Error
return tasks, err
}
// SetSpecialties sets the specialties for a contractor
func (r *ContractorRepository) SetSpecialties(contractorID uint, specialtyIDs []uint) error {
var contractor models.Contractor
if err := r.db.First(&contractor, contractorID).Error; err != nil {
return err
}
// Clear existing specialties
if err := r.db.Model(&contractor).Association("Specialties").Clear(); err != nil {
return err
}
if len(specialtyIDs) == 0 {
return nil
}
// Add new specialties
var specialties []models.ContractorSpecialty
if err := r.db.Where("id IN ?", specialtyIDs).Find(&specialties).Error; err != nil {
return err
}
return r.db.Model(&contractor).Association("Specialties").Append(specialties)
}
// CountByResidence counts contractors in a residence
func (r *ContractorRepository) CountByResidence(residenceID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Contractor{}).
Where("residence_id = ? AND is_active = ?", residenceID, true).
Count(&count).Error
return count, err
}
// === Specialty Operations ===
// GetAllSpecialties returns all contractor specialties
func (r *ContractorRepository) GetAllSpecialties() ([]models.ContractorSpecialty, error) {
var specialties []models.ContractorSpecialty
err := r.db.Order("display_order, name").Find(&specialties).Error
return specialties, err
}
// FindSpecialtyByID finds a specialty by ID
func (r *ContractorRepository) FindSpecialtyByID(id uint) (*models.ContractorSpecialty, error) {
var specialty models.ContractorSpecialty
err := r.db.First(&specialty, id).Error
if err != nil {
return nil, err
}
return &specialty, nil
}

View File

@@ -0,0 +1,125 @@
package repositories
import (
"time"
"gorm.io/gorm"
"github.com/treytartt/mycrib-api/internal/models"
)
// DocumentRepository handles database operations for documents
type DocumentRepository struct {
db *gorm.DB
}
// NewDocumentRepository creates a new document repository
func NewDocumentRepository(db *gorm.DB) *DocumentRepository {
return &DocumentRepository{db: db}
}
// FindByID finds a document by ID with preloaded relations
func (r *DocumentRepository) FindByID(id uint) (*models.Document, error) {
var document models.Document
err := r.db.Preload("CreatedBy").
Preload("Task").
Where("id = ? AND is_active = ?", id, true).
First(&document).Error
if err != nil {
return nil, err
}
return &document, nil
}
// FindByResidence finds all documents for a residence
func (r *DocumentRepository) FindByResidence(residenceID uint) ([]models.Document, error) {
var documents []models.Document
err := r.db.Preload("CreatedBy").
Where("residence_id = ? AND is_active = ?", residenceID, true).
Order("created_at DESC").
Find(&documents).Error
return documents, err
}
// FindByUser finds all documents accessible to a user
func (r *DocumentRepository) FindByUser(residenceIDs []uint) ([]models.Document, error) {
var documents []models.Document
err := r.db.Preload("CreatedBy").
Preload("Residence").
Where("residence_id IN ? AND is_active = ?", residenceIDs, true).
Order("created_at DESC").
Find(&documents).Error
return documents, err
}
// FindWarranties finds all warranty documents
func (r *DocumentRepository) FindWarranties(residenceIDs []uint) ([]models.Document, error) {
var documents []models.Document
err := r.db.Preload("CreatedBy").
Preload("Residence").
Where("residence_id IN ? AND is_active = ? AND document_type = ?",
residenceIDs, true, models.DocumentTypeWarranty).
Order("expiry_date ASC NULLS LAST").
Find(&documents).Error
return documents, err
}
// FindExpiringWarranties finds warranties expiring within the specified days
func (r *DocumentRepository) FindExpiringWarranties(residenceIDs []uint, days int) ([]models.Document, error) {
threshold := time.Now().UTC().AddDate(0, 0, days)
now := time.Now().UTC()
var documents []models.Document
err := r.db.Preload("CreatedBy").
Preload("Residence").
Where("residence_id IN ? AND is_active = ? AND document_type = ? AND expiry_date > ? AND expiry_date <= ?",
residenceIDs, true, models.DocumentTypeWarranty, now, threshold).
Order("expiry_date ASC").
Find(&documents).Error
return documents, err
}
// Create creates a new document
func (r *DocumentRepository) Create(document *models.Document) error {
return r.db.Create(document).Error
}
// Update updates a document
func (r *DocumentRepository) Update(document *models.Document) error {
return r.db.Save(document).Error
}
// Delete soft-deletes a document
func (r *DocumentRepository) Delete(id uint) error {
return r.db.Model(&models.Document{}).
Where("id = ?", id).
Update("is_active", false).Error
}
// Activate activates a document
func (r *DocumentRepository) Activate(id uint) error {
return r.db.Model(&models.Document{}).
Where("id = ?", id).
Update("is_active", true).Error
}
// Deactivate deactivates a document
func (r *DocumentRepository) Deactivate(id uint) error {
return r.db.Model(&models.Document{}).
Where("id = ?", id).
Update("is_active", false).Error
}
// CountByResidence counts documents in a residence
func (r *DocumentRepository) CountByResidence(residenceID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Document{}).
Where("residence_id = ? AND is_active = ?", residenceID, true).
Count(&count).Error
return count, err
}
// FindByIDIncludingInactive finds a document by ID including inactive ones
func (r *DocumentRepository) FindByIDIncludingInactive(id uint, document *models.Document) error {
return r.db.Preload("CreatedBy").First(document, id).Error
}

View File

@@ -0,0 +1,265 @@
package repositories
import (
"time"
"gorm.io/gorm"
"github.com/treytartt/mycrib-api/internal/models"
)
// NotificationRepository handles database operations for notifications
type NotificationRepository struct {
db *gorm.DB
}
// NewNotificationRepository creates a new notification repository
func NewNotificationRepository(db *gorm.DB) *NotificationRepository {
return &NotificationRepository{db: db}
}
// === Notifications ===
// FindByID finds a notification by ID
func (r *NotificationRepository) FindByID(id uint) (*models.Notification, error) {
var notification models.Notification
err := r.db.First(&notification, id).Error
if err != nil {
return nil, err
}
return &notification, nil
}
// FindByUser finds all notifications for a user
func (r *NotificationRepository) FindByUser(userID uint, limit, offset int) ([]models.Notification, error) {
var notifications []models.Notification
query := r.db.Where("user_id = ?", userID).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
err := query.Find(&notifications).Error
return notifications, err
}
// Create creates a new notification
func (r *NotificationRepository) Create(notification *models.Notification) error {
return r.db.Create(notification).Error
}
// MarkAsRead marks a notification as read
func (r *NotificationRepository) MarkAsRead(id uint) error {
now := time.Now().UTC()
return r.db.Model(&models.Notification{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"read": true,
"read_at": now,
}).Error
}
// MarkAllAsRead marks all notifications for a user as read
func (r *NotificationRepository) MarkAllAsRead(userID uint) error {
now := time.Now().UTC()
return r.db.Model(&models.Notification{}).
Where("user_id = ? AND read = ?", userID, false).
Updates(map[string]interface{}{
"read": true,
"read_at": now,
}).Error
}
// MarkAsSent marks a notification as sent
func (r *NotificationRepository) MarkAsSent(id uint) error {
now := time.Now().UTC()
return r.db.Model(&models.Notification{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"sent": true,
"sent_at": now,
}).Error
}
// SetError sets an error message on a notification
func (r *NotificationRepository) SetError(id uint, errorMsg string) error {
return r.db.Model(&models.Notification{}).
Where("id = ?", id).
Update("error_message", errorMsg).Error
}
// CountUnread counts unread notifications for a user
func (r *NotificationRepository) CountUnread(userID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Notification{}).
Where("user_id = ? AND read = ?", userID, false).
Count(&count).Error
return count, err
}
// GetPendingNotifications gets notifications that need to be sent
func (r *NotificationRepository) GetPendingNotifications(limit int) ([]models.Notification, error) {
var notifications []models.Notification
err := r.db.Where("sent = ?", false).
Order("created_at ASC").
Limit(limit).
Find(&notifications).Error
return notifications, err
}
// === Notification Preferences ===
// FindPreferencesByUser finds notification preferences for a user
func (r *NotificationRepository) FindPreferencesByUser(userID uint) (*models.NotificationPreference, error) {
var prefs models.NotificationPreference
err := r.db.Where("user_id = ?", userID).First(&prefs).Error
if err != nil {
return nil, err
}
return &prefs, nil
}
// CreatePreferences creates notification preferences for a user
func (r *NotificationRepository) CreatePreferences(prefs *models.NotificationPreference) error {
return r.db.Create(prefs).Error
}
// UpdatePreferences updates notification preferences
func (r *NotificationRepository) UpdatePreferences(prefs *models.NotificationPreference) error {
return r.db.Save(prefs).Error
}
// GetOrCreatePreferences gets or creates notification preferences for a user
func (r *NotificationRepository) GetOrCreatePreferences(userID uint) (*models.NotificationPreference, error) {
prefs, err := r.FindPreferencesByUser(userID)
if err == nil {
return prefs, nil
}
if err == gorm.ErrRecordNotFound {
prefs = &models.NotificationPreference{
UserID: userID,
TaskDueSoon: true,
TaskOverdue: true,
TaskCompleted: true,
TaskAssigned: true,
ResidenceShared: true,
WarrantyExpiring: true,
}
if err := r.CreatePreferences(prefs); err != nil {
return nil, err
}
return prefs, nil
}
return nil, err
}
// === Device Registration ===
// FindAPNSDeviceByToken finds an APNS device by registration token
func (r *NotificationRepository) FindAPNSDeviceByToken(token string) (*models.APNSDevice, error) {
var device models.APNSDevice
err := r.db.Where("registration_id = ?", token).First(&device).Error
if err != nil {
return nil, err
}
return &device, nil
}
// FindAPNSDevicesByUser finds all APNS devices for a user
func (r *NotificationRepository) FindAPNSDevicesByUser(userID uint) ([]models.APNSDevice, error) {
var devices []models.APNSDevice
err := r.db.Where("user_id = ? AND active = ?", userID, true).Find(&devices).Error
return devices, err
}
// CreateAPNSDevice creates a new APNS device
func (r *NotificationRepository) CreateAPNSDevice(device *models.APNSDevice) error {
return r.db.Create(device).Error
}
// UpdateAPNSDevice updates an APNS device
func (r *NotificationRepository) UpdateAPNSDevice(device *models.APNSDevice) error {
return r.db.Save(device).Error
}
// DeleteAPNSDevice deletes an APNS device
func (r *NotificationRepository) DeleteAPNSDevice(id uint) error {
return r.db.Delete(&models.APNSDevice{}, id).Error
}
// DeactivateAPNSDevice deactivates an APNS device
func (r *NotificationRepository) DeactivateAPNSDevice(id uint) error {
return r.db.Model(&models.APNSDevice{}).
Where("id = ?", id).
Update("active", false).Error
}
// FindGCMDeviceByToken finds a GCM device by registration token
func (r *NotificationRepository) FindGCMDeviceByToken(token string) (*models.GCMDevice, error) {
var device models.GCMDevice
err := r.db.Where("registration_id = ?", token).First(&device).Error
if err != nil {
return nil, err
}
return &device, nil
}
// FindGCMDevicesByUser finds all GCM devices for a user
func (r *NotificationRepository) FindGCMDevicesByUser(userID uint) ([]models.GCMDevice, error) {
var devices []models.GCMDevice
err := r.db.Where("user_id = ? AND active = ?", userID, true).Find(&devices).Error
return devices, err
}
// CreateGCMDevice creates a new GCM device
func (r *NotificationRepository) CreateGCMDevice(device *models.GCMDevice) error {
return r.db.Create(device).Error
}
// UpdateGCMDevice updates a GCM device
func (r *NotificationRepository) UpdateGCMDevice(device *models.GCMDevice) error {
return r.db.Save(device).Error
}
// DeleteGCMDevice deletes a GCM device
func (r *NotificationRepository) DeleteGCMDevice(id uint) error {
return r.db.Delete(&models.GCMDevice{}, id).Error
}
// DeactivateGCMDevice deactivates a GCM device
func (r *NotificationRepository) DeactivateGCMDevice(id uint) error {
return r.db.Model(&models.GCMDevice{}).
Where("id = ?", id).
Update("active", false).Error
}
// GetActiveTokensForUser gets all active push tokens for a user
func (r *NotificationRepository) GetActiveTokensForUser(userID uint) (iosTokens []string, androidTokens []string, err error) {
apnsDevices, err := r.FindAPNSDevicesByUser(userID)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, nil, err
}
gcmDevices, err := r.FindGCMDevicesByUser(userID)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, nil, err
}
iosTokens = make([]string, 0, len(apnsDevices))
for _, d := range apnsDevices {
iosTokens = append(iosTokens, d.RegistrationID)
}
androidTokens = make([]string, 0, len(gcmDevices))
for _, d := range gcmDevices {
androidTokens = append(androidTokens, d.RegistrationID)
}
return iosTokens, androidTokens, nil
}

View File

@@ -0,0 +1,310 @@
package repositories
import (
"crypto/rand"
"errors"
"math/big"
"time"
"gorm.io/gorm"
"github.com/treytartt/mycrib-api/internal/models"
)
// ResidenceRepository handles database operations for residences
type ResidenceRepository struct {
db *gorm.DB
}
// NewResidenceRepository creates a new residence repository
func NewResidenceRepository(db *gorm.DB) *ResidenceRepository {
return &ResidenceRepository{db: db}
}
// FindByID finds a residence by ID with preloaded relations
func (r *ResidenceRepository) FindByID(id uint) (*models.Residence, error) {
var residence models.Residence
err := r.db.Preload("Owner").
Preload("Users").
Preload("PropertyType").
Where("id = ? AND is_active = ?", id, true).
First(&residence).Error
if err != nil {
return nil, err
}
return &residence, nil
}
// FindByIDSimple finds a residence by ID without preloading (for quick checks)
func (r *ResidenceRepository) FindByIDSimple(id uint) (*models.Residence, error) {
var residence models.Residence
err := r.db.Where("id = ? AND is_active = ?", id, true).First(&residence).Error
if err != nil {
return nil, err
}
return &residence, nil
}
// FindByUser finds all residences accessible to a user (owned or shared)
func (r *ResidenceRepository) FindByUser(userID uint) ([]models.Residence, error) {
var residences []models.Residence
// Find residences where user is owner OR user is in the shared users list
err := r.db.Preload("Owner").
Preload("Users").
Preload("PropertyType").
Where("is_active = ?", true).
Where("owner_id = ? OR id IN (?)",
userID,
r.db.Table("residence_residence_users").Select("residence_id").Where("user_id = ?", userID),
).
Order("is_primary DESC, created_at DESC").
Find(&residences).Error
if err != nil {
return nil, err
}
return residences, nil
}
// FindOwnedByUser finds all residences owned by a user
func (r *ResidenceRepository) FindOwnedByUser(userID uint) ([]models.Residence, error) {
var residences []models.Residence
err := r.db.Preload("Owner").
Preload("Users").
Preload("PropertyType").
Where("owner_id = ? AND is_active = ?", userID, true).
Order("is_primary DESC, created_at DESC").
Find(&residences).Error
if err != nil {
return nil, err
}
return residences, nil
}
// Create creates a new residence
func (r *ResidenceRepository) Create(residence *models.Residence) error {
return r.db.Create(residence).Error
}
// Update updates a residence
func (r *ResidenceRepository) Update(residence *models.Residence) error {
return r.db.Save(residence).Error
}
// Delete soft-deletes a residence by setting is_active to false
func (r *ResidenceRepository) Delete(id uint) error {
return r.db.Model(&models.Residence{}).
Where("id = ?", id).
Update("is_active", false).Error
}
// AddUser adds a user to a residence's shared users
func (r *ResidenceRepository) AddUser(residenceID, userID uint) error {
// Using raw SQL for the many-to-many join table
return r.db.Exec(
"INSERT INTO residence_residence_users (residence_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING",
residenceID, userID,
).Error
}
// RemoveUser removes a user from a residence's shared users
func (r *ResidenceRepository) RemoveUser(residenceID, userID uint) error {
return r.db.Exec(
"DELETE FROM residence_residence_users WHERE residence_id = ? AND user_id = ?",
residenceID, userID,
).Error
}
// GetResidenceUsers returns all users with access to a residence
func (r *ResidenceRepository) GetResidenceUsers(residenceID uint) ([]models.User, error) {
residence, err := r.FindByID(residenceID)
if err != nil {
return nil, err
}
users := make([]models.User, 0, len(residence.Users)+1)
users = append(users, residence.Owner)
users = append(users, residence.Users...)
return users, nil
}
// HasAccess checks if a user has access to a residence
func (r *ResidenceRepository) HasAccess(residenceID, userID uint) (bool, error) {
var count int64
// Check if user is owner
err := r.db.Model(&models.Residence{}).
Where("id = ? AND owner_id = ? AND is_active = ?", residenceID, userID, true).
Count(&count).Error
if err != nil {
return false, err
}
if count > 0 {
return true, nil
}
// Check if user is in shared users
err = r.db.Table("residence_residence_users").
Where("residence_id = ? AND user_id = ?", residenceID, userID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// IsOwner checks if a user is the owner of a residence
func (r *ResidenceRepository) IsOwner(residenceID, userID uint) (bool, error) {
var count int64
err := r.db.Model(&models.Residence{}).
Where("id = ? AND owner_id = ? AND is_active = ?", residenceID, userID, true).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// CountByOwner counts residences owned by a user
func (r *ResidenceRepository) CountByOwner(userID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Residence{}).
Where("owner_id = ? AND is_active = ?", userID, true).
Count(&count).Error
return count, err
}
// === Share Code Operations ===
// CreateShareCode creates a new share code for a residence
func (r *ResidenceRepository) CreateShareCode(residenceID, createdByID uint, expiresIn time.Duration) (*models.ResidenceShareCode, error) {
// Deactivate existing codes for this residence
err := r.db.Model(&models.ResidenceShareCode{}).
Where("residence_id = ? AND is_active = ?", residenceID, true).
Update("is_active", false).Error
if err != nil {
return nil, err
}
// Generate unique 6-character code
code, err := r.generateUniqueCode()
if err != nil {
return nil, err
}
expiresAt := time.Now().UTC().Add(expiresIn)
shareCode := &models.ResidenceShareCode{
ResidenceID: residenceID,
Code: code,
CreatedByID: createdByID,
IsActive: true,
ExpiresAt: &expiresAt,
}
if err := r.db.Create(shareCode).Error; err != nil {
return nil, err
}
return shareCode, nil
}
// FindShareCodeByCode finds an active share code by its code string
func (r *ResidenceRepository) FindShareCodeByCode(code string) (*models.ResidenceShareCode, error) {
var shareCode models.ResidenceShareCode
err := r.db.Preload("Residence").
Where("code = ? AND is_active = ?", code, true).
First(&shareCode).Error
if err != nil {
return nil, err
}
// Check if expired
if shareCode.ExpiresAt != nil && time.Now().UTC().After(*shareCode.ExpiresAt) {
return nil, errors.New("share code has expired")
}
return &shareCode, nil
}
// DeactivateShareCode deactivates a share code
func (r *ResidenceRepository) DeactivateShareCode(codeID uint) error {
return r.db.Model(&models.ResidenceShareCode{}).
Where("id = ?", codeID).
Update("is_active", false).Error
}
// GetActiveShareCode gets the active share code for a residence (if any)
func (r *ResidenceRepository) GetActiveShareCode(residenceID uint) (*models.ResidenceShareCode, error) {
var shareCode models.ResidenceShareCode
err := r.db.Where("residence_id = ? AND is_active = ?", residenceID, true).
First(&shareCode).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
// Check if expired
if shareCode.ExpiresAt != nil && time.Now().UTC().After(*shareCode.ExpiresAt) {
// Auto-deactivate expired code
r.DeactivateShareCode(shareCode.ID)
return nil, nil
}
return &shareCode, nil
}
// generateUniqueCode generates a unique 6-character alphanumeric code
func (r *ResidenceRepository) generateUniqueCode() (string, error) {
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Removed ambiguous chars: 0, O, I, 1
const codeLength = 6
maxAttempts := 10
for attempt := 0; attempt < maxAttempts; attempt++ {
code := make([]byte, codeLength)
for i := range code {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
code[i] = charset[num.Int64()]
}
codeStr := string(code)
// Check if code already exists
var count int64
r.db.Model(&models.ResidenceShareCode{}).
Where("code = ? AND is_active = ?", codeStr, true).
Count(&count)
if count == 0 {
return codeStr, nil
}
}
return "", errors.New("failed to generate unique share code")
}
// === Residence Type Operations ===
// GetAllResidenceTypes returns all residence types
func (r *ResidenceRepository) GetAllResidenceTypes() ([]models.ResidenceType, error) {
var types []models.ResidenceType
err := r.db.Order("id").Find(&types).Error
return types, err
}
// FindResidenceTypeByID finds a residence type by ID
func (r *ResidenceRepository) FindResidenceTypeByID(id uint) (*models.ResidenceType, error) {
var residenceType models.ResidenceType
err := r.db.First(&residenceType, id).Error
if err != nil {
return nil, err
}
return &residenceType, nil
}

View File

@@ -0,0 +1,203 @@
package repositories
import (
"time"
"gorm.io/gorm"
"github.com/treytartt/mycrib-api/internal/models"
)
// SubscriptionRepository handles database operations for subscriptions
type SubscriptionRepository struct {
db *gorm.DB
}
// NewSubscriptionRepository creates a new subscription repository
func NewSubscriptionRepository(db *gorm.DB) *SubscriptionRepository {
return &SubscriptionRepository{db: db}
}
// === User Subscription ===
// FindByUserID finds a subscription by user ID
func (r *SubscriptionRepository) FindByUserID(userID uint) (*models.UserSubscription, error) {
var sub models.UserSubscription
err := r.db.Where("user_id = ?", userID).First(&sub).Error
if err != nil {
return nil, err
}
return &sub, nil
}
// GetOrCreate gets or creates a subscription for a user (defaults to free tier)
func (r *SubscriptionRepository) GetOrCreate(userID uint) (*models.UserSubscription, error) {
sub, err := r.FindByUserID(userID)
if err == nil {
return sub, nil
}
if err == gorm.ErrRecordNotFound {
sub = &models.UserSubscription{
UserID: userID,
Tier: models.TierFree,
AutoRenew: true,
}
if err := r.db.Create(sub).Error; err != nil {
return nil, err
}
return sub, nil
}
return nil, err
}
// Update updates a subscription
func (r *SubscriptionRepository) Update(sub *models.UserSubscription) error {
return r.db.Save(sub).Error
}
// UpgradeToPro upgrades a user to Pro tier
func (r *SubscriptionRepository) UpgradeToPro(userID uint, expiresAt time.Time, platform string) error {
now := time.Now().UTC()
return r.db.Model(&models.UserSubscription{}).
Where("user_id = ?", userID).
Updates(map[string]interface{}{
"tier": models.TierPro,
"subscribed_at": now,
"expires_at": expiresAt,
"cancelled_at": nil,
"platform": platform,
"auto_renew": true,
}).Error
}
// DowngradeToFree downgrades a user to Free tier
func (r *SubscriptionRepository) DowngradeToFree(userID uint) error {
now := time.Now().UTC()
return r.db.Model(&models.UserSubscription{}).
Where("user_id = ?", userID).
Updates(map[string]interface{}{
"tier": models.TierFree,
"cancelled_at": now,
"auto_renew": false,
}).Error
}
// SetAutoRenew sets the auto-renew flag
func (r *SubscriptionRepository) SetAutoRenew(userID uint, autoRenew bool) error {
return r.db.Model(&models.UserSubscription{}).
Where("user_id = ?", userID).
Update("auto_renew", autoRenew).Error
}
// UpdateReceiptData updates the Apple receipt data
func (r *SubscriptionRepository) UpdateReceiptData(userID uint, receiptData string) error {
return r.db.Model(&models.UserSubscription{}).
Where("user_id = ?", userID).
Update("apple_receipt_data", receiptData).Error
}
// UpdatePurchaseToken updates the Google purchase token
func (r *SubscriptionRepository) UpdatePurchaseToken(userID uint, token string) error {
return r.db.Model(&models.UserSubscription{}).
Where("user_id = ?", userID).
Update("google_purchase_token", token).Error
}
// === Tier Limits ===
// GetTierLimits gets the limits for a subscription tier
func (r *SubscriptionRepository) GetTierLimits(tier models.SubscriptionTier) (*models.TierLimits, error) {
var limits models.TierLimits
err := r.db.Where("tier = ?", tier).First(&limits).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// Return defaults
if tier == models.TierFree {
defaults := models.GetDefaultFreeLimits()
return &defaults, nil
}
defaults := models.GetDefaultProLimits()
return &defaults, nil
}
return nil, err
}
return &limits, nil
}
// GetAllTierLimits gets all tier limits
func (r *SubscriptionRepository) GetAllTierLimits() ([]models.TierLimits, error) {
var limits []models.TierLimits
err := r.db.Find(&limits).Error
return limits, err
}
// === Subscription Settings (Singleton) ===
// GetSettings gets the subscription settings
func (r *SubscriptionRepository) GetSettings() (*models.SubscriptionSettings, error) {
var settings models.SubscriptionSettings
err := r.db.First(&settings).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// Return default settings (limitations disabled)
return &models.SubscriptionSettings{
EnableLimitations: false,
}, nil
}
return nil, err
}
return &settings, nil
}
// === Upgrade Triggers ===
// GetUpgradeTrigger gets an upgrade trigger by key
func (r *SubscriptionRepository) GetUpgradeTrigger(key string) (*models.UpgradeTrigger, error) {
var trigger models.UpgradeTrigger
err := r.db.Where("trigger_key = ? AND is_active = ?", key, true).First(&trigger).Error
if err != nil {
return nil, err
}
return &trigger, nil
}
// GetAllUpgradeTriggers gets all active upgrade triggers
func (r *SubscriptionRepository) GetAllUpgradeTriggers() ([]models.UpgradeTrigger, error) {
var triggers []models.UpgradeTrigger
err := r.db.Where("is_active = ?", true).Find(&triggers).Error
return triggers, err
}
// === Feature Benefits ===
// GetFeatureBenefits gets all active feature benefits
func (r *SubscriptionRepository) GetFeatureBenefits() ([]models.FeatureBenefit, error) {
var benefits []models.FeatureBenefit
err := r.db.Where("is_active = ?", true).Order("display_order").Find(&benefits).Error
return benefits, err
}
// === Promotions ===
// GetActivePromotions gets all currently active promotions for a tier
func (r *SubscriptionRepository) GetActivePromotions(tier models.SubscriptionTier) ([]models.Promotion, error) {
now := time.Now().UTC()
var promotions []models.Promotion
err := r.db.Where("is_active = ? AND target_tier = ? AND start_date <= ? AND end_date >= ?",
true, tier, now, now).
Order("start_date DESC").
Find(&promotions).Error
return promotions, err
}
// GetPromotionByID gets a promotion by ID
func (r *SubscriptionRepository) GetPromotionByID(promotionID string) (*models.Promotion, error) {
var promotion models.Promotion
err := r.db.Where("promotion_id = ? AND is_active = ?", promotionID, true).First(&promotion).Error
if err != nil {
return nil, err
}
return &promotion, nil
}

View File

@@ -0,0 +1,347 @@
package repositories
import (
"time"
"gorm.io/gorm"
"github.com/treytartt/mycrib-api/internal/models"
)
// TaskRepository handles database operations for tasks
type TaskRepository struct {
db *gorm.DB
}
// NewTaskRepository creates a new task repository
func NewTaskRepository(db *gorm.DB) *TaskRepository {
return &TaskRepository{db: db}
}
// === Task CRUD ===
// FindByID finds a task by ID with preloaded relations
func (r *TaskRepository) FindByID(id uint) (*models.Task, error) {
var task models.Task
err := r.db.Preload("Residence").
Preload("CreatedBy").
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
Preload("Status").
Preload("Frequency").
Preload("Completions").
Preload("Completions.CompletedBy").
First(&task, id).Error
if err != nil {
return nil, err
}
return &task, nil
}
// FindByResidence finds all tasks for a residence
func (r *TaskRepository) FindByResidence(residenceID uint) ([]models.Task, error) {
var tasks []models.Task
err := r.db.Preload("CreatedBy").
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
Preload("Status").
Preload("Frequency").
Preload("Completions").
Where("residence_id = ?", residenceID).
Order("due_date ASC NULLS LAST, created_at DESC").
Find(&tasks).Error
return tasks, err
}
// FindByUser finds all tasks accessible to a user (across all their residences)
func (r *TaskRepository) FindByUser(userID uint, residenceIDs []uint) ([]models.Task, error) {
var tasks []models.Task
err := r.db.Preload("Residence").
Preload("CreatedBy").
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
Preload("Status").
Preload("Frequency").
Preload("Completions").
Where("residence_id IN ?", residenceIDs).
Order("due_date ASC NULLS LAST, created_at DESC").
Find(&tasks).Error
return tasks, err
}
// Create creates a new task
func (r *TaskRepository) Create(task *models.Task) error {
return r.db.Create(task).Error
}
// Update updates a task
func (r *TaskRepository) Update(task *models.Task) error {
return r.db.Save(task).Error
}
// Delete hard-deletes a task
func (r *TaskRepository) Delete(id uint) error {
return r.db.Delete(&models.Task{}, id).Error
}
// === Task State Operations ===
// MarkInProgress marks a task as in progress
func (r *TaskRepository) MarkInProgress(id uint, statusID uint) error {
return r.db.Model(&models.Task{}).
Where("id = ?", id).
Update("status_id", statusID).Error
}
// Cancel cancels a task
func (r *TaskRepository) Cancel(id uint) error {
return r.db.Model(&models.Task{}).
Where("id = ?", id).
Update("is_cancelled", true).Error
}
// Uncancel uncancels a task
func (r *TaskRepository) Uncancel(id uint) error {
return r.db.Model(&models.Task{}).
Where("id = ?", id).
Update("is_cancelled", false).Error
}
// Archive archives a task
func (r *TaskRepository) Archive(id uint) error {
return r.db.Model(&models.Task{}).
Where("id = ?", id).
Update("is_archived", true).Error
}
// Unarchive unarchives a task
func (r *TaskRepository) Unarchive(id uint) error {
return r.db.Model(&models.Task{}).
Where("id = ?", id).
Update("is_archived", false).Error
}
// === Kanban Board ===
// GetKanbanData retrieves tasks organized for kanban display
func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int) (*models.KanbanBoard, error) {
var tasks []models.Task
err := r.db.Preload("CreatedBy").
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
Preload("Status").
Preload("Frequency").
Preload("Completions").
Preload("Completions.CompletedBy").
Where("residence_id = ? AND is_archived = ?", residenceID, false).
Order("due_date ASC NULLS LAST, priority_id DESC, created_at DESC").
Find(&tasks).Error
if err != nil {
return nil, err
}
// Organize into columns
now := time.Now().UTC()
threshold := now.AddDate(0, 0, daysThreshold)
overdue := make([]models.Task, 0)
dueSoon := make([]models.Task, 0)
upcoming := make([]models.Task, 0)
inProgress := make([]models.Task, 0)
completed := make([]models.Task, 0)
cancelled := make([]models.Task, 0)
for _, task := range tasks {
if task.IsCancelled {
cancelled = append(cancelled, task)
continue
}
// Check if completed (has completions)
if len(task.Completions) > 0 {
completed = append(completed, task)
continue
}
// Check status for in-progress (status_id = 2 typically)
if task.Status != nil && task.Status.Name == "In Progress" {
inProgress = append(inProgress, task)
continue
}
// Check due date
if task.DueDate != nil {
if task.DueDate.Before(now) {
overdue = append(overdue, task)
} else if task.DueDate.Before(threshold) {
dueSoon = append(dueSoon, task)
} else {
upcoming = append(upcoming, task)
}
} else {
upcoming = append(upcoming, task)
}
}
columns := []models.KanbanColumn{
{
Name: "overdue_tasks",
DisplayName: "Overdue",
ButtonTypes: []string{"edit", "cancel", "mark_in_progress"},
Icons: map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"},
Color: "#FF3B30",
Tasks: overdue,
Count: len(overdue),
},
{
Name: "due_soon_tasks",
DisplayName: "Due Soon",
ButtonTypes: []string{"edit", "complete", "mark_in_progress"},
Icons: map[string]string{"ios": "clock", "android": "Schedule"},
Color: "#FF9500",
Tasks: dueSoon,
Count: len(dueSoon),
},
{
Name: "upcoming_tasks",
DisplayName: "Upcoming",
ButtonTypes: []string{"edit", "cancel"},
Icons: map[string]string{"ios": "calendar", "android": "Event"},
Color: "#007AFF",
Tasks: upcoming,
Count: len(upcoming),
},
{
Name: "in_progress_tasks",
DisplayName: "In Progress",
ButtonTypes: []string{"edit", "complete"},
Icons: map[string]string{"ios": "hammer", "android": "Build"},
Color: "#5856D6",
Tasks: inProgress,
Count: len(inProgress),
},
{
Name: "completed_tasks",
DisplayName: "Completed",
ButtonTypes: []string{"view"},
Icons: map[string]string{"ios": "checkmark.circle", "android": "CheckCircle"},
Color: "#34C759",
Tasks: completed,
Count: len(completed),
},
{
Name: "cancelled_tasks",
DisplayName: "Cancelled",
ButtonTypes: []string{"uncancel", "delete"},
Icons: map[string]string{"ios": "xmark.circle", "android": "Cancel"},
Color: "#8E8E93",
Tasks: cancelled,
Count: len(cancelled),
},
}
return &models.KanbanBoard{
Columns: columns,
DaysThreshold: daysThreshold,
ResidenceID: string(rune(residenceID)),
}, nil
}
// === Lookup Operations ===
// GetAllCategories returns all task categories
func (r *TaskRepository) GetAllCategories() ([]models.TaskCategory, error) {
var categories []models.TaskCategory
err := r.db.Order("display_order, name").Find(&categories).Error
return categories, err
}
// GetAllPriorities returns all task priorities
func (r *TaskRepository) GetAllPriorities() ([]models.TaskPriority, error) {
var priorities []models.TaskPriority
err := r.db.Order("level").Find(&priorities).Error
return priorities, err
}
// GetAllStatuses returns all task statuses
func (r *TaskRepository) GetAllStatuses() ([]models.TaskStatus, error) {
var statuses []models.TaskStatus
err := r.db.Order("display_order").Find(&statuses).Error
return statuses, err
}
// GetAllFrequencies returns all task frequencies
func (r *TaskRepository) GetAllFrequencies() ([]models.TaskFrequency, error) {
var frequencies []models.TaskFrequency
err := r.db.Order("display_order").Find(&frequencies).Error
return frequencies, err
}
// FindStatusByName finds a status by name
func (r *TaskRepository) FindStatusByName(name string) (*models.TaskStatus, error) {
var status models.TaskStatus
err := r.db.Where("name = ?", name).First(&status).Error
if err != nil {
return nil, err
}
return &status, nil
}
// CountByResidence counts tasks in a residence
func (r *TaskRepository) CountByResidence(residenceID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Task{}).
Where("residence_id = ? AND is_cancelled = ? AND is_archived = ?", residenceID, false, false).
Count(&count).Error
return count, err
}
// === Task Completion Operations ===
// CreateCompletion creates a new task completion
func (r *TaskRepository) CreateCompletion(completion *models.TaskCompletion) error {
return r.db.Create(completion).Error
}
// FindCompletionByID finds a completion by ID
func (r *TaskRepository) FindCompletionByID(id uint) (*models.TaskCompletion, error) {
var completion models.TaskCompletion
err := r.db.Preload("Task").
Preload("CompletedBy").
First(&completion, id).Error
if err != nil {
return nil, err
}
return &completion, nil
}
// FindCompletionsByTask finds all completions for a task
func (r *TaskRepository) FindCompletionsByTask(taskID uint) ([]models.TaskCompletion, error) {
var completions []models.TaskCompletion
err := r.db.Preload("CompletedBy").
Where("task_id = ?", taskID).
Order("completed_at DESC").
Find(&completions).Error
return completions, err
}
// FindCompletionsByUser finds all completions by a user
func (r *TaskRepository) FindCompletionsByUser(userID uint, residenceIDs []uint) ([]models.TaskCompletion, error) {
var completions []models.TaskCompletion
err := r.db.Preload("Task").
Preload("CompletedBy").
Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
Where("task_task.residence_id IN ?", residenceIDs).
Order("completed_at DESC").
Find(&completions).Error
return completions, err
}
// DeleteCompletion deletes a task completion
func (r *TaskRepository) DeleteCompletion(id uint) error {
return r.db.Delete(&models.TaskCompletion{}, id).Error
}

View File

@@ -0,0 +1,373 @@
package repositories
import (
"errors"
"strings"
"time"
"gorm.io/gorm"
"github.com/treytartt/mycrib-api/internal/models"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists")
ErrInvalidToken = errors.New("invalid token")
ErrTokenNotFound = errors.New("token not found")
ErrCodeNotFound = errors.New("code not found")
ErrCodeExpired = errors.New("code expired")
ErrCodeUsed = errors.New("code already used")
ErrTooManyAttempts = errors.New("too many attempts")
ErrRateLimitExceeded = errors.New("rate limit exceeded")
)
// UserRepository handles user-related database operations
type UserRepository struct {
db *gorm.DB
}
// NewUserRepository creates a new user repository
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
// FindByID finds a user by ID
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
var user models.User
if err := r.db.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
// FindByIDWithProfile finds a user by ID with profile preloaded
func (r *UserRepository) FindByIDWithProfile(id uint) (*models.User, error) {
var user models.User
if err := r.db.Preload("Profile").First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
// FindByUsername finds a user by username (case-insensitive)
func (r *UserRepository) FindByUsername(username string) (*models.User, error) {
var user models.User
if err := r.db.Where("LOWER(username) = LOWER(?)", username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
// FindByEmail finds a user by email (case-insensitive)
func (r *UserRepository) FindByEmail(email string) (*models.User, error) {
var user models.User
if err := r.db.Where("LOWER(email) = LOWER(?)", email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
// FindByUsernameOrEmail finds a user by username or email
func (r *UserRepository) FindByUsernameOrEmail(identifier string) (*models.User, error) {
var user models.User
if err := r.db.Where("LOWER(username) = LOWER(?) OR LOWER(email) = LOWER(?)", identifier, identifier).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
// Create creates a new user
func (r *UserRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}
// Update updates a user
func (r *UserRepository) Update(user *models.User) error {
return r.db.Save(user).Error
}
// UpdateLastLogin updates the user's last login timestamp
func (r *UserRepository) UpdateLastLogin(userID uint) error {
now := time.Now().UTC()
return r.db.Model(&models.User{}).Where("id = ?", userID).Update("last_login", now).Error
}
// ExistsByUsername checks if a username exists
func (r *UserRepository) ExistsByUsername(username string) (bool, error) {
var count int64
if err := r.db.Model(&models.User{}).Where("LOWER(username) = LOWER(?)", username).Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
// ExistsByEmail checks if an email exists
func (r *UserRepository) ExistsByEmail(email string) (bool, error) {
var count int64
if err := r.db.Model(&models.User{}).Where("LOWER(email) = LOWER(?)", email).Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
// --- Auth Token Methods ---
// GetOrCreateToken gets or creates an auth token for a user
func (r *UserRepository) GetOrCreateToken(userID uint) (*models.AuthToken, error) {
var token models.AuthToken
result := r.db.Where("user_id = ?", userID).First(&token)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
token = models.AuthToken{UserID: userID}
if err := r.db.Create(&token).Error; err != nil {
return nil, err
}
} else if result.Error != nil {
return nil, result.Error
}
return &token, nil
}
// DeleteToken deletes an auth token
func (r *UserRepository) DeleteToken(token string) error {
result := r.db.Where("key = ?", token).Delete(&models.AuthToken{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrTokenNotFound
}
return nil
}
// DeleteTokenByUserID deletes an auth token by user ID
func (r *UserRepository) DeleteTokenByUserID(userID uint) error {
return r.db.Where("user_id = ?", userID).Delete(&models.AuthToken{}).Error
}
// --- User Profile Methods ---
// GetOrCreateProfile gets or creates a user profile
func (r *UserRepository) GetOrCreateProfile(userID uint) (*models.UserProfile, error) {
var profile models.UserProfile
result := r.db.Where("user_id = ?", userID).First(&profile)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
profile = models.UserProfile{UserID: userID}
if err := r.db.Create(&profile).Error; err != nil {
return nil, err
}
} else if result.Error != nil {
return nil, result.Error
}
return &profile, nil
}
// UpdateProfile updates a user profile
func (r *UserRepository) UpdateProfile(profile *models.UserProfile) error {
return r.db.Save(profile).Error
}
// SetProfileVerified sets the profile verified status
func (r *UserRepository) SetProfileVerified(userID uint, verified bool) error {
return r.db.Model(&models.UserProfile{}).Where("user_id = ?", userID).Update("verified", verified).Error
}
// --- Confirmation Code Methods ---
// CreateConfirmationCode creates a new confirmation code
func (r *UserRepository) CreateConfirmationCode(userID uint, code string, expiresAt time.Time) (*models.ConfirmationCode, error) {
// Invalidate any existing unused codes for this user
r.db.Model(&models.ConfirmationCode{}).
Where("user_id = ? AND is_used = ?", userID, false).
Update("is_used", true)
confirmCode := &models.ConfirmationCode{
UserID: userID,
Code: code,
ExpiresAt: expiresAt,
IsUsed: false,
}
if err := r.db.Create(confirmCode).Error; err != nil {
return nil, err
}
return confirmCode, nil
}
// FindConfirmationCode finds a valid confirmation code for a user
func (r *UserRepository) FindConfirmationCode(userID uint, code string) (*models.ConfirmationCode, error) {
var confirmCode models.ConfirmationCode
if err := r.db.Where("user_id = ? AND code = ? AND is_used = ?", userID, code, false).
First(&confirmCode).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrCodeNotFound
}
return nil, err
}
if !confirmCode.IsValid() {
if confirmCode.IsUsed {
return nil, ErrCodeUsed
}
return nil, ErrCodeExpired
}
return &confirmCode, nil
}
// MarkConfirmationCodeUsed marks a confirmation code as used
func (r *UserRepository) MarkConfirmationCodeUsed(codeID uint) error {
return r.db.Model(&models.ConfirmationCode{}).Where("id = ?", codeID).Update("is_used", true).Error
}
// --- Password Reset Code Methods ---
// CreatePasswordResetCode creates a new password reset code
func (r *UserRepository) CreatePasswordResetCode(userID uint, codeHash string, resetToken string, expiresAt time.Time) (*models.PasswordResetCode, error) {
// Invalidate any existing unused codes for this user
r.db.Model(&models.PasswordResetCode{}).
Where("user_id = ? AND used = ?", userID, false).
Update("used", true)
resetCode := &models.PasswordResetCode{
UserID: userID,
CodeHash: codeHash,
ResetToken: resetToken,
ExpiresAt: expiresAt,
Used: false,
Attempts: 0,
MaxAttempts: 5,
}
if err := r.db.Create(resetCode).Error; err != nil {
return nil, err
}
return resetCode, nil
}
// FindPasswordResetCode finds a password reset code by email and checks validity
func (r *UserRepository) FindPasswordResetCodeByEmail(email string) (*models.PasswordResetCode, *models.User, error) {
user, err := r.FindByEmail(email)
if err != nil {
return nil, nil, err
}
var resetCode models.PasswordResetCode
if err := r.db.Where("user_id = ? AND used = ?", user.ID, false).
Order("created_at DESC").
First(&resetCode).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, ErrCodeNotFound
}
return nil, nil, err
}
return &resetCode, user, nil
}
// FindPasswordResetCodeByToken finds a password reset code by reset token
func (r *UserRepository) FindPasswordResetCodeByToken(resetToken string) (*models.PasswordResetCode, error) {
var resetCode models.PasswordResetCode
if err := r.db.Where("reset_token = ?", resetToken).First(&resetCode).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrCodeNotFound
}
return nil, err
}
if !resetCode.IsValid() {
if resetCode.Used {
return nil, ErrCodeUsed
}
if resetCode.Attempts >= resetCode.MaxAttempts {
return nil, ErrTooManyAttempts
}
return nil, ErrCodeExpired
}
return &resetCode, nil
}
// IncrementResetCodeAttempts increments the attempt counter
func (r *UserRepository) IncrementResetCodeAttempts(codeID uint) error {
return r.db.Model(&models.PasswordResetCode{}).Where("id = ?", codeID).
Update("attempts", gorm.Expr("attempts + 1")).Error
}
// MarkPasswordResetCodeUsed marks a password reset code as used
func (r *UserRepository) MarkPasswordResetCodeUsed(codeID uint) error {
return r.db.Model(&models.PasswordResetCode{}).Where("id = ?", codeID).Update("used", true).Error
}
// CountRecentPasswordResetRequests counts reset requests in the last hour
func (r *UserRepository) CountRecentPasswordResetRequests(userID uint) (int64, error) {
var count int64
oneHourAgo := time.Now().UTC().Add(-1 * time.Hour)
if err := r.db.Model(&models.PasswordResetCode{}).
Where("user_id = ? AND created_at > ?", userID, oneHourAgo).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// --- Search Methods ---
// SearchUsers searches users by username, email, first name, or last name
func (r *UserRepository) SearchUsers(query string, limit, offset int) ([]models.User, int64, error) {
var users []models.User
var total int64
searchQuery := "%" + strings.ToLower(query) + "%"
baseQuery := r.db.Model(&models.User{}).
Where("LOWER(username) LIKE ? OR LOWER(email) LIKE ? OR LOWER(first_name) LIKE ? OR LOWER(last_name) LIKE ?",
searchQuery, searchQuery, searchQuery, searchQuery)
if err := baseQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := baseQuery.Offset(offset).Limit(limit).Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
// ListUsers lists all users with pagination
func (r *UserRepository) ListUsers(limit, offset int) ([]models.User, int64, error) {
var users []models.User
var total int64
if err := r.db.Model(&models.User{}).Count(&total).Error; err != nil {
return nil, 0, err
}
if err := r.db.Offset(offset).Limit(limit).Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}