Skip to content
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
14 changes: 8 additions & 6 deletions backend/cmd/api/context.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package main

import (
"github.com/gin-gonic/gin"
"context"
"net/http"

"github.com/philjestin/ranked-talishar/internal/data"
)

type contextKey string

const userContextKey = contextKey("user")

func ContextSetUser(ctx *gin.Context, user *data.User) {
ctx.Set(string(userContextKey), user)
func (app *application) contextSetUser(r *http.Request, user *data.User) *http.Request {
ctx := context.WithValue(r.Context(), userContextKey, user)
return r.WithContext(ctx)
}

func ContextGetUser(ctx *gin.Context) *data.User {
user, ok := ctx.Value(userContextKey).(*data.User)

func (app *application) contextGetUser(r *http.Request) *data.User {
user, ok := r.Context().Value(userContextKey).(*data.User)
if !ok {
panic("missing user value in request context")
}
Expand Down
87 changes: 87 additions & 0 deletions backend/cmd/api/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"fmt"
"net/http"
)

func (app *application) logError(r *http.Request, err error) {
var (
method = r.Method
uri = r.URL.RequestURI()
)

app.logger.Error(err.Error(), "method", method, "uri", uri)
}

func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message any) {
env := envelope{"error": message}

err := app.writeJSON(w, status, env, nil)
if err != nil {
app.logError(r, err)
w.WriteHeader(500)
}
}

func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
app.logError(r, err)

message := "the server encountered a problem and could not process your request"
app.errorResponse(w, r, http.StatusInternalServerError, message)
}

func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) {
message := "the requested resource could not be found"
app.errorResponse(w, r, http.StatusNotFound, message)
}

func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
message := fmt.Sprintf("the %s method is not supported for this resource", r.Method)
app.errorResponse(w, r, http.StatusMethodNotAllowed, message)
}

func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
app.errorResponse(w, r, http.StatusBadRequest, err.Error())
}

func (app *application) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
app.errorResponse(w, r, http.StatusUnprocessableEntity, errors)
}

func (app *application) editConflictResponse(w http.ResponseWriter, r *http.Request) {
message := "unable to update the record due to an edit conflict, please try again"
app.errorResponse(w, r, http.StatusConflict, message)
}

func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http.Request) {
message := "rate limit exceeded"
app.errorResponse(w, r, http.StatusTooManyRequests, message)
}

func (app *application) invalidCredentialsResponse(w http.ResponseWriter, r *http.Request) {
message := "invalid authentication credentials"
app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) invalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", "Bearer")

message := "invalid or missing authentication token"
app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) authenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
message := "you must be authenticated to access this resource"
app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) inactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
message := "your user account must be activated to access this resource"
app.errorResponse(w, r, http.StatusForbidden, message)
}

func (app *application) notPermittedResponse(w http.ResponseWriter, r *http.Request) {
message := "your user account doesn't have the necessary permissions to access this resource"
app.errorResponse(w, r, http.StatusForbidden, message)
}
20 changes: 20 additions & 0 deletions backend/cmd/api/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import (
"net/http"
)

func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
env := envelope{
"status": "available",
"system_info": map[string]string{
"environment": app.config.env,
"version": version,
},
}

err := app.writeJSON(w, http.StatusOK, env, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
165 changes: 165 additions & 0 deletions backend/cmd/api/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/google/uuid"
"github.com/philjestin/ranked-talishar/internal/validator"

"github.com/julienschmidt/httprouter"
)

func (app *application) readIDParam(r *http.Request) (int64, error) {
params := httprouter.ParamsFromContext(r.Context())

id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil || id < 1 {
return 0, errors.New("invalid id parameter")
}

return id, nil
}

func (app *application) readUuidParam(r *http.Request) uuid.UUID {
params := httprouter.ParamsFromContext(r.Context())

id := params.ByName("id")

uuidId := uuid.MustParse(id)

return uuidId
}

type envelope map[string]any

func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error {
js, err := json.MarshalIndent(data, "", "\t")
if err != nil {
return err
}

js = append(js, '\n')

for key, value := range headers {
w.Header()[key] = value
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(js)

return nil
}

func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst any) error {
maxBytes := 1_048_576
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))

dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()

err := dec.Decode(dst)
if err != nil {
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
var invalidUnmarshalError *json.InvalidUnmarshalError
var maxBytesError *http.MaxBytesError

switch {
case errors.As(err, &syntaxError):
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)

case errors.Is(err, io.ErrUnexpectedEOF):
return errors.New("body contains badly-formed JSON")

case errors.As(err, &unmarshalTypeError):
if unmarshalTypeError.Field != "" {
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
}
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)

case errors.Is(err, io.EOF):
return errors.New("body must not be empty")

case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return fmt.Errorf("body contains unknown key %s", fieldName)

case errors.As(err, &maxBytesError):
return fmt.Errorf("body must not be larger than %d bytes", maxBytesError.Limit)

case errors.As(err, &invalidUnmarshalError):
panic(err)

default:
return err
}
}

err = dec.Decode(&struct{}{})
if !errors.Is(err, io.EOF) {
return errors.New("body must only contain a single JSON value")
}

return nil
}

func (app *application) readString(qs url.Values, key string, defaultValue string) string {
s := qs.Get(key)

if s == "" {
return defaultValue
}

return s
}

func (app *application) readCSV(qs url.Values, key string, defaultValue []string) []string {
csv := qs.Get(key)

if csv == "" {
return defaultValue
}

return strings.Split(csv, ",")
}

func (app *application) readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
s := qs.Get(key)

if s == "" {
return defaultValue
}

i, err := strconv.Atoi(s)
if err != nil {
v.AddError(key, "must be an integer value")
return defaultValue
}

return i
}

func (app *application) background(fn func()) {
app.wg.Add(1)

go func() {

defer app.wg.Done()

defer func() {
if err := recover(); err != nil {
app.logger.Error(fmt.Sprintf("%v", err))
}
}()

fn()
}()
}
41 changes: 41 additions & 0 deletions backend/cmd/api/heroes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"errors"
"net/http"

"github.com/philjestin/ranked-talishar/internal/data"
)

func (app *application) showAllHeroesHandler(w http.ResponseWriter, r *http.Request) {
heroes, err := app.models.Heroes.GetAllHeroes()
if err != nil {
app.serverErrorResponse(w, r, err)
return
}

err = app.writeJSON(w, http.StatusOK, envelope{"heroes": heroes}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}

func (app *application) showHeroHandler(w http.ResponseWriter, r *http.Request) {
id := app.readUuidParam(r)

hero, err := app.models.Heroes.GetHeroById(id)
if err != nil {
switch {
case errors.Is(err, data.ErrRecordNotFound):
app.notFoundResponse(w, r)
default:
app.serverErrorResponse(w, r, err)
}
return
}

err = app.writeJSON(w, http.StatusOK, envelope{"hero": hero}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
Loading