Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reworked middleware to add token route and validate users #25

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export DB_NAME=knoldus
# Example: sRqXKbHFdQ8Wka6NWvk3ZaZhY3JD3CnUw7H5B42gETZS8feYUFUJ8k2jRvnNQrLwEyPWnwfuGN9XG3GQaPxC6HdtNNYmbZW6MEN32BfSpsDzdVtQJXuNHfmUPsRw4LDdyRxs4PVFHAmGfnF3BWPZgJpnMst28QCDbKpWfbM8G8vmPQD9fRh6KxsZqsGz3QpCmwekHPnuX3n9n9KHp8emReeLq7EKnSsmhCcVeQNB2psgSUkVYpgeaXBDHZ8TVjmq
export SECRET_KEY='your-256-bit-secret'

export PORT=8081
export PORT=8082
export BASE_URL=localhost
export ENV=local
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.22

- name: Build
run: go build -v ./...
Expand All @@ -30,7 +30,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.22

- name: Test and Coverage
run: go test ./... -coverprofile=coverage.txt -covermode=atomic
Expand Down
16 changes: 16 additions & 0 deletions api/handlers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type App struct {

// New creates a new mux router and all the routes
func (a *App) New() *mux.Router {
// setup go-guardian for middleware
m := api.MiddlewareDB{DB: databases.NewUserDatabase(a.dbHelper)}
m.SetupGoGuardian()

r := mux.NewRouter()

u := User{DB: databases.NewUserDatabase(a.dbHelper)}
Expand All @@ -37,17 +41,26 @@ func (a *App) New() *mux.Router {
ev := EmsVehicle{DB: databases.NewEmsVehicleDatabase(a.dbHelper)}
w := Warrant{DB: databases.NewWarrantDatabase(a.dbHelper)}
call := Call{DB: databases.NewCallDatabase(a.dbHelper)}
s := Spotlight{DB: databases.NewSpotlightDatabase(a.dbHelper)}

// healthchex
r.HandleFunc("/health", healthCheckHandler)

apiCreate := r.PathPrefix("/api/v1").Subrouter()

apiCreate.Handle("/auth/token", api.Middleware(http.HandlerFunc(api.CreateToken))).Methods("POST")
apiCreate.Handle("/auth/logout", api.Middleware(http.HandlerFunc(api.RevokeToken))).Methods("DELETE")

apiCreate.Handle("/community/{community_id}", api.Middleware(http.HandlerFunc(c.CommunityHandler))).Methods("GET")
apiCreate.Handle("/community/{community_id}/{owner_id}", api.Middleware(http.HandlerFunc(c.CommunityByCommunityAndOwnerIDHandler))).Methods("GET")
apiCreate.Handle("/communities/{owner_id}", api.Middleware(http.HandlerFunc(c.CommunitiesByOwnerIDHandler))).Methods("GET")

apiCreate.Handle("/user/{user_id}", api.Middleware(http.HandlerFunc(u.UserHandler))).Methods("GET")
apiCreate.Handle("/user/create-user", http.HandlerFunc(u.UserCreateHandler)).Methods("POST")
apiCreate.Handle("/user/check-user", http.HandlerFunc(u.UserCheckEmailHandler)).Methods("POST")
apiCreate.Handle("/users/discover-people", api.Middleware(http.HandlerFunc(u.UsersDiscoverPeopleHandler))).Methods("GET")
apiCreate.Handle("/users/{active_community_id}", api.Middleware(http.HandlerFunc(u.UsersFindAllHandler))).Methods("GET")

apiCreate.Handle("/civilian/{civilian_id}", api.Middleware(http.HandlerFunc(civ.CivilianByIDHandler))).Methods("GET")
apiCreate.Handle("/civilians", api.Middleware(http.HandlerFunc(civ.CivilianHandler))).Methods("GET")
apiCreate.Handle("/civilians/user/{user_id}", api.Middleware(http.HandlerFunc(civ.CiviliansByUserIDHandler))).Methods("GET")
Expand Down Expand Up @@ -80,6 +93,9 @@ func (a *App) New() *mux.Router {
apiCreate.Handle("/calls", api.Middleware(http.HandlerFunc(call.CallHandler))).Methods("GET")
apiCreate.Handle("/calls/community/{community_id}", api.Middleware(http.HandlerFunc(call.CallsByCommunityIDHandler))).Methods("GET")

apiCreate.Handle("/spotlight", api.Middleware(http.HandlerFunc(s.SpotlightHandler))).Methods("GET")
apiCreate.Handle("/spotlight", api.Middleware(http.HandlerFunc(s.SpotlightCreateHandler))).Methods("POST")

// swagger docs hosted at "/"
r.PathPrefix("/").Handler(http.StripPrefix("/", http.FileServer(http.Dir("./docs/"))))
return r
Expand Down
6 changes: 3 additions & 3 deletions api/handlers/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ func TestApp_CommunityHandlerInvalidToken(t *testing.T) {
req.Header.Add("Authorization", "Bearer asdfasdf")
response := executeRequest(req)

checkResponseCode(t, http.StatusInternalServerError, response.Code)
checkResponseCode(t, http.StatusUnauthorized, response.Code)

var m map[string]string
json.Unmarshal(response.Body.Bytes(), &m)
if m["error"] != "failed to parse token, token contains an invalid number of segments" {
t.Errorf("Expected the 'error' key of the reponse to be set to 'failed to parse token, token contains an invalid number of segments'. Got '%s'", m["error"])
if m["error"] != "unauthorized" {
t.Errorf("Expected the 'error' key of the reponse to be set to 'unauthorized'. Got '%s'", m["error"])
}
}

Expand Down
100 changes: 100 additions & 0 deletions api/handlers/spotlight.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package handlers

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"

"github.com/gorilla/mux"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"

"github.com/linesmerrill/police-cad-api/config"
"github.com/linesmerrill/police-cad-api/databases"
"github.com/linesmerrill/police-cad-api/models"
)

// Spotlight exported for testing purposes
type Spotlight struct {
DB databases.SpotlightDatabase
}

// SpotlightHandler returns all spotlights
func (s Spotlight) SpotlightHandler(w http.ResponseWriter, r *http.Request) {
Limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
zap.S().Warnf(fmt.Sprintf("limit not set, using default of %v, err: %v", Limit|10, err))
}
limit64 := int64(Limit)
Page = getPage(Page, r)
skip64 := int64(Page * Limit)
dbResp, err := s.DB.Find(context.TODO(), bson.D{}, &options.FindOptions{Limit: &limit64, Skip: &skip64})
if err != nil {
config.ErrorStatus("failed to get spotlight", http.StatusNotFound, w, err)
return
}
// Because the frontend requires that the data elements inside models.Spotlight exist, if
// len == 0 then we will just return an empty data object
if len(dbResp) == 0 {
dbResp = []models.Spotlight{}
}
b, err := json.Marshal(dbResp)
if err != nil {
config.ErrorStatus("failed to marshal response", http.StatusInternalServerError, w, err)
return
}
w.WriteHeader(http.StatusOK)
w.Write(b)
}

// SpotlightByIDHandler returns a spotlight by ID
func (s Spotlight) SpotlightByIDHandler(w http.ResponseWriter, r *http.Request) {
spotlightID := mux.Vars(r)["spotlight_id"]

zap.S().Debugf("spotlight_id: %v", spotlightID)

cID, err := primitive.ObjectIDFromHex(spotlightID)
if err != nil {
config.ErrorStatus("failed to get objectID from Hex", http.StatusBadRequest, w, err)
return
}

dbResp, err := s.DB.FindOne(context.Background(), bson.M{"_id": cID})
if err != nil {
config.ErrorStatus("failed to get spotlight by ID", http.StatusNotFound, w, err)
return
}

b, err := json.Marshal(dbResp)
if err != nil {
config.ErrorStatus("failed to marshal response", http.StatusInternalServerError, w, err)
return
}
w.WriteHeader(http.StatusOK)
w.Write(b)
}

// SpotlightCreateHandler creates a spotlight
func (s Spotlight) SpotlightCreateHandler(w http.ResponseWriter, r *http.Request) {
var spotlightDetails models.SpotlightDetails
err := json.NewDecoder(r.Body).Decode(&spotlightDetails)
if err != nil {
config.ErrorStatus("failed to decode request", http.StatusBadRequest, w, err)
return
}

h := s.DB.InsertOne(context.Background(), spotlightDetails)
zap.S().Debugf("inserted spotlight: %v", h)

b, err := json.Marshal(spotlightDetails)
if err != nil {
config.ErrorStatus("failed to marshal response", http.StatusInternalServerError, w, err)
return
}
w.WriteHeader(http.StatusCreated)
w.Write(b)
}
1 change: 1 addition & 0 deletions api/handlers/spotlight_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package handlers
138 changes: 134 additions & 4 deletions api/handlers/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ package handlers

import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"fmt"
"net/http"

"github.com/gorilla/mux"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"

"github.com/linesmerrill/police-cad-api/config"
"github.com/linesmerrill/police-cad-api/databases"
"github.com/linesmerrill/police-cad-api/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)

// User exported for testing purposes
type User struct {
DB databases.UserDatabase
}
Expand Down Expand Up @@ -70,3 +74,129 @@ func (u User) UsersFindAllHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(b)
}

// UserLoginHandler returns a session token for a user
func (u User) UserLoginHandler(w http.ResponseWriter, r *http.Request) {
email, password, ok := r.BasicAuth()
if ok {
usernameHash := sha256.Sum256([]byte(email))

// fetch email & pass from db
dbEmailResp, err := u.DB.Find(context.Background(), bson.M{"user.email": email})
if err != nil {
config.ErrorStatus("failed to get user by ID", http.StatusNotFound, w, err)
return
}
if len(dbEmailResp) == 0 {
config.ErrorStatus("no matching email found", http.StatusUnauthorized, w, fmt.Errorf("no matching email found"))
return
}

expectedUsernameHash := sha256.Sum256([]byte(dbEmailResp[0].Details.Email))
usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1

err = bcrypt.CompareHashAndPassword([]byte(dbEmailResp[0].Details.Password), []byte(password))
if err != nil {
config.ErrorStatus("failed to compare password", http.StatusUnauthorized, w, err)
return
}

if usernameMatch {
w.WriteHeader(http.StatusOK)
return
}
}

w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)

}

// UserCreateHandler creates a user
func (u User) UserCreateHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var user models.UserDetails
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
config.ErrorStatus("failed to decode request", http.StatusBadRequest, w, err)
return
}

// check if the user already exists
existingUser, _ := u.DB.FindOne(context.Background(), bson.M{"email": user.Email})
if existingUser != nil {
config.ErrorStatus("email already exists", http.StatusConflict, w, fmt.Errorf("duplicate email"))
return
}

// hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
config.ErrorStatus("failed to hash password", http.StatusInternalServerError, w, err)
return
}
user.Password = string(hashedPassword)

// insert the user
_ = u.DB.InsertOne(context.Background(), user)
if err != nil {
config.ErrorStatus("failed to insert user", http.StatusInternalServerError, w, err)
return
}

w.WriteHeader(http.StatusCreated)

}

// UserCheckEmailHandler checks if an email exists using POST
func (u User) UserCheckEmailHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var user models.UserDetails
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
config.ErrorStatus("failed to decode request", http.StatusBadRequest, w, err)
return
}

// check if the user already exists
existingUser, _ := u.DB.FindOne(context.Background(), bson.M{"email": user.Email})
if existingUser != nil {
config.ErrorStatus("email already exists", http.StatusConflict, w, fmt.Errorf("duplicate email"))
return
}

w.WriteHeader(http.StatusOK)
}

// UsersDiscoverPeopleHandler returns a list of users that we suggest to the user to follow
func (u User) UsersDiscoverPeopleHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
email := r.URL.Query().Get("email")
if email == "" {
config.ErrorStatus("query param email is required", http.StatusBadRequest, w, fmt.Errorf("query param email is required"))
return
}

pipeline := []bson.M{
{"$match": bson.M{"user.email": bson.M{"$ne": email}}},
{"$sample": bson.M{"size": 4}},
}

dbResp, err := u.DB.Aggregate(context.Background(), pipeline)
if err != nil {
config.ErrorStatus("failed to get discover people recommendations", http.StatusInternalServerError, w, err)
return
}
// Because the frontend requires that the data elements inside models.User exist, if
// len == 0 then we will just return an empty data object
if len(dbResp) == 0 {
dbResp = []models.User{}
}
b, err := json.Marshal(dbResp)
if err != nil {
config.ErrorStatus("failed to marshal response", http.StatusInternalServerError, w, err)
return
}
w.WriteHeader(http.StatusOK)
w.Write(b)
}
Loading
Loading