qa: Use gin

This commit is contained in:
nemunaire 2022-11-06 16:36:31 +01:00
parent 9fd5564410
commit abdf146fea
13 changed files with 596 additions and 378 deletions

5
go.mod
View File

@ -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

12
go.sum
View File

@ -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=

55
qa/api/auth.go Normal file
View File

@ -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()
}
}

View File

@ -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"))
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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"))
}

View File

@ -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)
}

View File

@ -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,
},
})
}

68
qa/app.go Normal file
View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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))
})
}