new endpoints, budget adjustment

main
Gabe Farrell 3 years ago
parent cc36b80e72
commit c4047a36cc

@ -1,4 +1,19 @@
# General
## GET /userinfo
Headers: x-session-key
Response:
```json
{
"name": string,
"email": string,
}
```
# Auth (/auth) router # Auth (/auth) router
Prepend all request under the auth router with '/auth'
e.g. `/auth/login`
## POST /login ## POST /login
Form: email (string), password (string) Form: email (string), password (string)
@ -21,20 +36,48 @@ Response:
"session": string, "session": string,
} }
``` ```
## GET /userinfo ## POST /deleteaccount
Headers: x-session-key Form: password (string)
Requires x-session-key
Response: Response:
```json ```json
{ {
"name": string, "status": int,
"email": string, }
```
## 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 # Widget (/w) router
**IMPORTANT!** All requests for the widget router require the x-session-key header be set **IMPORTANT!** All requests for the widget router require the x-session-key header be set
to the user's current session token. to the user's current session token.
Prepend all request under the auth router with '/auth'
e.g. `/w/balance`
## GET /balance ## GET /balance
Return the current balance of the account Return the current balance of the account
@ -116,22 +159,35 @@ Response:
```json ```json
{ {
"status": int, "status": int,
// total monthly budget
"budget": { "budget": {
"currency": string, "currency": string,
"whole": int, "whole": int,
"decimal": int, "decimal": int,
}, },
// budgets for each category
"budget_categories": { "budget_categories": {
"category": { "example_category": {
"currency": string, "currency": string,
"whole": int, "whole": int,
"decimal": int, "decimal": int,
}, },
... ...
}, },
// an array of all defined categories
"categories": [ string ], "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": { "expenses": {
"category": [ "example_category": [
{ {
"timestamp": unix, "timestamp": unix,
"category": string, "category": string,

@ -41,6 +41,9 @@ func main() {
r.Post("/auth/login", routes.Login) r.Post("/auth/login", routes.Login)
r.Post("/auth/login/session", routes.Login) r.Post("/auth/login/session", routes.Login)
r.Post("/auth/createaccount", routes.CreateAccount) 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.Get("/userinfo", routes.UserInfo)
r.Mount("/w", widgets.Router()) r.Mount("/w", widgets.Router())

@ -34,12 +34,17 @@ func CreateAccount(w http.ResponseWriter, r *http.Request) {
v.Name = r.FormValue("name") v.Name = r.FormValue("name")
if v.Email == "" || v.Password == "" || v.Name == "" { if v.Email == "" || v.Password == "" || v.Name == "" {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "{\"error\":\"field(s) are missing\"}")
return 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) 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 return
} }

@ -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}"))
}

@ -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}"))
}

@ -40,6 +40,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
v.Password = r.FormValue("password") v.Password = r.FormValue("password")
if v.Email == "" || v.Password == "" { if v.Email == "" || v.Password == "" {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "{\"error\":\"both email and password must be provided\"}")
return 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 // set new session cookie for user, either persistent (remember me) or temporary
sessionID := uuid.NewString() sessionID := uuid.NewString()
var session = &http.Cookie{ // var session = &http.Cookie{
Name: "session", // Name: "session",
Value: sessionID, // Value: sessionID,
Path: "/", // Path: "/",
Secure: true, // Secure: true,
SameSite: http.SameSiteLaxMode, // SameSite: http.SameSiteLaxMode,
MaxAge: 0, // MaxAge: 0,
} // }
http.SetCookie(w, session) // http.SetCookie(w, session)
// update the new user session in the DB // update the new user session in the DB
filter := bson.D{primitive.E{Key: "email", Value: strings.ToLower(account.Email)}} 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) userCollection.UpdateOne(context.TODO(), filter, update, opts)
account.Status = 200 account.Status = 200
account.Session = sessionID
acc, err := json.Marshal(account) acc, err := json.Marshal(account)
if err != nil { if err != nil {
fmt.Println("Error marshalling bson.D response") fmt.Println("Error marshalling bson.D response")

@ -22,6 +22,8 @@ type BudgetResponse struct {
BudgetCategories map[string]money.Money `json:"budget_categories"` BudgetCategories map[string]money.Money `json:"budget_categories"`
// categories in the budget // categories in the budget
Categories []string `json:"categories"` Categories []string `json:"categories"`
// total expenses by category
ExpensesByCategory map[string]money.Money `json:"expenses_by_category"`
// transactions mapped to a category // transactions mapped to a category
Expenses map[string][]db.Transaction `json:"expenses"` Expenses map[string][]db.Transaction `json:"expenses"`
} }
@ -53,11 +55,13 @@ func GetBudget(w http.ResponseWriter, r *http.Request) {
} }
response.Categories = cats response.Categories = cats
response.Expenses = make(map[string][]db.Transaction) response.Expenses = make(map[string][]db.Transaction)
response.ExpensesByCategory = make(map[string]money.Money)
for _, e := range user.Expenses { for _, e := range user.Expenses {
if response.Expenses[e.Category] == nil { if response.Expenses[e.Category] == nil {
response.Expenses[e.Category] = make([]db.Transaction, 0) response.Expenses[e.Category] = make([]db.Transaction, 0)
} }
response.Expenses[e.Category] = append(response.Expenses[e.Category], e) 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 response.Status = 200

@ -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() { if user.Expenses[i].Timestamp < time.Now().Add(-30*24*time.Hour).Unix() {
break break
} }
total = money.Add(total, user.Expenses[i].Amount) total = money.Add(user.Expenses[i].Amount, total)
} }
ret, err := json.Marshal(total) ret, err := json.Marshal(total)

Loading…
Cancel
Save