mirror of
https://github.com/gabehf/BudgetBuddy.git
synced 2026-03-07 21:48:14 -08:00
new endpoints, budget adjustment
This commit is contained in:
parent
cc36b80e72
commit
c4047a36cc
9 changed files with 276 additions and 20 deletions
68
API.md
68
API.md
|
|
@ -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,
|
||||||
|
|
|
||||||
3
main.go
3
main.go
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
66
routes/deleteAccount.go
Normal file
66
routes/deleteAccount.go
Normal file
|
|
@ -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}"))
|
||||||
|
}
|
||||||
120
routes/editAccount.go
Normal file
120
routes/editAccount.go
Normal file
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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}}}}
|
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)
|
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) {
|
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}}}}
|
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)
|
userCollection.UpdateOne(context.TODO(), filter, update, opts)
|
||||||
|
|
||||||
w.Write([]byte("{\"status\": 200}"))
|
w.Write([]byte("{\"status\":200}"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue