initial commit
This commit is contained in:
commit
d40b69f1f9
58 changed files with 7919 additions and 0 deletions
204
backend/router/route.go
Normal file
204
backend/router/route.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.red-panda.pet/pandaware/house/backend/db"
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
type IRequestContext interface {
|
||||
Search(key string) string
|
||||
Parameter(key string) string
|
||||
Cookie(name string) (*http.Cookie, error)
|
||||
SetCookie(cookie *http.Cookie)
|
||||
JSON(statusCode int, body any) error
|
||||
Redirect(statusCode int, url string) error
|
||||
Text(statusCode int, body string) error
|
||||
Bytes(statusCode int, body []byte) error
|
||||
With(key any, value any)
|
||||
}
|
||||
|
||||
type RequestError struct {
|
||||
Inner error `json:"-"`
|
||||
RequestID string `json:"requestID"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
func (r *RequestError) Error() string {
|
||||
return r.Inner.Error()
|
||||
}
|
||||
|
||||
var _ error = new(RequestError)
|
||||
|
||||
type Context struct {
|
||||
id string
|
||||
statusCode int
|
||||
start time.Time
|
||||
log *log.Logger
|
||||
context context.Context
|
||||
search url.Values
|
||||
|
||||
resp http.ResponseWriter
|
||||
Request *http.Request
|
||||
DB *sql.Conn
|
||||
Query *db.Queries
|
||||
}
|
||||
|
||||
// Redirect implements IRequestContext.
|
||||
func (r *Context) Redirect(statusCode int, url string) error {
|
||||
http.Redirect(r, r.Request, url, statusCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// With implements IRequestContext.
|
||||
func (r *Context) With(key, value any) {
|
||||
r.context = context.WithValue(r.context, key, value)
|
||||
}
|
||||
|
||||
// Bytes implements IRequestContext.
|
||||
func (r *Context) Bytes(statusCode int, body []byte) error {
|
||||
r.WriteHeader(statusCode)
|
||||
_, err := r.Write(body)
|
||||
return err
|
||||
}
|
||||
|
||||
// Text implements IRequestContext.
|
||||
func (r *Context) Text(statusCode int, body string) error {
|
||||
r.Header().Set("Content-Type", "text/plain")
|
||||
return r.Bytes(statusCode, []byte(body))
|
||||
}
|
||||
|
||||
// JSON implements IRequestContext.
|
||||
func (r *Context) JSON(statusCode int, body any) error {
|
||||
enc := json.NewEncoder(r)
|
||||
r.Header().Set("Content-Type", "application/json")
|
||||
r.WriteHeader(statusCode)
|
||||
return enc.Encode(body)
|
||||
}
|
||||
|
||||
func (r *Context) Error(err error, statusCode int, message string) error {
|
||||
return &RequestError{
|
||||
Inner: err,
|
||||
RequestID: r.id,
|
||||
StatusCode: statusCode,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
func (r *Context) GenericError(err error, statusCode int) error {
|
||||
message := "Error"
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
message = "Bad request"
|
||||
case http.StatusUnauthorized:
|
||||
message = "Unauthorized"
|
||||
case http.StatusNotFound:
|
||||
message = "Not found"
|
||||
case http.StatusInternalServerError:
|
||||
message = "Internal server error"
|
||||
}
|
||||
|
||||
return r.Error(err, statusCode, message)
|
||||
}
|
||||
|
||||
// Cookie implements IRequestContext.
|
||||
func (r *Context) Cookie(name string) (*http.Cookie, error) {
|
||||
return r.Request.Cookie(name)
|
||||
}
|
||||
|
||||
// Parameter implements IRequestContext.
|
||||
func (r *Context) Parameter(key string) string {
|
||||
return r.Request.PathValue(key)
|
||||
}
|
||||
|
||||
// Search implements IRequestContext.
|
||||
func (r *Context) Search(key string) string {
|
||||
if r.search == nil {
|
||||
r.search = r.Request.URL.Query()
|
||||
}
|
||||
return r.search.Get(key)
|
||||
}
|
||||
|
||||
// SetCookie implements IRequestContext.
|
||||
func (r *Context) SetCookie(cookie *http.Cookie) {
|
||||
http.SetCookie(r, cookie)
|
||||
}
|
||||
|
||||
// Deadline implements context.Context.
|
||||
func (r *Context) Deadline() (deadline time.Time, ok bool) {
|
||||
return r.context.Deadline()
|
||||
}
|
||||
|
||||
// Done implements context.Context.
|
||||
func (r *Context) Done() <-chan struct{} {
|
||||
return r.context.Done()
|
||||
}
|
||||
|
||||
// Err implements context.Context.
|
||||
func (r *Context) Err() error {
|
||||
return r.context.Err()
|
||||
}
|
||||
|
||||
// Value implements context.Context.
|
||||
func (r *Context) Value(key any) any {
|
||||
return r.context.Value(key)
|
||||
}
|
||||
|
||||
// Header implements http.ResponseWriter.
|
||||
func (r *Context) Header() http.Header {
|
||||
return r.resp.Header()
|
||||
}
|
||||
|
||||
// Write implements http.ResponseWriter.
|
||||
func (r *Context) Write(bs []byte) (int, error) {
|
||||
if r.statusCode == 0 {
|
||||
r.statusCode = 200
|
||||
}
|
||||
|
||||
r.log.Helper()
|
||||
r.log.Info("",
|
||||
"id", r.id,
|
||||
"duration", time.Since(r.start),
|
||||
"status", r.statusCode,
|
||||
)
|
||||
|
||||
return r.resp.Write(bs)
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter.
|
||||
func (r *Context) WriteHeader(statusCode int) {
|
||||
r.statusCode = statusCode
|
||||
r.resp.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
var _ context.Context = new(Context)
|
||||
var _ http.ResponseWriter = new(Context)
|
||||
var _ IRequestContext = new(Context)
|
||||
|
||||
type Route interface {
|
||||
Handle(ctx *Context) error
|
||||
}
|
||||
|
||||
type AuthorizedRoute interface {
|
||||
Authorize(ctx *Context) error
|
||||
}
|
||||
|
||||
type ValidatedSearchRoute interface {
|
||||
ValidateSearch(ctx *Context) error
|
||||
}
|
||||
|
||||
type ValidatedBodyRoute interface {
|
||||
ValidateBody(ctx *Context) error
|
||||
}
|
||||
|
||||
type ValidatedParamRoute interface {
|
||||
ValidateParams(ctx *Context) error
|
||||
}
|
||||
203
backend/router/router.go
Normal file
203
backend/router/router.go
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.red-panda.pet/pandaware/house/backend/db"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IRouter interface {
|
||||
Register(method, pattern string, route Route)
|
||||
GET(pattern string, route Route)
|
||||
POST(pattern string, route Route)
|
||||
HEAD(pattern string, route Route)
|
||||
PUT(pattern string, route Route)
|
||||
PATCH(pattern string, route Route)
|
||||
DELETE(pattern string, route Route)
|
||||
OPTIONS(pattern string, route Route)
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
log *log.Logger
|
||||
mux *http.ServeMux
|
||||
db *sql.DB
|
||||
prefix string
|
||||
}
|
||||
|
||||
func NewRouter(logger *log.Logger, db *sql.DB) *Router {
|
||||
r := new(Router)
|
||||
r.mux = http.NewServeMux()
|
||||
r.log = logger
|
||||
r.db = db
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (router *Router) SetPrefix(prefix string) {
|
||||
router.prefix = prefix
|
||||
}
|
||||
|
||||
func (router *Router) NewContext(w http.ResponseWriter, r *http.Request) (*Context, error) {
|
||||
router.log.Helper()
|
||||
|
||||
ctx := new(Context)
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := router.db.Conn(r.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := r.Method
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
path := r.URL.EscapedPath()
|
||||
|
||||
ctx.id = id.String()
|
||||
ctx.start = time.Now()
|
||||
ctx.resp = w
|
||||
ctx.Request = r
|
||||
ctx.log = router.log.WithPrefix(method + " " + path)
|
||||
ctx.DB = conn
|
||||
ctx.Query = db.New(ctx.DB)
|
||||
ctx.context = r.Context()
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (router *Router) handleError(ctx *Context, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if reqErr, ok := err.(*RequestError); ok {
|
||||
err = ctx.JSON(reqErr.StatusCode, reqErr)
|
||||
ctx.log.Warn("error during request", "err", reqErr.Inner)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// All implements IRouter.
|
||||
func (router *Router) Register(method, path string, route Route) {
|
||||
router.log.Helper()
|
||||
|
||||
routeImpl := any(route)
|
||||
|
||||
authorize, implAuthorized := routeImpl.(AuthorizedRoute)
|
||||
validateBody, implBodyValidation := routeImpl.(ValidatedBodyRoute)
|
||||
validateParams, implParamValidation := routeImpl.(ValidatedParamRoute)
|
||||
validateSearch, implSearchValidation := routeImpl.(ValidatedSearchRoute)
|
||||
|
||||
pattern := fmt.Sprintf("%s %s%s", method, router.prefix, path)
|
||||
|
||||
router.mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, err := router.NewContext(w, r)
|
||||
//TODO: don't panic :3
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if implAuthorized {
|
||||
err = authorize.Authorize(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
router.handleError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if implSearchValidation {
|
||||
err = validateSearch.ValidateSearch(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
router.handleError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if implParamValidation {
|
||||
err = validateParams.ValidateParams(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
router.handleError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if implBodyValidation {
|
||||
err = validateBody.ValidateBody(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
router.handleError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = route.Handle(ctx)
|
||||
router.handleError(ctx, err)
|
||||
})
|
||||
}
|
||||
|
||||
// GET implements IRouter.
|
||||
func (router *Router) GET(pattern string, route Route) {
|
||||
router.Register("GET ", pattern, route)
|
||||
}
|
||||
|
||||
// DELETE implements IRouter.
|
||||
func (router *Router) DELETE(pattern string, route Route) {
|
||||
router.Register("DELETE", pattern, route)
|
||||
}
|
||||
|
||||
// HEAD implements IRouter.
|
||||
func (router *Router) HEAD(pattern string, route Route) {
|
||||
router.Register("HEAD", pattern, route)
|
||||
}
|
||||
|
||||
// OPTIONS implements IRouter.
|
||||
func (router *Router) OPTIONS(pattern string, route Route) {
|
||||
router.Register("OPTIONS", pattern, route)
|
||||
}
|
||||
|
||||
// POST implements IRouter.
|
||||
func (router *Router) POST(pattern string, route Route) {
|
||||
router.Register("POST", pattern, route)
|
||||
}
|
||||
|
||||
// PUT implements IRouter.
|
||||
func (router *Router) PUT(pattern string, route Route) {
|
||||
router.Register("PUT", pattern, route)
|
||||
}
|
||||
|
||||
// PATCH implements IRouter.
|
||||
func (router *Router) PATCH(pattern string, route Route) {
|
||||
router.Register("PATCH", pattern, route)
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
router.log.Helper()
|
||||
router.log.Warn("recovered from panic", "err", rec)
|
||||
}
|
||||
}()
|
||||
|
||||
router.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
var _ http.Handler = new(Router)
|
||||
var _ IRouter = new(Router)
|
||||
Loading…
Add table
Add a link
Reference in a new issue