diff --git a/API.md b/API.md new file mode 100644 index 0000000..e29dc6f --- /dev/null +++ b/API.md @@ -0,0 +1,170 @@ +# API Reference + +Base URL: `http://localhost:8080` + +## Public Routes + +### GET /healthcheck +Check if the server is running. + +**Response** +```json +{ "message": "Chimes is healthy!" } +``` + +--- + +## 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. + +### POST /api/users +Create a new user. Firebase UID is extracted from the token. + +**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 | diff --git a/README.md b/README.md index f359387..49761c1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ # chimes-backend - diff --git a/controllers/users.go b/controllers/users.go index c2b7551..a0b9e6f 100644 --- a/controllers/users.go +++ b/controllers/users.go @@ -1,14 +1,14 @@ 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/middleware" + "github.com/cuappdev/chimes-backend/models" + "github.com/gin-gonic/gin" ) // GET /users @@ -23,29 +23,29 @@ func FindUsers(c *gin.Context) { // 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}) + // 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": "missing 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 @@ -73,11 +73,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 +116,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 +183,3 @@ func RefreshToken() gin.HandlerFunc { }) } } - 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..d2aa406 100644 --- a/main.go +++ b/main.go @@ -63,14 +63,21 @@ func main() { 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..dc69c89 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,26 @@ func RequireAuth(firebaseAuthClient *firebaseauth.Client) gin.HandlerFunc { } } +// RequireAdmin ensures the request is authenticated and the user is an admin. +// Aborts with 401 if the UID is missing or user not found, and 403 if not admin +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/users.go b/models/users.go index 874c982..5df76fc 100644 --- a/models/users.go +++ b/models/users.go @@ -1,24 +1,26 @@ 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"` + FirstName string `json:"firstname" binding:"required"` + LastName string `json:"lastname" binding:"required"` + Email string `json:"email" binding:"required"` } type Seller struct { @@ -33,10 +35,10 @@ type Seller struct { // 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 +48,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 +}