initial commit

This commit is contained in:
basil 2025-06-14 23:47:44 -04:00
commit d40b69f1f9
Signed by: basil
SSH key fingerprint: SHA256:y04xIFL/yqNaG9ae9Vl95vELtHfApGAIoOGLeVLP/fE
58 changed files with 7919 additions and 0 deletions

204
backend/router/route.go Normal file
View 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
View 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)