Initial commit
This commit is contained in:
12
pkg/router/context.go
Normal file
12
pkg/router/context.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"addrss/pkg/auth"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Response Response
|
||||
Claims auth.AccessClaims
|
||||
Request Request
|
||||
route Route
|
||||
}
|
||||
11
pkg/router/controller.go
Normal file
11
pkg/router/controller.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
type Controller interface {
|
||||
AddRoutes()
|
||||
}
|
||||
|
||||
func AddControllers(controllers ...Controller) {
|
||||
for _, controller := range controllers {
|
||||
controller.AddRoutes()
|
||||
}
|
||||
}
|
||||
76
pkg/router/request.go
Normal file
76
pkg/router/request.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"addrss/pkg/config"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
*http.Request
|
||||
params map[string]any
|
||||
}
|
||||
|
||||
func (req Request) Param(key string) (any, error) {
|
||||
value, ok := req.params[key]
|
||||
if ok {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("param %s not found", key)
|
||||
}
|
||||
|
||||
func (req Request) ParamInt64(key string) (int64, error) {
|
||||
param, err := req.Param(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(param.(string))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int64(i), nil
|
||||
}
|
||||
|
||||
// Bind Binds the request body to the struct pointer v using the Content-Type header to select a binder. This function panics if v cannot be bound.
|
||||
func (req Request) Bind(v any) error {
|
||||
mime := req.Header.Get("Content-Type")
|
||||
mime = strings.Split(mime, ";")[0]
|
||||
|
||||
switch mime {
|
||||
case "application/json":
|
||||
err := json.NewDecoder(req.Body).Decode(&v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while decoding http request body as json: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("no binder exists for type %s", mime)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
|
||||
maxMegs, err := config.GetInt64("uploads.maxMegabytes")
|
||||
if err != nil {
|
||||
maxMegs = 8000000
|
||||
}
|
||||
|
||||
if err := req.ParseMultipartForm(maxMegs << 20); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
file, header, err := req.Request.FormFile(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return file, header, nil
|
||||
}
|
||||
99
pkg/router/response.go
Normal file
99
pkg/router/response.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (resp Response) View(name string, data any) {
|
||||
fp := filepath.Join("internal/views", fmt.Sprintf("%s.html", name))
|
||||
tmpl, _ := template.ParseFiles(fp)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tmpl.ExecuteTemplate(buf, "index", data); err != nil {
|
||||
resp.statusCode(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp.html(buf.Bytes(), http.StatusOK)
|
||||
}
|
||||
|
||||
func (resp Response) OK(data any) {
|
||||
resp.json(data, http.StatusOK)
|
||||
}
|
||||
|
||||
func (resp Response) NoContent() {
|
||||
resp.statusCode(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (resp Response) BadRequest(err error) {
|
||||
resp.error(err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (resp Response) Unauthorized(err error) {
|
||||
resp.error(err, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (resp Response) Forbidden(err error) {
|
||||
resp.error(err, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func (resp Response) NotFound(err error) {
|
||||
resp.error(err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (resp Response) Conflict(err error) {
|
||||
resp.error(err, http.StatusConflict)
|
||||
}
|
||||
|
||||
func (resp Response) UnsupportedMediaType(err error) {
|
||||
resp.error(err, http.StatusUnsupportedMediaType)
|
||||
}
|
||||
|
||||
func (resp Response) InternalServerError(err error) {
|
||||
resp.error(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (resp Response) error(err error, status int) {
|
||||
er := struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: err.Error(),
|
||||
}
|
||||
resp.json(er, status)
|
||||
}
|
||||
|
||||
func (resp Response) html(content []byte, status int) {
|
||||
resp.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
resp.WriteHeader(status)
|
||||
|
||||
if _, err := resp.Write(content); err != nil {
|
||||
panic("Failed to write HTTP response: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (resp Response) json(data any, statusCode int) {
|
||||
content, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
panic("Failed to marshal JSON: " + err.Error())
|
||||
}
|
||||
|
||||
resp.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||
resp.WriteHeader(statusCode)
|
||||
|
||||
if _, err := resp.Write(content); err != nil {
|
||||
panic("Failed to write HTTP response: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (resp Response) statusCode(code int) {
|
||||
resp.WriteHeader(code)
|
||||
}
|
||||
33
pkg/router/route.go
Normal file
33
pkg/router/route.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package router
|
||||
|
||||
type Route struct {
|
||||
handler func(*Context)
|
||||
method string
|
||||
anonymous bool
|
||||
scope string
|
||||
params map[string]any
|
||||
path string
|
||||
bindModel any
|
||||
}
|
||||
|
||||
func (r Route) Anonymous() Route {
|
||||
for i, route := range routeMap[r.method] {
|
||||
if route.path == r.path {
|
||||
routeMap[r.method][i].scope = ""
|
||||
routeMap[r.method][i].anonymous = true
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Route) RequireScope(scope string) Route {
|
||||
for i, route := range routeMap[r.method] {
|
||||
if route.path == r.path {
|
||||
routeMap[r.method][i].scope = scope
|
||||
routeMap[r.method][i].anonymous = false
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
192
pkg/router/router.go
Normal file
192
pkg/router/router.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"addrss/pkg/auth"
|
||||
"addrss/pkg/config"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var routeMap = map[string][]Route{}
|
||||
|
||||
type router struct{}
|
||||
|
||||
func Serve(addr string) error {
|
||||
if len(routeMap) == 0 {
|
||||
return errors.New("no registered routes")
|
||||
}
|
||||
|
||||
return http.ListenAndServe(addr, router{})
|
||||
}
|
||||
|
||||
func (r router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
route, ok := getRoute(req)
|
||||
if !ok {
|
||||
// See if the request is for a file that exists in the content directory
|
||||
file := filepath.Join("content", req.URL.Path)
|
||||
if info, err := os.Stat(file); err == nil && !info.IsDir() {
|
||||
http.ServeFile(w, req, file)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := Context{
|
||||
Request: Request{req, map[string]any{}},
|
||||
Response: Response{w},
|
||||
Claims: auth.AccessClaims{},
|
||||
route: route,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
err := fmt.Errorf("recovered from panic: %v", rec)
|
||||
ctx.Response.InternalServerError(err)
|
||||
}
|
||||
}()
|
||||
|
||||
originHeader := req.Header.Get("Origin")
|
||||
if originHeader != "" {
|
||||
o, err := config.Get("cors.origins")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
origins := o.([]any)
|
||||
for _, origin := range origins {
|
||||
if origin.(string) == originHeader {
|
||||
w.Header().Add("Access-Control-Allow-Origin", origin.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Method == http.MethodOptions {
|
||||
acrh := req.Header.Get("Access-Control-Request-Headers")
|
||||
acrm := req.Header.Get("Access-Control-Request-Method")
|
||||
if route.method != acrm {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Congrats, you get whatever headers you want
|
||||
w.Header().Add("Access-Control-Allow-Headers", acrh)
|
||||
w.Header().Add("Access-Control-Allow-Methods", acrm)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !route.anonymous {
|
||||
authSegments := strings.Split(req.Header.Get("Authorization"), " ")
|
||||
|
||||
if len(authSegments) != 2 || authSegments[0] != "Bearer" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.ValidateJwtToken(authSegments[1], &ctx.Claims); err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if route.scope != "" {
|
||||
if !strings.Contains(ctx.Claims.Scopes, route.scope) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
route.handler(&ctx)
|
||||
}
|
||||
|
||||
func AddGet(pattern string, handler func(*Context)) Route {
|
||||
return addRoute(http.MethodGet, pattern, handler)
|
||||
}
|
||||
|
||||
func AddPost(pattern string, handler func(*Context)) Route {
|
||||
return addRoute(http.MethodPost, pattern, handler)
|
||||
}
|
||||
|
||||
func AddPut(pattern string, handler func(*Context)) Route {
|
||||
return addRoute(http.MethodPut, pattern, handler)
|
||||
}
|
||||
|
||||
func AddPatch(pattern string, handler func(*Context)) Route {
|
||||
return addRoute(http.MethodPatch, pattern, handler)
|
||||
}
|
||||
|
||||
func AddDelete(pattern string, handler func(*Context)) Route {
|
||||
return addRoute(http.MethodDelete, pattern, handler)
|
||||
}
|
||||
|
||||
func addRoute(method string, path string, handler func(*Context)) Route {
|
||||
route := Route{
|
||||
handler: handler,
|
||||
method: method,
|
||||
path: path,
|
||||
}
|
||||
|
||||
if routeMap[method] == nil {
|
||||
routeMap[method] = []Route{}
|
||||
}
|
||||
|
||||
routeMap[method] = append(routeMap[method], route)
|
||||
|
||||
return route
|
||||
}
|
||||
|
||||
func getRoute(req *http.Request) (Route, bool) {
|
||||
method := req.Method
|
||||
if method == http.MethodOptions {
|
||||
method = req.Header.Get("Access-Control-Request-Method")
|
||||
}
|
||||
|
||||
req.URL.Path = strings.TrimSuffix(req.URL.Path, "/")
|
||||
|
||||
for _, route := range routeMap[method] {
|
||||
//if strings.Contains(route.path, ":") {
|
||||
// rte, ok := regexMatchRoute(route, req.URL.Path)
|
||||
// if ok {
|
||||
// return rte, true
|
||||
// }
|
||||
//} else {
|
||||
if route.path == req.URL.Path {
|
||||
return route, true
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
return Route{}, false
|
||||
}
|
||||
|
||||
//func regexMatchRoute(route Route, path string) (Route, bool) {
|
||||
// regex := regexp.MustCompile(`:[a-zA-Z]+`)
|
||||
// matchPath := regex.ReplaceAllString(route.path, `([A-Za-z0-9\-_.]+)`)
|
||||
// matchPath += "$"
|
||||
// regex = regexp.MustCompile(matchPath)
|
||||
//
|
||||
// // matchPath := route.path
|
||||
// if matches := regex.FindStringSubmatch(path); len(matches) > 0 {
|
||||
// if len(matches) > 1 {
|
||||
// route.params = map[string]any{}
|
||||
//
|
||||
// values := strings.Split(path, "/")
|
||||
// for i, param := range strings.Split(route.path, "/") {
|
||||
// if strings.Contains(param, ":") {
|
||||
// route.params[param[1:]] = values[i]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return route, true
|
||||
// }
|
||||
//
|
||||
// return route, false
|
||||
//}
|
||||
Reference in New Issue
Block a user