diff --git a/go.mod b/go.mod index 820d14d5..12fd0be5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/BurntSushi/toml v1.2.1 github.com/asticode/go-astisub v0.21.0 + github.com/gin-contrib/sessions v0.0.5 github.com/gin-gonic/gin v1.8.1 github.com/go-git/go-git/v5 v5.4.2 github.com/go-sql-driver/mysql v1.6.0 @@ -36,6 +37,9 @@ require ( github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-json v0.9.7 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/gorilla/context v1.1.1 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/gorilla/sessions v1.2.1 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -47,6 +51,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/u2takey/go-utils v0.3.1 // indirect github.com/ugorji/go/codec v1.2.7 // indirect diff --git a/go.sum b/go.sum index fe27fcc8..5b8d5161 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= +github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= @@ -68,6 +70,12 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -122,6 +130,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= +github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= @@ -139,8 +149,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/studio-b12/gowebdav v0.0.0-20221015232716-17255f2e7423 h1:Wd8WDEEusB5+En4PiRWJp1cP59QLNsQun+mOTW8+s6s= -github.com/studio-b12/gowebdav v0.0.0-20221015232716-17255f2e7423/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/studio-b12/gowebdav v0.0.0-20221102155456-200a600c0272 h1:dXbdJSdxf0EnR4SkcsfRNuGCvoEk9lavXbSCFXN2gJc= github.com/studio-b12/gowebdav v0.0.0-20221102155456-200a600c0272/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/u2takey/ffmpeg-go v0.4.1 h1:l5ClIwL3N2LaH1zF3xivb3kP2HW95eyG5xhHE1JdZ9Y= diff --git a/qa/api/auth.go b/qa/api/auth.go new file mode 100644 index 00000000..8f1a1eaf --- /dev/null +++ b/qa/api/auth.go @@ -0,0 +1,55 @@ +package api + +import ( + "log" + "net/http" + "os" + "path" + "strconv" + + "github.com/gin-gonic/gin" +) + +var Simulator string +var TeamsDir string + +func authMiddleware(access ...func(string, int64, *gin.Context) bool) gin.HandlerFunc { + return func(c *gin.Context) { + ficteam := Simulator + if t := c.Request.Header.Get("X-FIC-Team"); t != "" { + ficteam = t + } + + var teamid int64 + var err error + + if ficteam == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Need to authenticate"}) + return + } else if teamid, err = strconv.ParseInt(ficteam, 10, 64); err != nil { + if lnk, err := os.Readlink(path.Join(TeamsDir, ficteam)); err != nil { + log.Printf("[ERR] Unable to readlink %q: %s\n", path.Join(TeamsDir, ficteam), err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to validate authentication."}) + return + } else if teamid, err = strconv.ParseInt(lnk, 10, 64); err != nil { + log.Printf("[ERR] Error during ParseInt team %q: %s\n", lnk, err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to validate authentication."}) + return + } + } + + // Check access limitation + for _, a := range access { + if !a(ficteam, teamid, c) { + return + } + } + + // Retrieve corresponding user + c.Set("LoggedUser", ficteam) + c.Set("LoggedTeam", teamid) + + // We are now ready to continue + c.Next() + } +} diff --git a/qa/api/exercice.go b/qa/api/exercice.go index 0414b10d..1303406b 100644 --- a/qa/api/exercice.go +++ b/qa/api/exercice.go @@ -1,36 +1,58 @@ package api import ( + "fmt" + "log" + "net/http" "strconv" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/exercices/", apiHandler(listExercices)) +func declareExercicesRoutes(router *gin.RouterGroup) { + router.GET("/exercices", listExercices) - router.GET("/api/exercices/:eid", apiHandler(exerciceHandler(showExercice))) + exercicesRoutes := router.Group("/exercices/:eid") + exercicesRoutes.Use(exerciceHandler) + exercicesRoutes.GET("", showExercice) } -func exerciceHandler(f func(QAUser, *fic.Exercice, []byte) (interface{}, error)) func(QAUser, httprouter.Params, []byte) (interface{}, error) { - return func(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - if eid, err := strconv.ParseInt(string(ps.ByName("eid")), 10, 64); err != nil { - return nil, err - } else if exercice, err := fic.GetExercice(eid); err != nil { - return nil, err - } else { - return f(u, exercice, body) +func exerciceHandler(c *gin.Context) { + var exercice *fic.Exercice + if eid, err := strconv.ParseInt(string(c.Param("eid")), 10, 64); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad exercice identifier."}) + return + } else if exercice, err = fic.GetExercice(eid); err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Exercice not found."}) + return + } + + if th, ok := c.Get("theme"); ok { + if exercice.IdTheme != th.(*fic.Theme).Id { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Exercice not found."}) + return } } + + c.Set("exercice", exercice) + + c.Next() } -func listExercices(_ QAUser, _ httprouter.Params, body []byte) (interface{}, error) { +func listExercices(c *gin.Context) { // List all exercices - return fic.GetExercices() + exercices, err := fic.GetExercices() + if err != nil { + log.Println("Unable to GetExercices: ", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list exercices: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, exercices) } -func showExercice(_ QAUser, exercice *fic.Exercice, body []byte) (interface{}, error) { - return exercice, nil +func showExercice(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("exercice")) } diff --git a/qa/api/handler.go b/qa/api/handler.go deleted file mode 100644 index 424c0288..00000000 --- a/qa/api/handler.go +++ /dev/null @@ -1,113 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "path" - "strconv" - "time" - - "github.com/julienschmidt/httprouter" -) - -var Simulator string -var TeamsDir string - -type QAUser struct { - User string `json:"name"` - TeamId int64 `json:"id_team"` -} - -type DispatchFunction func(QAUser, httprouter.Params, []byte) (interface{}, error) - -func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) { - return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - ficteam := Simulator - if t := r.Header.Get("X-FIC-Team"); t != "" { - ficteam = t - } - - var teamid int64 - var err error - - if ficteam == "" { - log.Printf("%s 401 \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent()) - w.Header().Set("Content-Type", "application/json") - http.Error(w, fmt.Sprintf("{\"errmsg\":\"Need to authenticate.\"}"), http.StatusUnauthorized) - return - } else if teamid, err = strconv.ParseInt(ficteam, 10, 64); err != nil { - if lnk, err := os.Readlink(path.Join(TeamsDir, ficteam)); err != nil { - log.Printf("[ERR] Unable to readlink %q: %s\n", path.Join(TeamsDir, ficteam), err) - http.Error(w, fmt.Sprintf("{\"errmsg\":\"Unable to validate authentication.\"}"), http.StatusInternalServerError) - return - } else if teamid, err = strconv.ParseInt(lnk, 10, 64); err != nil { - log.Printf("[ERR] Error during ParseInt team %q: %s\n", lnk, err) - http.Error(w, fmt.Sprintf("{\"errmsg\":\"Unable to validate authentication.\"}"), http.StatusInternalServerError) - return - } - } - - log.Printf("%s \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent()) - - // Read the body - if r.ContentLength < 0 || r.ContentLength > 6553600 { - http.Error(w, fmt.Sprintf("{\"errmsg\":\"Request too large or request size unknown\"}"), http.StatusRequestEntityTooLarge) - return - } - var body []byte - if r.ContentLength > 0 { - tmp := make([]byte, 1024) - for { - n, err := r.Body.Read(tmp) - for j := 0; j < n; j++ { - body = append(body, tmp[j]) - } - if err != nil || n <= 0 { - break - } - } - } - - var ret interface{} - - ret, err = f(QAUser{ficteam, teamid}, ps, body) - - // Format response - resStatus := http.StatusOK - if err != nil { - ret = map[string]string{"errmsg": err.Error()} - resStatus = http.StatusBadRequest - log.Println(r.RemoteAddr, resStatus, err.Error()) - } - - if ret == nil { - ret = map[string]string{"errmsg": "Page not found"} - resStatus = http.StatusNotFound - } - - w.Header().Set("X-FIC-Time", fmt.Sprintf("%f", float64(time.Now().UnixNano()/1000)/1000000)) - - if str, found := ret.(string); found { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(resStatus) - io.WriteString(w, str) - } else if bts, found := ret.([]byte); found { - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Content-Disposition", "attachment") - w.Header().Set("Content-Transfer-Encoding", "binary") - w.WriteHeader(resStatus) - w.Write(bts) - } else if j, err := json.Marshal(ret); err != nil { - w.Header().Set("Content-Type", "application/json") - http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", err), http.StatusInternalServerError) - } else { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(resStatus) - w.Write(j) - } - } -} diff --git a/qa/api/qa.go b/qa/api/qa.go index 25bf8950..be6c03c8 100644 --- a/qa/api/qa.go +++ b/qa/api/qa.go @@ -1,144 +1,231 @@ package api import ( - "encoding/json" - "errors" + "fmt" + "log" + "net/http" "strconv" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/qa/:eid", apiHandler(exerciceHandler(getExerciceQA))) - router.POST("/api/qa/:eid", apiHandler(exerciceHandler(createExerciceQA))) +func declareQARoutes(router *gin.RouterGroup) { + exercicesRoutes := router.Group("/qa/:eid") + exercicesRoutes.Use(exerciceHandler) + exercicesRoutes.GET("", getExerciceQA) + exercicesRoutes.POST("", createExerciceQA) - router.PUT("/api/qa/:eid/:qid", apiHandler(qaHandler(updateExerciceQA))) - router.DELETE("/api/qa/:eid/:qid", apiHandler(qaHandler(deleteExerciceQA))) + qaRoutes := exercicesRoutes.Group("/:qid") + qaRoutes.Use(qaHandler) + qaRoutes.PUT("", updateExerciceQA) + qaRoutes.DELETE("", deleteExerciceQA) + qaRoutes.GET("comments", getQAComments) + qaRoutes.POST("comments", createQAComment) - router.GET("/api/qa/:eid/:qid/comments", apiHandler(qaHandler(getQAComments))) - router.POST("/api/qa/:eid/:qid/comments", apiHandler(qaHandler(createQAComment))) - - router.DELETE("/api/qa/:eid/:qid/comments/:cid", apiHandler(qaCommentHandler(deleteQAComment))) + commentsRoutes := qaRoutes.Group("comments/:cid") + commentsRoutes.Use(qaCommentHandler) + commentsRoutes.DELETE("", deleteQAComment) } -func qaHandler(f func(QAUser, *fic.QAQuery, *fic.Exercice, []byte) (interface{}, error)) func(QAUser, httprouter.Params, []byte) (interface{}, error) { - return func(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - return exerciceHandler(func(u QAUser, exercice *fic.Exercice, _ []byte) (interface{}, error) { - if qid, err := strconv.ParseInt(string(ps.ByName("qid")), 10, 64); err != nil { - return nil, err - } else if query, err := exercice.GetQAQuery(qid); err != nil { - return nil, err - } else { - return f(u, query, exercice, body) - } - })(u, ps, body) +func qaHandler(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + var qa *fic.QAQuery + if qid, err := strconv.ParseInt(string(c.Param("qid")), 10, 64); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad QA identifier."}) + return + } else if qa, err = exercice.GetQAQuery(qid); err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "QA entry not found."}) + return } + + c.Set("qa", qa) + + c.Next() } -func qaCommentHandler(f func(QAUser, *fic.QAComment, *fic.QAQuery, *fic.Exercice, []byte) (interface{}, error)) func(QAUser, httprouter.Params, []byte) (interface{}, error) { - return func(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - return qaHandler(func(u QAUser, query *fic.QAQuery, exercice *fic.Exercice, _ []byte) (interface{}, error) { - if cid, err := strconv.ParseInt(string(ps.ByName("cid")), 10, 64); err != nil { - return nil, err - } else if comment, err := query.GetComment(cid); err != nil { - return nil, err - } else { - return f(u, comment, query, exercice, body) - } - })(u, ps, body) +func qaCommentHandler(c *gin.Context) { + qa := c.MustGet("qa").(*fic.QAQuery) + var comment *fic.QAComment + if cid, err := strconv.ParseInt(string(c.Param("cid")), 10, 64); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad comment identifier."}) + return + } else if comment, err = qa.GetComment(cid); err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Comment entry not found."}) + return } + + c.Set("comment", comment) + + c.Next() } -func getExerciceQA(_ QAUser, exercice *fic.Exercice, body []byte) (interface{}, error) { - return exercice.GetQAQueries() +func getExerciceQA(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + qa, err := exercice.GetQAQueries() + if err != nil { + log.Println("Unable to GetQAQueries: ", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list QA entries: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, qa) } -func createExerciceQA(u QAUser, exercice *fic.Exercice, body []byte) (interface{}, error) { +type QAQueryAndComment struct { + *fic.QAQuery + Content string `json:"content"` +} + +func createExerciceQA(c *gin.Context) { + teamid := c.MustGet("LoggedTeam").(int64) + ficteam := c.MustGet("LoggedUser").(string) + // Create a new query - var uq *fic.QAQuery - if err := json.Unmarshal(body, &uq); err != nil { - return nil, err + var uq QAQueryAndComment + if err := c.ShouldBindJSON(&uq); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(uq.State) == 0 { - return nil, errors.New("State not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "State not filled"}) + return } if len(uq.Subject) == 0 { if uq.State == "ok" { uq.Subject = "RAS" } else { - return nil, errors.New("Subject not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Subject not filled"}) + return } } - if qa, err := exercice.NewQAQuery(uq.Subject, &u.TeamId, u.User, uq.State); err != nil { - return nil, err - } else { - var uc *fic.QAComment - if err := json.Unmarshal(body, &uc); err != nil { - return nil, err - } - - if uc.Content != "" { - _, err = qa.AddComment(uc.Content, &u.TeamId, u.User) - } - - return qa, err + exercice := c.MustGet("exercice").(*fic.Exercice) + qa, err := exercice.NewQAQuery(uq.Subject, &teamid, ficteam, uq.State) + if err != nil { + log.Println("Unable to NewQAQuery: ", err.Error()) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unable to create the new QA query. Please retry."}) + return } + + if len(uq.Content) > 0 { + _, err = qa.AddComment(uq.Content, &teamid, ficteam) + + if err != nil { + log.Println("Unable to AddComment: ", err.Error()) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "QA entry added successfully, but unable to create the associated comment. Please retry."}) + return + } + } + + c.JSON(http.StatusOK, qa) } -func updateExerciceQA(u QAUser, query *fic.QAQuery, exercice *fic.Exercice, body []byte) (interface{}, error) { +func updateExerciceQA(c *gin.Context) { + query := c.MustGet("qa").(*fic.QAQuery) + var uq *fic.QAQuery - if err := json.Unmarshal(body, &uq); err != nil { - return nil, err + if err := c.ShouldBindJSON(&uq); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } uq.Id = query.Id if uq.User != query.User && (uq.IdExercice != query.IdExercice || uq.IdTeam != query.IdTeam || uq.User != query.User || uq.Creation != query.Creation || uq.State != query.State || uq.Subject != query.Subject || uq.Closed != query.Closed) { - return nil, errors.New("You can only update your own entry.") + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "You can only update your own entry."}) + return } - if _, err := uq.Update(); err != nil { - return nil, err - } else { - return uq, err - } -} - -func deleteExerciceQA(u QAUser, query *fic.QAQuery, exercice *fic.Exercice, body []byte) (interface{}, error) { - if u.User != query.User { - return nil, errors.New("You can only delete your own entry.") + _, err := uq.Update() + if err != nil { + log.Println("Unable to Update QAQuery:", err.Error()) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unable to update the query. Please try again."}) + return } - return query.Delete() + c.JSON(http.StatusOK, uq) } -func getQAComments(_ QAUser, query *fic.QAQuery, exercice *fic.Exercice, body []byte) (interface{}, error) { - return query.GetComments() +func deleteExerciceQA(c *gin.Context) { + query := c.MustGet("qa").(*fic.QAQuery) + user := c.MustGet("LoggedUser").(string) + + if user != query.User { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "You can only delete your own entry."}) + return + } + + _, err := query.Delete() + if err != nil { + log.Println("Unable to Delete QAQuery:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to delete the query. Please try again."}) + return + } + + c.JSON(http.StatusNoContent, nil) } -func createQAComment(u QAUser, query *fic.QAQuery, exercice *fic.Exercice, body []byte) (interface{}, error) { +func getQAComments(c *gin.Context) { + query := c.MustGet("qa").(*fic.QAQuery) + + comments, err := query.GetComments() + if err != nil { + log.Println("Unable to GetComments: ", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list comments: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, comments) +} + +func createQAComment(c *gin.Context) { // Create a new query var uc *fic.QAComment - if err := json.Unmarshal(body, &uc); err != nil { - return nil, err + if err := c.ShouldBindJSON(&uc); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(uc.Content) == 0 { - return nil, errors.New("Empty comment") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Empty comment."}) + return } - return query.AddComment(uc.Content, &u.TeamId, u.User) -} + teamid := c.MustGet("LoggedTeam").(int64) + ficteam := c.MustGet("LoggedUser").(string) -func deleteQAComment(u QAUser, comment *fic.QAComment, query *fic.QAQuery, exercice *fic.Exercice, body []byte) (interface{}, error) { - if u.User != comment.User { - return nil, errors.New("You can only delete your own comment.") + query := c.MustGet("qa").(*fic.QAQuery) + comment, err := query.AddComment(uc.Content, &teamid, ficteam) + if err != nil { + log.Println("Unable to AddComment: ", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to add your comment. Please try again later."}) + return } - return comment.Delete() + c.JSON(http.StatusOK, comment) +} + +func deleteQAComment(c *gin.Context) { + ficteam := c.MustGet("LoggedUser").(string) + comment := c.MustGet("comment").(*fic.QAComment) + + if ficteam != comment.User { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "You can only delete your own comment."}) + return + } + + _, err := comment.Delete() + if err != nil { + log.Println("Unable to Delete QAComment:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to delete the comment. Please try again."}) + return + } + + c.JSON(http.StatusNoContent, nil) + } diff --git a/qa/api/router.go b/qa/api/router.go index a6bd873b..1c08cf26 100644 --- a/qa/api/router.go +++ b/qa/api/router.go @@ -1,11 +1,16 @@ package api import ( - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -var router = httprouter.New() +func DeclareRoutes(router *gin.RouterGroup) { + apiRoutes := router.Group("/api") + apiRoutes.Use(authMiddleware()) -func Router() *httprouter.Router { - return router + declareExercicesRoutes(apiRoutes) + declareQARoutes(apiRoutes) + declareThemesRoutes(apiRoutes) + declareTodoRoutes(apiRoutes) + declareVersionRoutes(apiRoutes) } diff --git a/qa/api/theme.go b/qa/api/theme.go index 99f27a38..12229be1 100644 --- a/qa/api/theme.go +++ b/qa/api/theme.go @@ -1,64 +1,64 @@ package api import ( + "fmt" + "log" + "net/http" "strconv" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/themes", apiHandler(listThemes)) - router.GET("/api/themes.json", apiHandler(exportThemes)) +func declareThemesRoutes(router *gin.RouterGroup) { + router.GET("/themes", listThemes) + router.GET("/themes.json", exportThemes) - router.GET("/api/themes/:thid", apiHandler(themeHandler(showTheme))) + themesRoutes := router.Group("/themes/:thid") + themesRoutes.Use(themeHandler) + themesRoutes.GET("", showTheme) - router.GET("/api/themes/:thid/exercices", apiHandler(themeHandler(listThemedExercices))) - - router.GET("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(showExercice))) + declareExercicesRoutes(themesRoutes) } -func themeHandler(f func(QAUser, *fic.Theme, []byte) (interface{}, error)) func(QAUser, httprouter.Params, []byte) (interface{}, error) { - return func(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - if thid, err := strconv.ParseInt(string(ps.ByName("thid")), 10, 64); err != nil { - return nil, err - } else if theme, err := fic.GetTheme(thid); err != nil { - return nil, err - } else { - return f(u, theme, body) - } +func themeHandler(c *gin.Context) { + var theme *fic.Theme + if thid, err := strconv.ParseInt(string(c.Param("thid")), 10, 64); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad theme identifier."}) + return + } else if theme, err = fic.GetTheme(thid); err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found."}) + return } + + c.Set("theme", theme) + + c.Next() } -func getExercice(args []string) (*fic.Exercice, error) { - if tid, err := strconv.ParseInt(string(args[0]), 10, 64); err != nil { - return nil, err - } else if theme, err := fic.GetTheme(tid); err != nil { - return nil, err - } else if eid, err := strconv.Atoi(string(args[1])); err != nil { - return nil, err - } else { - return theme.GetExercice(eid) +func listThemes(c *gin.Context) { + themes, err := fic.GetThemes() + if err != nil { + log.Println("Unable to GetThemes: ", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list themes: %s", err.Error())}) + return } + + c.JSON(http.StatusOK, themes) } -func listThemes(_ QAUser, _ httprouter.Params, _ []byte) (interface{}, error) { - return fic.GetThemes() +func exportThemes(c *gin.Context) { + themes, err := fic.ExportThemes() + if err != nil { + log.Println("Unable to ExportThemes: ", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to export themes: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, themes) } -func exportThemes(_ QAUser, _ httprouter.Params, _ []byte) (interface{}, error) { - return fic.ExportThemes() -} - -func showTheme(_ QAUser, theme *fic.Theme, _ []byte) (interface{}, error) { - return theme, nil -} - -func listThemedExercices(_ QAUser, theme *fic.Theme, _ []byte) (interface{}, error) { - return theme.GetExercices() -} - -func showThemedExercice(_ QAUser, theme *fic.Theme, exercice *fic.Exercice, body []byte) (interface{}, error) { - return exercice, nil +func showTheme(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("theme")) } diff --git a/qa/api/todo.go b/qa/api/todo.go index ada608e5..373a0993 100644 --- a/qa/api/todo.go +++ b/qa/api/todo.go @@ -1,118 +1,176 @@ package api import ( - "encoding/json" - "errors" + "net/http" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/qa_exercices.json", apiHandler(getExerciceTested)) - router.GET("/api/qa_mywork.json", apiHandler(getQAWork)) - router.GET("/api/qa_myexercices.json", apiHandler(getQAView)) - router.POST("/api/qa_my_exercices.json", apiHandler(addQAView)) - router.GET("/api/qa_work.json", apiHandler(getQATodo)) - router.POST("/api/qa_work.json", apiHandler(createQATodo)) +func declareTodoRoutes(router *gin.RouterGroup) { + router.GET("/qa_exercices.json", getExerciceTested) + router.GET("/qa_mywork.json", getQAWork) + router.GET("/qa_myexercices.json", getQAView) + router.POST("/qa_my_exercices.json", addQAView) + router.GET("/qa_work.json", getQATodo) + router.POST("/qa_work.json", createQATodo) } type exerciceTested map[int64]string -func getExerciceTested(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - if team, err := fic.GetTeam(u.TeamId); err != nil { - return nil, err - } else if exercices, err := fic.GetExercices(); err != nil { - return nil, err - } else { - ret := exerciceTested{} +func getExerciceTested(c *gin.Context) { + teamid := c.MustGet("LoggedTeam").(int64) - for _, exercice := range exercices { - if team.HasAccess(exercice) { - if t := team.HasSolved(exercice); t != nil { - ret[exercice.Id] = "solved" - } else if cnt, _ := team.CountTries(exercice); cnt > 0 { - ret[exercice.Id] = "tried" - } else { - ret[exercice.Id] = "access" - } + team, err := fic.GetTeam(teamid) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + exercices, err := fic.GetExercices() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + ret := exerciceTested{} + + for _, exercice := range exercices { + if team.HasAccess(exercice) { + if t := team.HasSolved(exercice); t != nil { + ret[exercice.Id] = "solved" + } else if cnt, _ := team.CountTries(exercice); cnt > 0 { + ret[exercice.Id] = "tried" + } else { + ret[exercice.Id] = "access" } } - - return ret, nil } + + c.JSON(http.StatusOK, ret) } -func getQAView(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - if team, err := fic.GetTeam(u.TeamId); err != nil { - return nil, err - } else { - return team.GetQAView() +func getQAView(c *gin.Context) { + teamid := c.MustGet("LoggedTeam").(int64) + + team, err := fic.GetTeam(teamid) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } + + view, err := team.GetQAView() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, view) } -func getQAWork(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - if team, err := fic.GetTeam(u.TeamId); err != nil { - return nil, err - } else { - return team.GetQAQueries() +func getQAWork(c *gin.Context) { + teamid := c.MustGet("LoggedTeam").(int64) + + team, err := fic.GetTeam(teamid) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } + + queries, err := team.GetQAQueries() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, queries) } -func getQATodo(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - if team, err := fic.GetTeam(u.TeamId); err != nil { - return nil, err - } else { - todo, err := team.GetQATodo() - if err != nil { - return nil, err +func getQATodo(c *gin.Context) { + teamid := c.MustGet("LoggedTeam").(int64) + + team, err := fic.GetTeam(teamid) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + todo, err := team.GetQATodo() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + exercices, err := fic.GetExercices() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + for _, exercice := range exercices { + if cnt, _ := team.CountTries(exercice); cnt > 0 { + todo = append(todo, &fic.QATodo{0, teamid, exercice.Id}) } - - if exercices, err := fic.GetExercices(); err != nil { - return todo, nil - } else { - for _, exercice := range exercices { - if cnt, _ := team.CountTries(exercice); cnt > 0 { - todo = append(todo, &fic.QATodo{0, team.Id, exercice.Id}) - } - } - } - - return todo, nil } + + c.JSON(http.StatusOK, todo) } -func createQATodo(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - if u.User != "nemunaire" { - return nil, errors.New("Restricted") +func createQATodo(c *gin.Context) { + ficteam := c.MustGet("LoggedUser").(string) + + if ficteam != "nemunaire" { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Restricted"}) + return } var ut fic.QATodo - if err := json.Unmarshal(body, &ut); err != nil { - return nil, err + if err := c.ShouldBindJSON(&ut); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } - if team, err := fic.GetTeam(ut.IdTeam); err != nil { - return nil, err - } else { - return team.NewQATodo(ut.IdExercice) + team, err := fic.GetTeam(ut.IdTeam) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } + + todo, err := team.NewQATodo(ut.IdExercice) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, todo) } -func addQAView(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) { - if u.User != "nemunaire" { - return nil, errors.New("Restricted") +func addQAView(c *gin.Context) { + ficteam := c.MustGet("LoggedUser").(string) + + if ficteam != "nemunaire" { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Restricted"}) + return } var ut fic.QATodo - if err := json.Unmarshal(body, &ut); err != nil { - return nil, err + if err := c.ShouldBindJSON(&ut); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } - if team, err := fic.GetTeam(ut.IdTeam); err != nil { - return nil, err - } else { - return team.NewQAView(ut.IdExercice) + team, err := fic.GetTeam(ut.IdTeam) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } + + view, err := team.NewQAView(ut.IdExercice) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, view) } diff --git a/qa/api/version.go b/qa/api/version.go index c351b81e..0d28020c 100644 --- a/qa/api/version.go +++ b/qa/api/version.go @@ -1,13 +1,24 @@ package api import ( - "github.com/julienschmidt/httprouter" + "net/http" + + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/version", apiHandler(showVersion)) +func declareVersionRoutes(router *gin.RouterGroup) { + router.GET("/version", showVersion) } -func showVersion(u QAUser, _ httprouter.Params, body []byte) (interface{}, error) { - return map[string]interface{}{"version": 0.1, "auth": u}, nil +func showVersion(c *gin.Context) { + teamid := c.MustGet("LoggedTeam").(int64) + ficteam := c.MustGet("LoggedUser").(string) + + c.JSON(http.StatusOK, gin.H{ + "version": 0.2, + "auth": map[string]interface{}{ + "name": ficteam, + "id_team": teamid, + }, + }) } diff --git a/qa/app.go b/qa/app.go new file mode 100644 index 00000000..7958be71 --- /dev/null +++ b/qa/app.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "log" + "net/http" + "time" + + "srs.epita.fr/fic-server/qa/api" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/memstore" + "github.com/gin-gonic/gin" +) + +type App struct { + router *gin.Engine + srv *http.Server +} + +func NewApp(baseURL string) App { + gin.ForceConsoleColor() + router := gin.Default() + + store := memstore.NewStore([]byte("secret")) + router.Use(sessions.Sessions("qa-session", store)) + + api.DeclareRoutes(router.Group("")) + + var baserouter *gin.RouterGroup + if len(baseURL) > 0 { + router.GET("/", func(c *gin.Context) { + c.Redirect(http.StatusFound, baseURL) + }) + + baserouter = router.Group(baseURL) + + api.DeclareRoutes(baserouter) + declareStaticRoutes(baserouter, baseURL) + } else { + declareStaticRoutes(router.Group(""), "") + } + + app := App{ + router: router, + } + + return app +} + +func (app *App) Start(bind string) { + app.srv = &http.Server{ + Addr: bind, + Handler: app.router, + } + + if err := app.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } +} + +func (app *App) Stop() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := app.srv.Shutdown(ctx); err != nil { + log.Fatal("Server Shutdown:", err) + } +} diff --git a/qa/main.go b/qa/main.go index 2d204306..f6ad9003 100644 --- a/qa/main.go +++ b/qa/main.go @@ -1,9 +1,8 @@ package main import ( - "context" "flag" - "fmt" + "io/fs" "log" "net/http" "net/url" @@ -80,8 +79,20 @@ func main() { // Sanitize options var err error log.Println("Checking paths...") - if StaticDir, err = filepath.Abs(StaticDir); err != nil { - log.Fatal(err) + if StaticDir != "" { + if sDir, err := filepath.Abs(StaticDir); err != nil { + log.Fatal(err) + } else { + log.Println("Serving pages from", sDir) + staticFS = http.Dir(sDir) + } + } else { + sub, err := fs.Sub(assets, "static") + if err != nil { + log.Fatal("Unable to cd to static/ directory:", err) + } + log.Println("Serving pages from memory.") + staticFS = http.FS(sub) } if BaseURL != "/" { BaseURL = path.Clean(BaseURL) @@ -101,25 +112,17 @@ func main() { } defer fic.DBClose() + a := NewApp(BaseURL) + go a.Start(*bind) + // Prepare graceful shutdown interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) - srv := &http.Server{ - Addr: *bind, - Handler: StripPrefix(BaseURL, api.Router()), - } - - // Serve content - go func() { - log.Fatal(srv.ListenAndServe()) - }() - log.Println(fmt.Sprintf("Ready, listening on %s", *bind)) - // Wait shutdown signal <-interrupt log.Print("The service is shutting down...") - srv.Shutdown(context.Background()) + a.Stop() log.Println("done") } diff --git a/qa/static.go b/qa/static.go index 0d5e479b..60d39f1d 100644 --- a/qa/static.go +++ b/qa/static.go @@ -2,23 +2,25 @@ package main import ( "bytes" - "io" + "embed" "io/ioutil" "log" "net/http" "os" "path" + "strings" - "srs.epita.fr/fic-server/qa/api" - - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) +//go:embed static +var assets embed.FS + var BaseURL = "/" var indexTmpl []byte -func getIndexHtml(w io.Writer) { +func getIndexHtml(c *gin.Context) { if len(indexTmpl) == 0 { if file, err := os.Open(path.Join(StaticDir, "index.html")); err != nil { log.Println("Unable to open index.html: ", err) @@ -33,34 +35,41 @@ func getIndexHtml(w io.Writer) { } } - w.Write(indexTmpl) + c.Writer.Write(indexTmpl) } -func init() { - api.Router().GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - getIndexHtml(w) +var staticFS http.FileSystem + +func serveFile(c *gin.Context, url string) { + c.Request.URL.Path = url + http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request) +} + +func declareStaticRoutes(router *gin.RouterGroup, baseURL string) { + router.GET("/", func(c *gin.Context) { + getIndexHtml(c) }) - api.Router().GET("/exercices/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - getIndexHtml(w) + router.GET("/exercices/*_", func(c *gin.Context) { + getIndexHtml(c) }) - api.Router().GET("/themes/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - getIndexHtml(w) + router.GET("/themes/*_", func(c *gin.Context) { + getIndexHtml(c) }) - api.Router().GET("/css/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + router.GET("/css/*_", func(c *gin.Context) { + serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL)) }) - api.Router().GET("/fonts/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + router.GET("/fonts/*_", func(c *gin.Context) { + serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL)) }) - api.Router().GET("/img/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + router.GET("/img/*_", func(c *gin.Context) { + serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL)) }) - api.Router().GET("/js/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + router.GET("/js/*_", func(c *gin.Context) { + serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL)) }) - api.Router().GET("/views/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + router.GET("/views/*_", func(c *gin.Context) { + serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL)) }) }