Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -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": "<firebase_id_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 <access_token>` 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": "<fcm_device_token>"
}
```

---

### DELETE /api/fcm/delete
Remove a device FCM token.

**Request**
```json
{
"token": "<fcm_device_token>"
}
```

---

### POST /api/fcm/test
Send a test push notification to the authenticated user's devices.

---

## Admin Routes

Requires `Authorization: Bearer <access_token>` 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 |
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# chimes-backend

71 changes: 35 additions & 36 deletions controllers/users.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 := ""
Expand Down Expand Up @@ -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,
},
})
}
Expand Down Expand Up @@ -183,4 +183,3 @@ func RefreshToken() gin.HandlerFunc {
})
}
}

Binary file removed hustle-backend
Binary file not shown.
9 changes: 8 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 21 additions & 0 deletions middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading