-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Kervin Christianata
committed
Feb 26, 2022
0 parents
commit 2f86a45
Showing
125 changed files
with
16,019 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.envrc | ||
bin/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
include .envrc | ||
|
||
# ==================================================================================== # | ||
# HELPERS | ||
# ==================================================================================== # | ||
|
||
## help: print this help message | ||
.PHONY: help | ||
help: | ||
@echo 'Usage:' | ||
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' | ||
|
||
.PHONY: confirm | ||
confirm: | ||
@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans: -N} = y ] | ||
|
||
# ==================================================================================== # | ||
# DEVELOPMENT | ||
# ==================================================================================== # | ||
|
||
## run/api: run the cmd/api application | ||
.PHONY: run/api | ||
run/api: | ||
go run ./cmd/api -db-dsn=${DB_DSN} | ||
|
||
## db/psql: connect to the database using psql | ||
.PHONY: db/psql | ||
db/psql: | ||
psql ${DB_DSN} | ||
|
||
## db/migrations/new name=$1: create a new database migration | ||
.PHONY: db/migrations/new | ||
db/migrations/new: | ||
@echo 'Creating migration files for ${name}' | ||
migrate create -seq -ext=.sql -dir=./migrations ${name} | ||
|
||
## db/migrations/up: apply all up database migrations | ||
.PHONY: db/migrations/up | ||
db/migrations/up: confirm | ||
@echo 'Running up migrations...' | ||
migrate -path=./migrations -database ${DB_DSN} up | ||
|
||
# ==================================================================================== # | ||
# QUALITY CONTROL | ||
# ==================================================================================== # | ||
|
||
## audit: tidy dependencies and format, vet and test all code | ||
.PHONY: audit | ||
audit: vendor | ||
@echo 'Formatting code...' | ||
go fmt ./... | ||
@echo 'Vetting code...' | ||
go vet ./... | ||
staticcheck ./... | ||
@echo 'Running tests...' | ||
go test -race -vet=off ./... | ||
|
||
## vendor: tidy and vendor dependencies | ||
.PHONY: vendor | ||
vendor: | ||
@echo 'Tidying and verifying module dependencies...' | ||
go mod tidy | ||
go mod verify | ||
@echo 'Vendoring dependencies...' | ||
go mod vendor | ||
|
||
# ==================================================================================== # | ||
# BUILD | ||
# ==================================================================================== # | ||
|
||
current_time = $(shell date --iso-8601=seconds) | ||
git_description = $(shell git describe --always --dirty) | ||
linker_flags = '-s -X main.buildTime=${current_time} -X main.version=${git_description}' | ||
|
||
## build/api: build the cmd/api application | ||
.PHONY: build/api | ||
build/api: | ||
@echo 'Building cmd/api...' | ||
go build -ldflags=${linker_flags} -o=./bin/api ./cmd/api | ||
GOOS=linux GOARCH=amd64 go build -ldflags=${linker_flags} -o=./bin/linux_amd64/api ./cmd/api | ||
|
||
# migrate -path= ./migrations -database ${GREENLIGHT_DB_DSN} up | ||
# migrate -path= ./migrations -database "postgres://postgres@localhost:5432/greenlight?sslmode=disable" up |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# kin-api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
|
||
"github.com/kervinch/internal/data" | ||
) | ||
|
||
type contextKey string | ||
|
||
const userContextKey = contextKey("user") | ||
|
||
// The contextSetUser() method returns a new copy of the request with the provided | ||
// User struct added to the context. Note that we use our userContextKey constant as the | ||
// key. | ||
func (app *application) contextSetUser(r *http.Request, user *data.User) *http.Request { | ||
ctx := context.WithValue(r.Context(), userContextKey, user) | ||
return r.WithContext(ctx) | ||
} | ||
|
||
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") | ||
} | ||
|
||
return user | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
func (app *application) logError(r *http.Request, err error) { | ||
app.logger.PrintError(err, map[string]string{ | ||
"request_method": r.Method, | ||
"request_url": r.URL.String(), | ||
}) | ||
} | ||
|
||
func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) { | ||
err := app.writeJSON(w, status, http.StatusText(status), message, 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) invactiveAccountResponse(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) { | ||
messsage := "your user account doesn't have the necessary permissions to access this resource" | ||
app.errorResponse(w, r, http.StatusForbidden, messsage) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package main | ||
|
||
import ( | ||
"net/http" | ||
) | ||
|
||
// Declare a handler which writes a plain-text response with information about the // application status, operating environment and version. | ||
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, http.StatusText(http.StatusOK), env, nil) | ||
if err != nil { | ||
app.serverErrorResponse(w, r, err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/julienschmidt/httprouter" | ||
"github.com/kervinch/internal/validator" | ||
) | ||
|
||
type envelope map[string]interface{} | ||
|
||
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) writeJSON(w http.ResponseWriter, status int, message string, data interface{}, headers http.Header) error { | ||
result := make(map[string]interface{}) | ||
|
||
result["code"] = status | ||
result["message"] = message | ||
if status >= 400 { | ||
result["error"] = data | ||
} else { | ||
result["data"] = data | ||
} | ||
|
||
js, err := json.Marshal(result) | ||
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 interface{}) 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 | ||
|
||
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 err.Error() == "http: request body too large": | ||
return fmt.Errorf("body must not be larger than %d bytes", maxBytes) | ||
|
||
case errors.As(err, &invalidUnmarshalError): | ||
panic(err) | ||
|
||
default: | ||
return err | ||
} | ||
} | ||
|
||
err = dec.Decode(&struct{}{}) | ||
if err != io.EOF { | ||
return errors.New("body must only contain a single JSON value") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (app *application) readStrings(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() { | ||
app.wg.Done() | ||
|
||
defer func() { | ||
if err := recover(); err != nil { | ||
app.logger.PrintError(fmt.Errorf("%s", err), nil) | ||
} | ||
}() | ||
|
||
fn() | ||
}() | ||
} |
Oops, something went wrong.