aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--backend/cmd/api/main.go11
-rw-r--r--backend/internal/api/v1/accounts/accounts.go216
-rw-r--r--frontend/src/lib/api.ts34
4 files changed, 262 insertions, 1 deletions
diff --git a/README.md b/README.md
index 9910111..7b8810a 100644
--- a/README.md
+++ b/README.md
@@ -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;