diff options
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | backend/cmd/api/main.go | 11 | ||||
-rw-r--r-- | backend/internal/api/v1/accounts/accounts.go | 216 | ||||
-rw-r--r-- | frontend/src/lib/api.ts | 34 |
4 files changed, 262 insertions, 1 deletions
@@ -87,7 +87,7 @@ An application designed to help manage personal finances, including income (like * [x] Set up database connection and migrations * [x] Add CORS middleware for cross-origin requests * [x] Implement User Authentication (Signup, Login, JWT/Session Management) -* [ ] Create basic CRUD APIs for Accounts (e.g., Salary Source, Bank Account) +* [x] Create basic CRUD APIs for Accounts (e.g., Salary Source, Bank Account) * [ ] Create basic CRUD APIs for Transactions (Income, Expense) * [x] Create basic CRUD APIs for Loans * [ ] Create basic CRUD APIs for Goals diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index b90962f..9d09012 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -6,6 +6,7 @@ import ( "time" "finance/backend/internal/api/auth" + "finance/backend/internal/api/v1/accounts" "finance/backend/internal/api/v1/loans" "finance/backend/internal/config" "finance/backend/internal/database" @@ -99,6 +100,16 @@ func main() { c.JSON(http.StatusOK, gin.H{"user": user}) }) + // Account routes + accountRoutes := protected.Group("/accounts") + { + accountRoutes.GET("", accounts.GetAccounts()) + accountRoutes.GET("/:id", accounts.GetAccountByID()) + accountRoutes.POST("", accounts.CreateAccount()) + accountRoutes.PUT("/:id", accounts.UpdateAccount()) + accountRoutes.DELETE("/:id", accounts.DeleteAccount()) + } + // Loan routes loanRoutes := protected.Group("/loans") { diff --git a/backend/internal/api/v1/accounts/accounts.go b/backend/internal/api/v1/accounts/accounts.go new file mode 100644 index 0000000..08c1c5c --- /dev/null +++ b/backend/internal/api/v1/accounts/accounts.go @@ -0,0 +1,216 @@ +package accounts + +import ( + "net/http" + "strconv" + + "finance/backend/internal/database" + "finance/backend/internal/models" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// GetAccounts returns all accounts for the authenticated user +func GetAccounts() gin.HandlerFunc { + return func(c *gin.Context) { + // Get user from context (set by auth middleware) + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + userObj := user.(models.User) + var accounts []models.Account + + // Fetch all accounts for the user + if err := database.DB.Where("user_id = ?", userObj.ID).Find(&accounts).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch accounts"}) + return + } + + c.JSON(http.StatusOK, gin.H{"accounts": accounts}) + } +} + +// GetAccountByID returns a specific account by ID +func GetAccountByID() gin.HandlerFunc { + return func(c *gin.Context) { + // Get user from context (set by auth middleware) + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + userObj := user.(models.User) + + // Get account ID from URL parameter + accountID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid account ID format"}) + return + } + + var account models.Account + + // Fetch the account and ensure it belongs to the authenticated user + if err := database.DB.Where("id = ? AND user_id = ?", accountID, userObj.ID).First(&account).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Account not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch account"}) + } + return + } + + c.JSON(http.StatusOK, gin.H{"account": account}) + } +} + +// CreateAccount creates a new account +func CreateAccount() gin.HandlerFunc { + return func(c *gin.Context) { + // Get user from context (set by auth middleware) + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + userObj := user.(models.User) + + // Define a struct to bind the request JSON + var input struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` // e.g., "Bank", "Credit Card", "Cash", "Loan", "Income Source" + Balance int64 `json:"balance" binding:"required"` + } + + // Bind JSON to struct + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Create account object + account := models.Account{ + UserID: userObj.ID, + Name: input.Name, + Type: input.Type, + Balance: input.Balance, + } + + // Save to database + if err := database.DB.Create(&account).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create account"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"account": account}) + } +} + +// UpdateAccount updates an existing account +func UpdateAccount() gin.HandlerFunc { + return func(c *gin.Context) { + // Get user from context (set by auth middleware) + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + userObj := user.(models.User) + + // Get account ID from URL parameter + accountID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid account ID format"}) + return + } + + // Check if the account exists and belongs to the user + var account models.Account + if err := database.DB.Where("id = ? AND user_id = ?", accountID, userObj.ID).First(&account).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Account not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch account"}) + } + return + } + + // Define a struct to bind the request JSON + var input struct { + Name string `json:"name"` + Type string `json:"type"` + Balance int64 `json:"balance"` + } + + // Bind JSON to struct + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update fields if provided + if input.Name != "" { + account.Name = input.Name + } + if input.Type != "" { + account.Type = input.Type + } + // For balance, we should allow setting it to 0, so check if it was provided + if c.Request.Method == "PUT" || c.Request.Method == "PATCH" { + if c.PostForm("balance") != "" || c.GetHeader("Content-Type") == "application/json" { + account.Balance = input.Balance + } + } + + // Save updates to database + if err := database.DB.Save(&account).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update account"}) + return + } + + c.JSON(http.StatusOK, gin.H{"account": account}) + } +} + +// DeleteAccount deletes an account +func DeleteAccount() gin.HandlerFunc { + return func(c *gin.Context) { + // Get user from context (set by auth middleware) + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + userObj := user.(models.User) + + // Get account ID from URL parameter + accountID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid account ID format"}) + return + } + + // Check if the account exists and belongs to the user + var account models.Account + if err := database.DB.Where("id = ? AND user_id = ?", accountID, userObj.ID).First(&account).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Account not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch account"}) + } + return + } + + // Delete the account + if err := database.DB.Delete(&account).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete account"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Account deleted successfully"}) + } +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 35ef8d6..be77f16 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -75,6 +75,40 @@ export const authApi = { } }; +// Account API +export interface Account { + ID: number; + CreatedAt: string; + UpdatedAt: string; + DeletedAt: string | null; + UserID: number; + Name: string; + Type: string; + Balance: number; +} + +export interface AccountInput { + name: string; + type: string; + balance: number; +} + +export const accountApi = { + getAccounts: () => fetchWithAuth('/accounts'), + getAccount: (id: number) => fetchWithAuth(`/accounts/${id}`), + createAccount: (account: AccountInput) => fetchWithAuth('/accounts', { + method: 'POST', + body: JSON.stringify(account) + }), + updateAccount: (id: number, account: Partial<AccountInput>) => fetchWithAuth(`/accounts/${id}`, { + method: 'PUT', + body: JSON.stringify(account) + }), + deleteAccount: (id: number) => fetchWithAuth(`/accounts/${id}`, { + method: 'DELETE' + }) +}; + // Loan API export interface Loan { ID: number; |