diff --git a/API.md b/API.md new file mode 100644 index 0000000..ad3979d --- /dev/null +++ b/API.md @@ -0,0 +1,169 @@ + +# API Reference + +Base URL: `http://localhost:8080` + +## Public Routes + +### GET /health +Check if the server is running. + +**Response** +```json +{ "status": "healthy", + "database": "connected" } +``` + +--- + +## Auth Routes + +No authentication required. + +### POST /api/verify-token +Exchange a Firebase ID token for a custom JWT access token + refresh token. Creates the user in the database if they don't exist yet. + +**Request** +```json +{ + "token": "" +} +``` + +**Response** +```json +{ + "access_token": "eyJ...", + "refresh_token": "eyJ...", + "expires_in": 604800, + "user": { + "id": 1, + "firebase_uid": "abc123", + "email": "user@example.com", + "firstname": "Tran", + "lastname": "Tran" + } +} +``` + +--- + +### POST /api/refresh-token +Get a new access token using a refresh token. + +**Request** +```json +{ + "refresh_token": "eyJ..." +} +``` + +**Response** +```json +{ + "access_token": "eyJ...", + "refresh_token": "eyJ...", + "expires_in": 604800 +} +``` + +--- + +## Protected Routes + +Requires `Authorization: Bearer ` header. + +**Request** +```json +{ + "firstname": "Tran", + "lastname": "Tran", + "email": "user@example.com" +} +``` + +**Response** +```json +{ + "data": { + "id": 1, + "firebase_uid": "abc123", + "firstname": "Tran", + "lastname": "Tran", + "email": "user@example.com", + "is_admin": false, + "created_at": "2026-03-10T00:00:00Z", + "updated_at": "2026-03-10T00:00:00Z" + } +} +``` + +--- + +### POST /api/fcm/register +Register a device FCM token for push notifications. + +**Request** +```json +{ + "token": "" +} +``` + +--- + +### DELETE /api/fcm/delete +Remove a device FCM token. + +**Request** +```json +{ + "token": "" +} +``` + +--- + +### POST /api/fcm/test +Send a test push notification to the authenticated user's devices. + +--- + +## Admin Routes + +Requires `Authorization: Bearer ` header. User must have `is_admin = true` in the database. + +Returns `403 Forbidden` if the user is not an admin. + +### GET /api/admin/users +Get all users. + +**Response** +```json +{ + "data": [ + { + "id": 1, + "firebase_uid": "abc123", + "firstname": "Tran", + "lastname": "Tran", + "email": "user@example.com", + "is_admin": true, + "created_at": "2026-03-10T00:00:00Z", + "updated_at": "2026-03-10T00:00:00Z" + } + ] +} +``` + +--- + +## Error Responses + +| Status | Meaning | +|--------|---------| +| 400 | Bad request — missing or invalid input | +| 401 | Unauthorized — missing or invalid token | +| 403 | Forbidden — not an admin | +| 404 | Not found | +| 500 | Internal server error | \ No newline at end of file diff --git a/controllers/services.go b/controllers/services.go deleted file mode 100644 index f9a0f19..0000000 --- a/controllers/services.go +++ /dev/null @@ -1,328 +0,0 @@ -package controllers - -import ( - "net/http" - "strconv" - - "github.com/cuappdev/chimes-backend/middleware" - "github.com/cuappdev/chimes-backend/models" - "github.com/cuappdev/chimes-backend/services" - "github.com/gin-gonic/gin" -) - -type ServiceController struct { - listingService *services.ListingService -} - -func NewServiceController() *ServiceController { - return &ServiceController{ - listingService: services.NewListingService(), - } -} - -// CreateServiceListingInput defines the input for creating a service listing -type CreateServiceListingInput struct { - Description string `json:"description" binding:"required"` - Categories string `json:"categories" binding:"required"` -} - -// CreateServiceInput defines the input for adding a service to a listing -type CreateServiceInput struct { - Title string `json:"title" binding:"required"` - Price float64 `json:"price" binding:"required"` - PriceUnit string `json:"price_unit" binding:"required"` -} - -// CreateServiceListing creates a new service listing -// POST /service-listings -func (sc *ServiceController) CreateServiceListing(c *gin.Context) { - // Get seller ID from auth middleware - sellerIDStr := middleware.UIDFrom(c) - if sellerIDStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - sellerID, err := strconv.ParseUint(sellerIDStr, 10, 32) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID"}) - return - } - - // Validate input - var input CreateServiceListingInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Use service to create listing - serviceListing, err := sc.listingService.CreateServiceListing(uint(sellerID), input.Description, input.Categories) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service listing"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": serviceListing}) -} - -// GetServiceListing retrieves a service listing with its services -// GET /service-listings/:id -func (sc *ServiceController) GetServiceListing(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) - return - } - - serviceListing, err := sc.listingService.GetServiceListingWithServices(uint(id)) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Service listing not found"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": serviceListing}) -} - -// GetServiceListings retrieves all service listings for the current user (seller) -// GET /service-listings -func (sc *ServiceController) GetServiceListings(c *gin.Context) { - // Get seller ID from auth middleware - sellerIDStr := middleware.UIDFrom(c) - if sellerIDStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - sellerID, err := strconv.ParseUint(sellerIDStr, 10, 32) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID"}) - return - } - - serviceListings, err := sc.listingService.GetServiceListings(uint(sellerID)) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch service listings"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": serviceListings}) -} - -// UpdateServiceListing updates a service listing -// PATCH /service-listings/:id -func (sc *ServiceController) UpdateServiceListing(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) - return - } - - // Get seller ID from auth middleware - sellerIDStr := middleware.UIDFrom(c) - if sellerIDStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - sellerID, err := strconv.ParseUint(sellerIDStr, 10, 32) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID"}) - return - } - - // Validate input - var input CreateServiceListingInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Use service to update listing - serviceListing, err := sc.listingService.UpdateServiceListing(uint(id), uint(sellerID), input.Description, input.Categories) - if err == models.ErrNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "Service listing not found or unauthorized"}) - return - } else if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service listing"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": serviceListing}) -} - -// DeleteServiceListing deletes a service listing and its services -// DELETE /service-listings/:id -func (sc *ServiceController) DeleteServiceListing(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) - return - } - - // Get seller ID from auth middleware - sellerIDStr := middleware.UIDFrom(c) - if sellerIDStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - sellerID, err := strconv.ParseUint(sellerIDStr, 10, 32) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID"}) - return - } - - // Use service to delete listing - err = sc.listingService.DeleteServiceListing(uint(id), uint(sellerID)) - if err == models.ErrNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "Service listing not found or unauthorized"}) - return - } else if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete service listing"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Service listing deleted successfully"}) -} - -// AddService adds a new service to a service listing -// POST /service-listings/:id/services -func (sc *ServiceController) AddService(c *gin.Context) { - // Get service listing ID from URL - listingID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service listing ID"}) - return - } - - // Get seller ID from auth middleware - sellerIDStr := middleware.UIDFrom(c) - if sellerIDStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - sellerID, err := strconv.ParseUint(sellerIDStr, 10, 32) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID"}) - return - } - - // Validate input - var input CreateServiceInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Use service to add service to listing - service, err := sc.listingService.AddService(uint(listingID), uint(sellerID), input.Title, input.Price, input.PriceUnit) - if err == models.ErrNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "Service listing not found or unauthorized"}) - return - } else if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": service}) -} - -// GetServices retrieves all services for a service listing -// GET /service-listings/:id/services -func (sc *ServiceController) GetServices(c *gin.Context) { - // Get service listing ID from URL - listingID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service listing ID"}) - return - } - - services, err := sc.listingService.GetServices(uint(listingID)) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch services"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": services}) -} - -// UpdateService updates a service -// PATCH /services/:id -func (sc *ServiceController) UpdateService(c *gin.Context) { - // Get service ID from URL - serviceID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"}) - return - } - - // Get seller ID from auth middleware - sellerIDStr := middleware.UIDFrom(c) - if sellerIDStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - sellerID, err := strconv.ParseUint(sellerIDStr, 10, 32) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID"}) - return - } - - // Validate input - var input CreateServiceInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Use service to update service - service, err := sc.listingService.UpdateService(uint(serviceID), uint(sellerID), input.Title, input.Price, input.PriceUnit) - if err == models.ErrNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "Service not found or unauthorized"}) - return - } else if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service"}) - return - } - - c.JSON(http.StatusOK, gin.H{"data": service}) -} - -// DeleteService deletes a service -// DELETE /services/:id -func (sc *ServiceController) DeleteService(c *gin.Context) { - // Get service ID from URL - serviceID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"}) - return - } - - // Get seller ID from auth middleware - sellerIDStr := middleware.UIDFrom(c) - if sellerIDStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - sellerID, err := strconv.ParseUint(sellerIDStr, 10, 32) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID"}) - return - } - - // Use service to delete service - err = sc.listingService.DeleteService(uint(serviceID), uint(sellerID)) - if err == models.ErrNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "Service not found or unauthorized"}) - return - } else if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete service"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Service deleted successfully"}) -} diff --git a/controllers/users.go b/controllers/users.go index c2b7551..bf3fab3 100644 --- a/controllers/users.go +++ b/controllers/users.go @@ -1,53 +1,24 @@ package controllers import ( -"net/http" -"strings" + "net/http" + "strings" -"github.com/gin-gonic/gin" -"github.com/cuappdev/chimes-backend/models" -"github.com/cuappdev/chimes-backend/middleware" - "github.com/cuappdev/chimes-backend/auth" firebaseauth "firebase.google.com/go/v4/auth" + "github.com/cuappdev/chimes-backend/auth" + "github.com/cuappdev/chimes-backend/models" + "github.com/gin-gonic/gin" ) // GET /users // Get all users func FindUsers(c *gin.Context) { - var users []models.User + var users []models.UserResponse models.DB.Find(&users) c.JSON(http.StatusOK, gin.H{"data": users}) } -// POST /users -// Create new user -func CreateUser(c *gin.Context) { - // Validate input - var input models.CreateUserInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - uid := middleware.UIDFrom(c) - if uid == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "midding firebase uid"}) - return - } - - // Create user - user := models.User{ - FirstName: input.FirstName, - LastName: input.LastName, - Email: input.Email, - Firebase_UID: uid, - } - models.DB.Create(&user) - - c.JSON(http.StatusOK, gin.H{"data": user}) -} - // VerifyTokenRequest represents the request body for token verification type VerifyTokenRequest struct { Token string `json:"token" binding:"required"` @@ -73,11 +44,11 @@ func VerifyToken(firebaseAuthClient *firebaseauth.Client) gin.HandlerFunc { // Extract user data from Firebase token claims := firebaseToken.Claims firebaseUID := firebaseToken.UID - + // Get user info from Firebase token claims email, _ := claims["email"].(string) name, _ := claims["name"].(string) - + // Parse name into first and last name nameParts := strings.Fields(name) firstName := "" @@ -116,11 +87,11 @@ func VerifyToken(firebaseAuthClient *firebaseauth.Client) gin.HandlerFunc { "refresh_token": tokenPair.RefreshToken, "expires_in": tokenPair.ExpiresIn, "user": gin.H{ - "id": user.ID, + "id": user.ID, "firebase_uid": user.Firebase_UID, - "email": user.Email, - "firstname": user.FirstName, - "lastname": user.LastName, + "email": user.Email, + "firstname": user.FirstName, + "lastname": user.LastName, }, }) } @@ -183,4 +154,3 @@ func RefreshToken() gin.HandlerFunc { }) } } - diff --git a/docker-compose.yml b/docker-compose.yml index 9393e17..3ca70dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,13 +2,13 @@ services: db: image: postgres:16 environment: - POSTGRES_DB: chimes - POSTGRES_USER: postgres - POSTGRES_PASSWORD: tnt + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pg_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d chimes"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 5s timeout: 5s retries: 10 diff --git a/hustle-backend b/hustle-backend deleted file mode 100755 index 52c22a6..0000000 Binary files a/hustle-backend and /dev/null differ diff --git a/main.go b/main.go index 8a570d1..543bd22 100644 --- a/main.go +++ b/main.go @@ -62,15 +62,20 @@ func main() { authd := api.Group("") authd.Use(middleware.RequireAuth(ac)) { - // User routes - authd.GET("/users", controllers.FindUsers) - authd.POST("/users", controllers.CreateUser) // Notification routes authd.POST("/fcm/register", controllers.RegisterFCMToken) authd.DELETE("/fcm/delete", controllers.DeleteFCMToken) authd.POST("/fcm/test", controllers.SendTestNotification) } + + //Admin routes + admin := api.Group("/admin") + admin.Use(middleware.RequireAuth(ac), middleware.RequireAdmin) + { + //User routes + admin.GET("/users", controllers.FindUsers) + } log.Println("Server starting on :8080") r.Run() diff --git a/middleware/auth.go b/middleware/auth.go index b979b79..e49d3fd 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -7,6 +7,7 @@ import ( firebaseauth "firebase.google.com/go/v4/auth" "github.com/cuappdev/chimes-backend/auth" + "github.com/cuappdev/chimes-backend/models" "github.com/gin-gonic/gin" ) @@ -49,6 +50,24 @@ func RequireAuth(firebaseAuthClient *firebaseauth.Client) gin.HandlerFunc { } } +func RequireAdmin(c *gin.Context) { + uid := UIDFrom(c) + if uid == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing uid"}) + return + } + var user models.User + if err := models.DB.Where("firebase_uid = ?", uid).First(&user).Error; err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not found"}) + return + } + if !user.IsAdmin { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin access required"}) + return + } + c.Next() +} + // RequireFirebaseUser validates only Firebase tokens (for backward compatibility) func RequireFirebaseUser(ac *firebaseauth.Client) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/models/services.go b/models/services.go deleted file mode 100644 index 718f6bd..0000000 --- a/models/services.go +++ /dev/null @@ -1,90 +0,0 @@ -package models - -import ( - "time" -) - -// ServiceListing represents a service listing created by a seller -type ServiceListing struct { - ID uint `json:"id" gorm:"primary_key"` - SellerID uint `json:"seller_id" gorm:"index;not null"` - Description string `json:"description"` - Categories string `json:"categories"` // Comma-separated categories - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Seller Seller `json:"-" gorm:"foreignKey:SellerID"` - Services []Service `json:"services" gorm:"foreignKey:ServiceListingID"` -} - -// Service represents an individual service offered within a service listing -type Service struct { - ID uint `json:"id" gorm:"primary_key"` - ServiceListingID uint `json:"service_listing_id" gorm:"index;not null"` - Title string `json:"title" gorm:"not null"` - Price float64 `json:"price" gorm:"not null"` - PriceUnit string `json:"price_unit" gorm:"not null"` // e.g., "per hour", "per cut" - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ServiceListing ServiceListing `json:"-" gorm:"foreignKey:ServiceListingID"` -} - -// CreateServiceListing creates a new service listing -func CreateServiceListing(sellerID uint, description string, categories string) (*ServiceListing, error) { - serviceListing := ServiceListing{ - SellerID: sellerID, - Description: description, - Categories: categories, - } - - if err := DB.Create(&serviceListing).Error; err != nil { - return nil, err - } - - return &serviceListing, nil -} - -// AddService adds a new service to an existing service listing -func (sl *ServiceListing) AddService(title string, price float64, priceUnit string) (*Service, error) { - service := Service{ - ServiceListingID: sl.ID, - Title: title, - Price: price, - PriceUnit: priceUnit, - } - - if err := DB.Create(&service).Error; err != nil { - return nil, err - } - - return &service, nil -} - -// GetServiceListingsBySellerID retrieves all service listings for a specific seller -func GetServiceListingsBySellerID(sellerID uint) ([]ServiceListing, error) { - var serviceListings []ServiceListing - err := DB.Where("seller_id = ?", sellerID).Find(&serviceListings).Error - if err != nil { - return nil, err - } - return serviceListings, nil -} - -// GetServicesByListingID retrieves all services for a specific service listing -func GetServicesByListingID(serviceListingID uint) ([]Service, error) { - var services []Service - err := DB.Where("service_listing_id = ?", serviceListingID).Find(&services).Error - if err != nil { - return nil, err - } - return services, nil -} - -// GetServiceListingWithServices retrieves a service listing with all its services -func GetServiceListingWithServices(serviceListingID uint) (*ServiceListing, error) { - var serviceListing ServiceListing - err := DB.Preload("Services").Where("id = ?", serviceListingID).First(&serviceListing).Error - if err != nil { - return nil, err - } - return &serviceListing, nil -} \ No newline at end of file diff --git a/models/setup.go b/models/setup.go index 1ea52fb..6d44dbb 100644 --- a/models/setup.go +++ b/models/setup.go @@ -1,55 +1,56 @@ package models import ( - "fmt" - "log" - "os" - "time" - "gorm.io/gorm" - "gorm.io/driver/postgres" + "fmt" + "log" + "os" + "time" + + "gorm.io/driver/postgres" + "gorm.io/gorm" ) var DB *gorm.DB func ConnectDatabase() error { - // Build connection string from env variables - host := getEnv("DB_HOST", "localhost") - user := getEnv("DB_USER", "postgres") - dbname := getEnv("DB_NAME", "chimes") - port := getEnv("DB_PORT", "5432") - password := getEnv("DB_PASSWORD", "") - sslmode := getEnv("DB_SSLMODE", "require") - - dsn := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=%s", user, password, host, port, dbname, sslmode) - - database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - return fmt.Errorf("failed to connect to database: %w", err) - } - - sqlDB, err := database.DB() - if err != nil { - return fmt.Errorf("failed to get database instance: %w", err) - } - - sqlDB.SetMaxIdleConns(10) - sqlDB.SetMaxOpenConns(100) - sqlDB.SetConnMaxLifetime(time.Hour) - - // Make sure to include all models to migrate here - err = database.AutoMigrate(&User{}, &Seller{}, &FCMToken{}, &ServiceListing{}, &Service{}) - if err != nil { - return fmt.Errorf("failed to migrate database: %w", err) - } - - DB = database - log.Println("Database connected successfully") - return nil + // Build connection string from env variables + host := getEnv("DB_HOST", "localhost") + user := getEnv("DB_USER", "postgres") + dbname := getEnv("DB_NAME", "chimes") + port := getEnv("DB_PORT", "5432") + password := getEnv("DB_PASSWORD", "") + sslmode := getEnv("DB_SSLMODE", "require") + + dsn := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=%s", user, password, host, port, dbname, sslmode) + + database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return fmt.Errorf("failed to connect to database: %w", err) + } + + sqlDB, err := database.DB() + if err != nil { + return fmt.Errorf("failed to get database instance: %w", err) + } + + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + // Make sure to include all models to migrate here + err = database.AutoMigrate(&User{}, &FCMToken{}) + if err != nil { + return fmt.Errorf("failed to migrate database: %w", err) + } + + DB = database + log.Println("Database connected successfully") + return nil } func getEnv(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} \ No newline at end of file + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} diff --git a/models/users.go b/models/users.go index 874c982..8707718 100644 --- a/models/users.go +++ b/models/users.go @@ -1,42 +1,36 @@ package models import ( - "log" + "log" + "time" ) type User struct { - ID uint `json:"id" gorm:"primary_key"` - Firebase_UID string `json:"firebase_uid" gorm:"uniqueIndex"` - Refresh_Token string `json:"refresh_token"` - FirstName string `json:"firstname"` - LastName string `json:"lastname"` - Email string `json:"email"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID uint `json:"id" gorm:"primary_key"` + Firebase_UID string `json:"firebase_uid" gorm:"uniqueIndex"` + Refresh_Token string `json:"refresh_token"` + FirstName string `json:"firstname"` + LastName string `json:"lastname"` + Email string `json:"email"` + IsAdmin bool `json:"is_admin"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } -type CreateUserInput struct { - FirstName string `json:"firstname" binding:"required"` - LastName string `json:"lastname" binding:"required"` - Email string `json:"email" binding:"required"` -} - -type Seller struct { - ID uint `json:"id" gorm:"primary_key"` - UserID uint `json:"user_id"` - Description string `json:"description"` - IsActive bool `json:"is_active"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` +type UserResponse struct { + ID uint `json:"id" gorm:"primary_key"` + FirstName string `json:"firstname"` + LastName string `json:"lastname"` + Email string `json:"email"` } // FindOrCreateUser finds an existing user by Firebase UID or creates a new one func FindOrCreateUser(firebaseUID, email, firstName, lastName string) (*User, error) { var user User - + // Try to find existing user result := DB.Where("firebase_uid = ?", firebaseUID).First(&user) - + if result.Error != nil { log.Printf("[ERROR] User not found by Firebase UID (%s): %v", firebaseUID, result.Error) // User doesn't exist, create new one @@ -46,17 +40,17 @@ func FindOrCreateUser(firebaseUID, email, firstName, lastName string) (*User, er FirstName: firstName, LastName: lastName, } - + if err := DB.Create(&user).Error; err != nil { log.Printf("[ERROR] Failed to create user (Firebase UID: %s): %v", firebaseUID, err) return nil, err } } - + return &user, nil } // UpdateRefreshToken updates the user's refresh token func (u *User) UpdateRefreshToken(refreshToken string) error { return DB.Model(u).Update("refresh_token", refreshToken).Error -} \ No newline at end of file +} diff --git a/services/listingservice.go b/services/listingservice.go deleted file mode 100644 index 86870fc..0000000 --- a/services/listingservice.go +++ /dev/null @@ -1,115 +0,0 @@ -package services - -import ( - "github.com/cuappdev/chimes-backend/models" -) - -type ListingService struct { - // Add any dependencies here -} - -func NewListingService() *ListingService { - return &ListingService{} -} - -// CreateServiceListing creates a new service listing with validation -func (s *ListingService) CreateServiceListing(sellerID uint, description, categories string) (*models.ServiceListing, error) { - return models.CreateServiceListing(sellerID, description, categories) -} - -// AddService adds a service to an existing listing with validation -func (s *ListingService) AddService(listingID, sellerID uint, title string, price float64, priceUnit string) (*models.Service, error) { - // Verify the listing exists and belongs to the seller - var listing models.ServiceListing - if err := models.DB.Where("id = ? AND seller_id = ?", listingID, sellerID).First(&listing).Error; err != nil { - return nil, err - } - - // Add any additional business logic here (e.g., price validation, etc.) - return listing.AddService(title, price, priceUnit) -} - -// GetServiceListings retrieves all listings for a seller -func (s *ListingService) GetServiceListings(sellerID uint) ([]models.ServiceListing, error) { - return models.GetServiceListingsBySellerID(sellerID) -} - -// UpdateServiceListing updates a service listing with validation -func (s *ListingService) UpdateServiceListing(id, sellerID uint, description, categories string) (*models.ServiceListing, error) { - // Find and verify ownership - var listing models.ServiceListing - if err := models.DB.Where("id = ? AND seller_id = ?", id, sellerID).First(&listing).Error; err != nil { - return nil, err - } - - // Update fields - listing.Description = description - listing.Categories = categories - - if err := models.DB.Save(&listing).Error; err != nil { - return nil, err - } - - return &listing, nil -} - -// DeleteServiceListing deletes a service listing and its services -func (s *ListingService) DeleteServiceListing(id, sellerID uint) error { - // Delete services first (due to foreign key constraint) - if err := models.DB.Where("service_listing_id = ?", id).Delete(&models.Service{}).Error; err != nil { - return err - } - - // Delete the service listing - result := models.DB.Where("id = ? AND seller_id = ?", id, sellerID).Delete(&models.ServiceListing{}) - if result.Error != nil { - return result.Error - } - - if result.RowsAffected == 0 { - return models.ErrNotFound - } - - return nil -} - -// GetServices retrieves all services for a listing with validation -func (s *ListingService) GetServices(listingID uint) ([]models.Service, error) { - return models.GetServicesByListingID(listingID) -} - -// UpdateService updates a service with validation -func (s *ListingService) UpdateService(serviceID, sellerID uint, title string, price float64, priceUnit string) (*models.Service, error) { - // Find the service with its listing to verify ownership - var service models.Service - if err := models.DB.Joins("JOIN service_listings ON services.service_listing_id = service_listings.id"). - Where("services.id = ? AND service_listings.seller_id = ?", serviceID, sellerID). - First(&service).Error; err != nil { - return nil, err - } - - // Update fields - service.Title = title - service.Price = price - service.PriceUnit = priceUnit - - if err := models.DB.Save(&service).Error; err != nil { - return nil, err - } - - return &service, nil -} - -// DeleteService deletes a service with validation -func (s *ListingService) DeleteService(serviceID, sellerID uint) error { - // Find the service with its listing to verify ownership - var service models.Service - if err := models.DB.Joins("JOIN service_listings ON services.service_listing_id = service_listings.id"). - Where("services.id = ? AND service_listings.seller_id = ?", serviceID, sellerID). - First(&service).Error; err != nil { - return err - } - - // Delete the service - return models.DB.Delete(&service).Error -}