From c4047a36cccd474ab3b668808bd2901b25f81214 Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Tue, 11 Apr 2023 22:41:39 -0400 Subject: [PATCH] new endpoints, budget adjustment --- API.md | 68 +++++++++++++++++++++-- main.go | 3 + routes/createAccount.go | 9 ++- routes/deleteAccount.go | 66 ++++++++++++++++++++++ routes/editAccount.go | 120 ++++++++++++++++++++++++++++++++++++++++ routes/login.go | 20 ++++--- widgets/budget.go | 4 ++ widgets/expenses.go | 2 +- widgets/transactions.go | 4 +- 9 files changed, 276 insertions(+), 20 deletions(-) create mode 100644 routes/deleteAccount.go create mode 100644 routes/editAccount.go diff --git a/API.md b/API.md index 69bd652..fc24c0b 100644 --- a/API.md +++ b/API.md @@ -1,4 +1,19 @@ +# General +## GET /userinfo +Headers: x-session-key + +Response: +```json +{ + "name": string, + "email": string, +} +``` + # Auth (/auth) router +Prepend all request under the auth router with '/auth' + +e.g. `/auth/login` ## POST /login Form: email (string), password (string) @@ -21,20 +36,48 @@ Response: "session": string, } ``` -## GET /userinfo -Headers: x-session-key +## POST /deleteaccount +Form: password (string) + +Requires x-session-key Response: ```json { - "name": string, - "email": string, + "status": int, +} +``` +## POST /changename +Form: name (string) + +Requires x-session-key + +Response: +```json +{ + "status": int, +} +``` +## POST /changepassword +Form: old (string), new (string) + +Requires x-session-key + +Response: +```json +{ + "status": int, } ``` # Widget (/w) router **IMPORTANT!** All requests for the widget router require the x-session-key header be set to the user's current session token. + + +Prepend all request under the auth router with '/auth' + +e.g. `/w/balance` ## GET /balance Return the current balance of the account @@ -116,22 +159,35 @@ Response: ```json { "status": int, + // total monthly budget "budget": { "currency": string, "whole": int, "decimal": int, }, + // budgets for each category "budget_categories": { - "category": { + "example_category": { "currency": string, "whole": int, "decimal": int, }, ... }, + // an array of all defined categories "categories": [ string ], + // month expense totals by category + "expenses_by_category": { + "example_category": { + "currency": string, + "whole": int, + "decimal": int + }, + ... + } + // list of all expenses by category "expenses": { - "category": [ + "example_category": [ { "timestamp": unix, "category": string, diff --git a/main.go b/main.go index 0b8081d..d0be6d3 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,9 @@ func main() { r.Post("/auth/login", routes.Login) r.Post("/auth/login/session", routes.Login) r.Post("/auth/createaccount", routes.CreateAccount) + r.Post("/auth/changepassword", routes.ChangePassword) + r.Post("/auth/changename", routes.ChangeName) + r.Post("/auth/deleteaccount", routes.DeleteAccount) r.Get("/userinfo", routes.UserInfo) r.Mount("/w", widgets.Router()) diff --git a/routes/createAccount.go b/routes/createAccount.go index 1dbed1f..bf1121c 100644 --- a/routes/createAccount.go +++ b/routes/createAccount.go @@ -34,12 +34,17 @@ func CreateAccount(w http.ResponseWriter, r *http.Request) { v.Name = r.FormValue("name") if v.Email == "" || v.Password == "" || v.Name == "" { w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, "{\"error\":\"field(s) are missing\"}") return } - if len(v.Password) < 8 || len(v.Password) > 255 || !EmailIsValid(v.Email) { + if len(v.Password) < 8 || len(v.Password) > 255 { w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, "{\"error\":\"there was a problem with your request. Please try again with different values\"}") + fmt.Fprint(w, "{\"error\":\"password must be 8 characters or greater\"}") + return + } else if !EmailIsValid(v.Email) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, "{\"error\":\"email is invalid\"}") return } diff --git a/routes/deleteAccount.go b/routes/deleteAccount.go new file mode 100644 index 0000000..e33bc33 --- /dev/null +++ b/routes/deleteAccount.go @@ -0,0 +1,66 @@ +package routes + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/readpref" + "golang.org/x/crypto/bcrypt" +) + +func DeleteAccount(w http.ResponseWriter, r *http.Request) { + log.Println("* /auth/deleteaccount") + // get session key from request + session := r.Header.Get("x-session-key") + // prepare DB + err := db.Client.Ping(context.Background(), readpref.Primary()) + if err != nil { + db.Connect() + } + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + // var v contains POST credentials + var v db.UserSchema + r.ParseForm() + v.Password = r.FormValue("password") + if v.Password == "" { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"password must be provided\"}") + return + } + + // cmp struct will be compared with v to verify credentials + var cmp db.UserSchema + found := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "session", Value: session}}) + if found.Err() != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"session key invalid\"}") + return + } + err = found.Decode(&cmp) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = bcrypt.CompareHashAndPassword([]byte(cmp.Password), []byte(v.Password)) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid password\"}") + return + } + + _, err = userCollection.DeleteOne(context.TODO(), bson.D{primitive.E{Key: "session", Value: session}}) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"unable to delete account\"}") + return + } + w.Write([]byte("{\"status\": 200}")) +} diff --git a/routes/editAccount.go b/routes/editAccount.go new file mode 100644 index 0000000..d7e259d --- /dev/null +++ b/routes/editAccount.go @@ -0,0 +1,120 @@ +package routes + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" + "golang.org/x/crypto/bcrypt" +) + +func ChangePassword(w http.ResponseWriter, r *http.Request) { + log.Println("* /auth/changepassword") + // get session key from request + session := r.Header.Get("x-session-key") + // prepare DB + err := db.Client.Ping(context.Background(), readpref.Primary()) + if err != nil { + db.Connect() + } + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + // var v contains POST credentials + r.ParseForm() + newpass := r.FormValue("new") + oldpass := r.FormValue("old") + if newpass == "" { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"password must be provided\"}") + return + } + + // cmp struct will be compared with v to verify credentials + var cmp db.UserSchema + found := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "session", Value: session}}) + if found.Err() != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + err = found.Decode(&cmp) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = bcrypt.CompareHashAndPassword([]byte(cmp.Password), []byte(oldpass)) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid password\"}") + return + } + + if len(newpass) < 8 || len(newpass) > 255 { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, "{\"error\":\"password must be 8 characters or greater\"}") + return + } + + // hash and store the user's hashed password + hashedPass, err := bcrypt.GenerateFromPassword([]byte(newpass), 8) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"internal server error, please try again later\"}") + } + + filter := bson.D{primitive.E{Key: "session", Value: session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "password", Value: string(hashedPass)}}}} + _, err = userCollection.UpdateOne(context.TODO(), filter, update, opts) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + w.Write([]byte("{\"status\": 200}")) + +} + +func ChangeName(w http.ResponseWriter, r *http.Request) { + log.Println("* /auth/changename") + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + r.ParseForm() + + if r.FormValue("name") == "" { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"name cannot be blank\"}") + return + } + + found := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "session", Value: session}}) + if found.Err() != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + filter := bson.D{primitive.E{Key: "session", Value: session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "name", Value: r.FormValue("name")}}}} + _, err := userCollection.UpdateOne(context.TODO(), filter, update, opts) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + w.Write([]byte("{\"status\": 200}")) +} diff --git a/routes/login.go b/routes/login.go index 8a10247..7ad3971 100644 --- a/routes/login.go +++ b/routes/login.go @@ -40,6 +40,7 @@ func Login(w http.ResponseWriter, r *http.Request) { v.Password = r.FormValue("password") if v.Email == "" || v.Password == "" { w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"both email and password must be provided\"}") return } @@ -75,15 +76,15 @@ func Login(w http.ResponseWriter, r *http.Request) { // set new session cookie for user, either persistent (remember me) or temporary sessionID := uuid.NewString() - var session = &http.Cookie{ - Name: "session", - Value: sessionID, - Path: "/", - Secure: true, - SameSite: http.SameSiteLaxMode, - MaxAge: 0, - } - http.SetCookie(w, session) + // var session = &http.Cookie{ + // Name: "session", + // Value: sessionID, + // Path: "/", + // Secure: true, + // SameSite: http.SameSiteLaxMode, + // MaxAge: 0, + // } + // http.SetCookie(w, session) // update the new user session in the DB filter := bson.D{primitive.E{Key: "email", Value: strings.ToLower(account.Email)}} @@ -92,6 +93,7 @@ func Login(w http.ResponseWriter, r *http.Request) { userCollection.UpdateOne(context.TODO(), filter, update, opts) account.Status = 200 + account.Session = sessionID acc, err := json.Marshal(account) if err != nil { fmt.Println("Error marshalling bson.D response") diff --git a/widgets/budget.go b/widgets/budget.go index 80bee58..a6cf19a 100644 --- a/widgets/budget.go +++ b/widgets/budget.go @@ -22,6 +22,8 @@ type BudgetResponse struct { BudgetCategories map[string]money.Money `json:"budget_categories"` // categories in the budget Categories []string `json:"categories"` + // total expenses by category + ExpensesByCategory map[string]money.Money `json:"expenses_by_category"` // transactions mapped to a category Expenses map[string][]db.Transaction `json:"expenses"` } @@ -53,11 +55,13 @@ func GetBudget(w http.ResponseWriter, r *http.Request) { } response.Categories = cats response.Expenses = make(map[string][]db.Transaction) + response.ExpensesByCategory = make(map[string]money.Money) for _, e := range user.Expenses { if response.Expenses[e.Category] == nil { response.Expenses[e.Category] = make([]db.Transaction, 0) } response.Expenses[e.Category] = append(response.Expenses[e.Category], e) + response.ExpensesByCategory[e.Category] = money.Add(e.Amount, response.ExpensesByCategory[e.Category]) } response.Status = 200 diff --git a/widgets/expenses.go b/widgets/expenses.go index 279fe2f..27dcc0c 100644 --- a/widgets/expenses.go +++ b/widgets/expenses.go @@ -41,7 +41,7 @@ func GetMonthExpenses(w http.ResponseWriter, r *http.Request) { if user.Expenses[i].Timestamp < time.Now().Add(-30*24*time.Hour).Unix() { break } - total = money.Add(total, user.Expenses[i].Amount) + total = money.Add(user.Expenses[i].Amount, total) } ret, err := json.Marshal(total) diff --git a/widgets/transactions.go b/widgets/transactions.go index 7095332..02b7846 100644 --- a/widgets/transactions.go +++ b/widgets/transactions.go @@ -153,7 +153,7 @@ func NewTransaction(w http.ResponseWriter, r *http.Request) { update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: r.FormValue("type"), Value: newArr}}}} userCollection.UpdateOne(context.TODO(), filter, update, opts) - w.Write([]byte("{\"status\": 200}")) + w.Write([]byte("{\"status\":200}")) } func NewRecurring(w http.ResponseWriter, r *http.Request) { @@ -243,6 +243,6 @@ func NewRecurring(w http.ResponseWriter, r *http.Request) { update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: r.FormValue("type"), Value: newArr}}}} userCollection.UpdateOne(context.TODO(), filter, update, opts) - w.Write([]byte("{\"status\": 200}")) + w.Write([]byte("{\"status\":200}")) }