diff --git a/handlers/account.go b/handlers/account.go index be7fe08..40930ef 100644 --- a/handlers/account.go +++ b/handlers/account.go @@ -39,7 +39,7 @@ func Register(w http.ResponseWriter, r *http.Request) { user.ID = utils.GenerateUUID() user.Role = "user" - if err := storage.AddUser(user); err != nil { + if err := storage.AddUser(&user); err != nil { log.Println("[api/register] " + r.RemoteAddr + ": " + err.Error()) http.Error(w, "user already exists", http.StatusBadRequest) return @@ -98,7 +98,7 @@ func Login(w http.ResponseWriter, r *http.Request) { deviceInfo := r.Header.Get("User-Agent") - if err := storage.AddRefreshToken(models.RefreshToken{ + if err := storage.AddRefreshToken(&models.RefreshToken{ ID: refreshID, UserID: user.ID, Token: refreshHash, @@ -144,19 +144,7 @@ func Logout(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } func TestHandler(w http.ResponseWriter, r *http.Request) { - claimsRaw := r.Context().Value(auth.UserContextKey) - if claimsRaw == nil { - log.Println("[api/ping] " + r.RemoteAddr + ": No claims found") - http.Error(w, "No claims in context", http.StatusUnauthorized) - return - } - - claims, ok := claimsRaw.(*auth.Claims) - if !ok { - log.Println("[api/ping] " + r.RemoteAddr + ": Invalid claims") - http.Error(w, "Invalid claims", http.StatusUnauthorized) - return - } + claims, _ := utils.IsLoggedIn(w, r) w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(map[string]interface{}{ @@ -198,7 +186,7 @@ func RefreshToken(w http.ResponseWriter, r *http.Request) { newExpires := time.Now().Add(7 * 24 * time.Hour).Unix() //7 days newID := utils.GenerateUUID() deviceInfo := r.Header.Get("User-Agent") - if err = storage.AddRefreshToken(models.RefreshToken{ + if err = storage.AddRefreshToken(&models.RefreshToken{ ID: newID, UserID: tokenRow.UserID, Token: newHash, diff --git a/handlers/expenses.go b/handlers/expenses.go index a2bd67d..86fdd1f 100644 --- a/handlers/expenses.go +++ b/handlers/expenses.go @@ -1,7 +1,66 @@ package handlers -import "net/http" +import ( + "encoding/json" + "net/http" + "shap-planner-backend/models" + "shap-planner-backend/storage" + "shap-planner-backend/utils" +) -func GetExpenses(w http.ResponseWriter, r *http.Request) {} +func Expenses(w http.ResponseWriter, r *http.Request) { + claims, _ := utils.IsLoggedIn(w, r) + switch r.Method { + case http.MethodGet: // -> Get Expenses + break + case http.MethodPost: // -> Create Expense + var body struct { + Expense models.Expense `json:"expense"` + Shares []models.ExpenseShare `json:"shares"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + if claims.UserID != body.Expense.PayerID { // You cannot create an expense in the name of another user + http.Error(w, "Invalid request", http.StatusUnauthorized) + return + } + if body.Expense.ID != "" { + http.Error(w, "Invalid request", http.StatusUnauthorized) + return + } + body.Expense.ID = utils.GenerateUUID() + for _, share := range body.Shares { + if share.ID != "" { + http.Error(w, "Invalid request", http.StatusUnauthorized) + return + } + if share.ExpenseID != "" { + http.Error(w, "Invalid request", http.StatusUnauthorized) + return + } + share.ExpenseID = body.Expense.ID + share.ID = utils.GenerateUUID() + err := storage.AddShare(&share) + if err != nil { + println(err.Error()) + http.Error(w, "Error adding expense", http.StatusBadRequest) // Should never happen + return + } + } + err := storage.AddExpense(&body.Expense) + if err != nil { + println(err.Error()) + http.Error(w, "Error adding expense", http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusCreated) + break + case http.MethodPut: // -> Update Expense + break + case http.MethodDelete: // -> Delete Expense + } +} func AdminPanel(w http.ResponseWriter, r *http.Request) {} diff --git a/models/dbmodels.go b/models/dbmodels.go index 72c5e30..ad8b428 100644 --- a/models/dbmodels.go +++ b/models/dbmodels.go @@ -8,10 +8,19 @@ type User struct { } type Expense struct { - ID string `json:"id"` - Amount int `json:"amt"` - Description string `json:"desc"` - - Payer User `json:"payer"` - Debtors []User `json:"debtors"` + ID string `json:"id"` + PayerID string `json:"payer_id"` + Amount int64 `json:"amount"` + Title string `json:"title"` + Description string `json:"description"` + Attachments []string `json:"attachments"` + CreatedAt int64 `json:"created_at"` + LastUpdatedAt int64 `json:"last_updated_at"` +} + +type ExpenseShare struct { + ID string `json:"id"` + ExpenseID string `json:"expense_id"` + UserID string `json:"user_id"` + ShareCents int64 `json:"share_cents"` } diff --git a/server/server.go b/server/server.go index 7067fbe..37fa3d2 100644 --- a/server/server.go +++ b/server/server.go @@ -59,7 +59,7 @@ func (server *Server) Run() { mux.HandleFunc("/api/logout", handlers.Logout) // Login required - mux.Handle("/api/expenses", auth.AuthMiddleware(server.JWTSecret)(http.HandlerFunc(handlers.GetExpenses))) + mux.Handle("/api/expenses", auth.AuthMiddleware(server.JWTSecret)(http.HandlerFunc(handlers.Expenses))) mux.Handle("/api/ping", auth.AuthMiddleware(server.JWTSecret)(http.HandlerFunc(handlers.TestHandler))) // Admin-only diff --git a/storage/storage.go b/storage/storage.go index bd8262e..9c9b67c 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -2,6 +2,7 @@ package storage import ( "database/sql" + "encoding/json" "errors" "shap-planner-backend/models" "strings" @@ -51,14 +52,89 @@ func InitDB(filepath string) error { //Create Expenses-Table _, err = DB.Exec(`CREATE TABLE IF NOT EXISTS expenses( - id TEXT PRIMARY KEY - + id TEXT PRIMARY KEY, + payer_id TEXT NOT NULL, + amount_cents INTEGER NOT NULL, + title TEXT NOT NULL, + description TEXT, + attachments TEXT, + created_at INTEGER NOT NULL, + last_updated_at INTEGER NOT NULL, + FOREIGN KEY(payer_id) REFERENCES users(id) )`) + if err != nil { + return err + } + _, err = DB.Exec(`CREATE TABLE IF NOT EXISTS expense_shares( + id TEXT PRIMARY KEY, + expense_id TEXT NOT NULL, + user_id TEXT NOT NULL, + share_cents INTEGER NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) + )`) + if err != nil { + return err + } + _, err = DB.Exec(`CREATE INDEX IF NOT EXISTS idx_shares_expense ON expense_shares(expense_id)`) + if err != nil { + return err + } + _, err = DB.Exec(`CREATE INDEX IF NOT EXISTS idx_shares_user ON expense_shares(user_id)`) + return err +} + +// Expenses +func AddExpense(expense *models.Expense) error { + var attachmentsData interface{} + if len(expense.Attachments) > 0 { + jsonBytes, err := json.Marshal(expense.Attachments) + if err != nil { + return err + } + attachmentsData = string(jsonBytes) + } else { + attachmentsData = nil + } + _, err := DB.Exec(`INSERT INTO expenses(id, payer_id, amount_cents, title, description, attachments, created_at, last_updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + expense.ID, + expense.PayerID, + expense.Amount, + expense.Title, + expense.Description, + attachmentsData, + expense.CreatedAt, + expense.LastUpdatedAt) + return err +} +func UpdateExpense(expense *models.Expense) error { + return nil +} +func DeleteExpense(expense *models.Expense) error { + return nil +} + +// func GetExpenseById(id string) (models.Expense, error) { +// return nil, nil +// } +func GetExpensesByUserId(userId string) ([]models.Expense, error) { + return nil, nil +} +func GetAllExpenses() ([]models.Expense, error) { + return nil, nil +} + +// Expense Shares +func AddShare(share *models.ExpenseShare) error { + _, err := DB.Exec("INSERT INTO expense_shares(id, expense_id, user_id, share_cents) VALUES (?, ?, ?, ?)", + share.ID, + share.ExpenseID, + share.UserID, + share.ShareCents) return err } // Users -func AddUser(user models.User) error { +func AddUser(user *models.User) error { _, err := DB.Exec("INSERT INTO users(id, username, password, role) VALUES (?, ?, ?, ?)", user.ID, strings.ToLower(user.Username), user.Password, user.Role) return err } @@ -76,7 +152,7 @@ func GetUserById(id string) (models.User, error) { } // Refresh Tokens -func AddRefreshToken(token models.RefreshToken) error { +func AddRefreshToken(token *models.RefreshToken) error { _, err := DB.Exec("INSERT INTO refresh_tokens(id, user_id, token_hash, expires_at, created_at, revoked, device_info) VALUES (?, ?, ?, ?, ?, ?, ?)", token.ID, token.UserID, token.Token, token.ExpiresAt, token.CreatedAt, token.Revoked, token.DeviceInfo) return err diff --git a/utils/util.go b/utils/util.go index 026803c..1050e0e 100644 --- a/utils/util.go +++ b/utils/util.go @@ -5,6 +5,8 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "net/http" + "shap-planner-backend/auth" "github.com/google/uuid" ) @@ -31,3 +33,17 @@ func HashToken(token string) string { hash := sha256.Sum256([]byte(token)) return hex.EncodeToString(hash[:]) } +func IsLoggedIn(w http.ResponseWriter, r *http.Request) (*auth.Claims, bool) { + claimsRaw := r.Context().Value(auth.UserContextKey) + if claimsRaw == nil { + http.Error(w, "No claims in context", http.StatusUnauthorized) + return nil, false + } + + claims, ok := claimsRaw.(*auth.Claims) + if !ok { + http.Error(w, "Invalid claims", http.StatusUnauthorized) + return nil, false + } + return claims, true +}