goploy/core/Route.go
2022-04-08 15:06:52 +08:00

334 lines
8.4 KiB
Go

package core
import (
"database/sql"
"errors"
"fmt"
"github.com/golang-jwt/jwt"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/model"
"github.com/zhenorzz/goploy/response"
"github.com/zhenorzz/goploy/web"
"io/fs"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"strconv"
"strings"
)
type Namespace struct {
ID int64
Role string
PermissionIDs map[int64]struct{}
}
type Goploy struct {
UserInfo model.User
Namespace Namespace
Request *http.Request
ResponseWriter http.ResponseWriter
URLQuery url.Values
Body []byte
}
type RouteApi interface {
Routes() []Route
}
type Response interface {
Write(http.ResponseWriter) error
}
type Route struct {
pattern string
method string // Method specifies the HTTP method (GET, POST, PUT, etc.).
roles map[string]struct{} // permission role
permissionIDs []int64 // permission list
white bool // no need to login
middlewares []func(gp *Goploy) error // Middlewares run before callback, trigger error will end the request
callback func(gp *Goploy) Response // Controller function
logFunc func(gp *Goploy, resp Response)
}
// Router is Route slice and global middlewares
type Router struct {
routes map[string]Route
middlewares []func(gp *Goploy) error // Middlewares run before all Route
}
func NewRouter() Router {
return Router{
routes: map[string]Route{},
}
}
func NewRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
return newRoute(pattern, method, callback)
}
func NewWhiteRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
route := newRoute(pattern, method, callback)
route.white = true
return route
}
func newRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
return Route{
pattern: pattern,
method: method,
callback: callback,
roles: map[string]struct{}{},
}
}
// Start a router
func (rt Router) Start() {
if config.Toml.Env == "production" {
subFS, err := fs.Sub(web.Dist, "dist")
if err != nil {
log.Fatal(err)
}
http.Handle("/assets/", http.FileServer(http.FS(subFS)))
http.Handle("/favicon.ico", http.FileServer(http.FS(subFS)))
}
http.Handle("/", rt)
}
// Middleware global Middleware handle function
func (rt Router) Middleware(middleware func(gp *Goploy) error) {
rt.middlewares = append(rt.middlewares, middleware)
}
// Add pattern path
// callback where path should be handled
func (rt Router) Add(ra RouteApi) Router {
for _, r := range ra.Routes() {
rt.routes[r.pattern] = r
}
return rt
}
// Roles Add much permission to the Route
func (r Route) Roles(roles ...string) Route {
for _, role := range roles {
r.roles[role] = struct{}{}
}
return r
}
func (r Route) Permissions(permissionIDs ...int64) Route {
for _, permissionID := range permissionIDs {
r.permissionIDs = append(r.permissionIDs, permissionID)
}
return r
}
// Middleware global Middleware handle function
func (r Route) Middleware(middleware func(gp *Goploy) error) Route {
r.middlewares = append(r.middlewares, middleware)
return r
}
// LogFunc callback finished
func (r Route) LogFunc(f func(gp *Goploy, resp Response)) Route {
r.logFunc = f
return r
}
func (rt Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If in production env, serve file in go server,
// else serve file in npm
if config.Toml.Env == "production" {
if "/" == r.URL.Path {
r, err := web.Dist.Open("dist/index.html")
if err != nil {
log.Fatal(err)
}
defer r.Close()
contents, err := ioutil.ReadAll(r)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, string(contents))
return
}
}
_, resp := rt.doRequest(w, r)
if err := resp.Write(w); err != nil {
Log(ERROR, err.Error())
}
return
}
func (rt Router) doRequest(w http.ResponseWriter, r *http.Request) (*Goploy, Response) {
gp := new(Goploy)
route, ok := rt.routes[r.URL.Path]
if !ok {
return gp, response.JSON{Code: response.Deny, Message: "No such method"}
}
if route.method != r.Method {
return gp, response.JSON{Code: response.IllegalRequest, Message: "Invalid request method"}
}
if !route.white {
unParseToken := ""
// check token
goployTokenCookie, err := r.Cookie(config.Toml.Cookie.Name)
if err != nil {
unParseToken = r.URL.Query().Get(config.Toml.Cookie.Name)
} else {
unParseToken = goployTokenCookie.Value
}
if unParseToken == "" {
return gp, response.JSON{Code: response.IllegalRequest, Message: "Illegal request"}
}
claims := jwt.MapClaims{}
token, err := jwt.ParseWithClaims(unParseToken, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(config.Toml.JWT.Key), nil
})
if err != nil || !token.Valid {
return gp, response.JSON{Code: response.LoginExpired, Message: "Login expired"}
}
namespaceIDRaw := r.Header.Get(NamespaceHeaderName)
if namespaceIDRaw == "" {
namespaceIDRaw = r.URL.Query().Get(NamespaceHeaderName)
}
namespaceID, err := strconv.ParseInt(namespaceIDRaw, 10, 64)
if err != nil {
return gp, response.JSON{Code: response.Deny, Message: "Invalid namespace"}
}
gp.UserInfo, err = model.User{ID: int64(claims["id"].(float64))}.GetData()
if err != nil {
return gp, response.JSON{Code: response.Deny, Message: "Get user information error"}
}
if gp.UserInfo.State != 1 {
return gp, response.JSON{Code: response.AccountDisabled, Message: "No available user"}
}
if gp.UserInfo.SuperManager == model.SuperManager {
permissionIDs, err := model.Permission{}.GetIDs()
if err != nil {
return gp, response.JSON{Code: response.Deny, Message: err.Error()}
}
gp.Namespace = Namespace{
ID: namespaceID,
Role: RoleAdmin,
PermissionIDs: permissionIDs,
}
} else {
namespace, err := model.NamespaceUser{
NamespaceID: namespaceID,
UserID: int64(claims["id"].(float64)),
}.GetDataByUserNamespace()
if err != nil {
if err == sql.ErrNoRows {
return gp, response.JSON{Code: response.NamespaceInvalid, Message: "No available namespace"}
} else {
return gp, response.JSON{Code: response.Deny, Message: err.Error()}
}
}
gp.Namespace = Namespace{
ID: namespaceID,
PermissionIDs: namespace.PermissionIDs,
}
}
if err = route.hasRole(gp.Namespace.Role); err != nil {
return gp, response.JSON{Code: response.Deny, Message: err.Error()}
}
if err = route.hasPermission(gp.Namespace.PermissionIDs); err != nil {
return gp, response.JSON{Code: response.Deny, Message: err.Error()}
}
goployTokenStr, err := model.User{ID: int64(claims["id"].(float64)), Name: claims["name"].(string)}.CreateToken()
if err == nil {
// update jwt time
cookie := http.Cookie{Name: config.Toml.Cookie.Name, Value: goployTokenStr, Path: "/", MaxAge: config.Toml.Cookie.Expire, HttpOnly: true}
http.SetCookie(w, &cookie)
}
}
gp.Request = r
gp.ResponseWriter = w
gp.URLQuery = r.URL.Query()
// save the body request data because ioutil.ReadAll will clear the requestBody
if r.ContentLength > 0 && hasContentType(r, "application/json") {
gp.Body, _ = ioutil.ReadAll(r.Body)
}
// common middlewares
for _, middleware := range rt.middlewares {
err := middleware(gp)
if err != nil {
return gp, response.JSON{Code: response.Error, Message: err.Error()}
}
}
// route middlewares
for _, middleware := range route.middlewares {
if err := middleware(gp); err != nil {
return gp, response.JSON{Code: response.Error, Message: err.Error()}
}
}
resp := route.callback(gp)
if route.logFunc != nil {
route.logFunc(gp, resp)
}
return gp, resp
}
func (r Route) hasRole(namespaceRole string) error {
if len(r.roles) == 0 {
return nil
}
if _, ok := r.roles[namespaceRole]; ok {
return nil
}
return errors.New("no permission")
}
func (r Route) hasPermission(permissionIDs map[int64]struct{}) error {
if len(r.permissionIDs) == 0 {
return nil
}
for _, permissionID := range r.permissionIDs {
if _, ok := permissionIDs[permissionID]; ok {
return nil
}
}
return errors.New("no permission")
}
func hasContentType(r *http.Request, mimetype string) bool {
contentType := r.Header.Get("Content-type")
if contentType == "" {
return false
}
for _, v := range strings.Split(contentType, ",") {
t, _, err := mime.ParseMediaType(v)
if err != nil {
break
}
if t == mimetype {
return true
}
}
return false
}