diff --git a/admin/api/certificate.go b/admin/api/certificate.go index ebb58a3f..6ad3ac37 100644 --- a/admin/api/certificate.go +++ b/admin/api/certificate.go @@ -7,13 +7,13 @@ import ( "crypto/x509/pkix" "encoding/base32" "encoding/base64" - "encoding/json" "errors" "fmt" "io/ioutil" "log" "math" "math/big" + "net/http" "os" "path" "strconv" @@ -23,91 +23,178 @@ import ( "srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) var TeamsDir string -func init() { - router.GET("/api/htpasswd", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return genHtpasswd(true) - })) - router.POST("/api/htpasswd", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - if htpasswd, err := genHtpasswd(true); err != nil { - return nil, err - } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "ficpasswd"), []byte(htpasswd), 0644); err != nil { - return nil, err - } else { - return true, nil - } - })) - router.DELETE("/api/htpasswd", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil { - return nil, err - } else { - return true, nil - } - })) - router.GET("/api/htpasswd.apr1", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return genHtpasswd(false) - })) - router.GET("/api/ca/", apiHandler(infoCA)) - router.GET("/api/ca.pem", apiHandler(getCAPEM)) - router.POST("/api/ca/new", apiHandler( - func(_ httprouter.Params, body []byte) (interface{}, error) { - var upki PKISettings - if err := json.Unmarshal(body, &upki); err != nil { - return nil, err - } - return true, pki.GenerateCA(upki.NotBefore, upki.NotAfter) - })) +func declareCertificateRoutes(router *gin.RouterGroup) { + router.GET("/htpasswd", func(c *gin.Context) { + ret, err := genHtpasswd(true) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.String(http.StatusOK, ret) + }) + router.POST("/htpasswd", func(c *gin.Context) { + if htpasswd, err := genHtpasswd(true); err != nil { + log.Println("Unable to generate htpasswd:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "ficpasswd"), []byte(htpasswd), 0644); err != nil { + log.Println("Unable to write htpasswd:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + c.AbortWithStatus(http.StatusOK) + }) + router.DELETE("/htpasswd", func(c *gin.Context) { + if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil { + log.Println("Unable to remove htpasswd:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + c.AbortWithStatus(http.StatusOK) + }) + router.GET("/htpasswd.apr1", func(c *gin.Context) { + ret, err := genHtpasswd(false) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.String(http.StatusOK, ret) + }) + router.GET("/ca", infoCA) + router.GET("/ca.pem", getCAPEM) + router.POST("/ca/new", func(c *gin.Context) { + var upki PKISettings + err := c.ShouldBindJSON(&upki) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } - router.GET("/api/teams/:tid/certificates", apiHandler(teamHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil { - return nil, err - } else { - var certs []CertExported - for _, serial := range serials { - if cert, err := fic.GetCertificate(serial); err == nil { - certs = append(certs, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, &team.Id, cert.Revoked}) - } else { - log.Println("Unable to get back certificate, whereas an association exists on disk: ", err) - } + if err := pki.GenerateCA(upki.NotBefore, upki.NotAfter); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusCreated, true) + }) + + router.GET("/certs", getCertificates) + router.POST("/certs", generateClientCert) + router.DELETE("/certs", func(c *gin.Context) { + v, err := fic.ClearCertificates() + if err != nil { + log.Println("Unable to ClearCertificates:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, v) + }) + + apiCertificatesRoutes := router.Group("/certs/:certid") + apiCertificatesRoutes.Use(CertificateHandler) + apiCertificatesRoutes.HEAD("", getTeamP12File) + apiCertificatesRoutes.GET("", getTeamP12File) + apiCertificatesRoutes.PUT("", updateCertificateAssociation) + apiCertificatesRoutes.DELETE("", func(c *gin.Context) { + cert := c.MustGet("cert").(*fic.Certificate) + + v, err := cert.Revoke() + if err != nil { + log.Println("Unable to Revoke:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, v) + }) +} + +func declareTeamCertificateRoutes(router *gin.RouterGroup) { + router.GET("/certificates", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil { + log.Println("Unable to GetTeamSerials:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } else { + var certs []CertExported + for _, serial := range serials { + if cert, err := fic.GetCertificate(serial); err == nil { + certs = append(certs, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, &team.Id, cert.Revoked}) + } else { + log.Println("Unable to get back certificate, whereas an association exists on disk: ", err) } - return certs, nil } - }))) + c.JSON(http.StatusOK, certs) + } + }) - router.GET("/api/teams/:tid/associations", apiHandler(teamHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return pki.GetTeamAssociations(TeamsDir, team.Id) - }))) - router.POST("/api/teams/:tid/associations/:assoc", apiHandler(teamAssocHandler( - func(team *fic.Team, assoc string, _ []byte) (interface{}, error) { - if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, assoc)); err != nil { - return nil, err - } - return "\"" + assoc + "\"", nil - }))) - router.DELETE("/api/teams/:tid/associations/:assoc", apiHandler(teamAssocHandler( - func(team *fic.Team, assoc string, _ []byte) (interface{}, error) { - return "null", pki.DeleteTeamAssociation(TeamsDir, assoc) - }))) + router.GET("/associations", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) - router.GET("/api/certs/", apiHandler(getCertificates)) - router.POST("/api/certs/", apiHandler(generateClientCert)) - router.DELETE("/api/certs/", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { return fic.ClearCertificates() })) + assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id) + if err != nil { + log.Println("Unable to GetTeamAssociations:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } - router.HEAD("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File))) - router.GET("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File))) - router.PUT("/api/certs/:certid", apiHandler(certificateHandler(updateCertificateAssociation))) - router.DELETE("/api/certs/:certid", apiHandler(certificateHandler( - func(cert *fic.Certificate, _ []byte) (interface{}, error) { return cert.Revoke() }))) + c.JSON(http.StatusOK, assocs) + }) + + apiTeamAssociationsRoutes := router.Group("/associations/:assoc") + apiTeamAssociationsRoutes.POST("", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, c.Params.ByName("assoc"))); err != nil { + log.Println("Unable to create association symlink:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create association symlink: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, c.Params.ByName("assoc")) + }) + apiTeamAssociationsRoutes.DELETE("", func(c *gin.Context) { + err := pki.DeleteTeamAssociation(TeamsDir, c.Params.ByName("assoc")) + if err != nil { + log.Printf("Unable to DeleteTeamAssociation(%s): %s", c.Params.ByName("assoc"), err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete association symlink: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, nil) + }) + +} + +func CertificateHandler(c *gin.Context) { + var certid uint64 + var err error + + cid := strings.TrimSuffix(string(c.Params.ByName("certid")), ".p12") + if certid, err = strconv.ParseUint(cid, 10, 64); err != nil { + if certid, err = strconv.ParseUint(cid, 16, 64); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid certficate identifier"}) + return + } + } + + cert, err := fic.GetCertificate(certid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Certificate not found"}) + return + } + + c.Set("cert", cert) + + c.Next() } func genHtpasswd(ssha bool) (ret string, err error) { @@ -187,13 +274,14 @@ type PKISettings struct { PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"publicKeyAlgorithm"` } -func infoCA(_ httprouter.Params, _ []byte) (interface{}, error) { +func infoCA(c *gin.Context) { _, cacert, err := pki.LoadCA() if err != nil { - return nil, err + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "CA not found"}) + return } - return PKISettings{ + c.JSON(http.StatusOK, PKISettings{ Version: cacert.Version, SerialNumber: cacert.SerialNumber, Issuer: cacert.Issuer, @@ -202,47 +290,78 @@ func infoCA(_ httprouter.Params, _ []byte) (interface{}, error) { NotAfter: cacert.NotAfter, SignatureAlgorithm: cacert.SignatureAlgorithm, PublicKeyAlgorithm: cacert.PublicKeyAlgorithm, - }, nil + }) } -func getCAPEM(_ httprouter.Params, _ []byte) (interface{}, error) { +func getCAPEM(c *gin.Context) { if _, err := os.Stat(pki.CACertPath()); os.IsNotExist(err) { - return nil, errors.New("Unable to locate the CA root certificate. Have you generated it?") + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Unable to locate the CA root certificate. Have you generated it?"}) + return } else if fd, err := os.Open(pki.CACertPath()); err != nil { - return nil, err + log.Println("Unable to open CA root certificate:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return } else { defer fd.Close() - return ioutil.ReadAll(fd) + + cnt, err := ioutil.ReadAll(fd) + if err != nil { + log.Println("Unable to read CA root certificate:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.String(http.StatusOK, string(cnt)) } } -func getTeamP12File(cert *fic.Certificate, _ []byte) (interface{}, error) { +func getTeamP12File(c *gin.Context) { + cert := c.MustGet("cert").(*fic.Certificate) + // Create p12 if necessary if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) { if err := pki.WriteP12(cert.Id, cert.Password); err != nil { - return nil, err + log.Println("Unable to WriteP12:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, err) + return } } if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) { - return nil, errors.New("Unable to locate the p12. Have you generated it?") + log.Println("Unable to compute ClientP12Path:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, errors.New("Unable to locate the p12. Have you generated it?")) + return } else if fd, err := os.Open(pki.ClientP12Path(cert.Id)); err != nil { - return nil, err + log.Println("Unable to open ClientP12Path:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err.Error())) + return } else { defer fd.Close() - return ioutil.ReadAll(fd) + + data, err := ioutil.ReadAll(fd) + if err != nil { + log.Println("Unable to open ClientP12Path:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err.Error())) + return + } + + c.Data(http.StatusOK, "application/x-pkcs12", data) } } -func generateClientCert(_ httprouter.Params, _ []byte) (interface{}, error) { +func generateClientCert(c *gin.Context) { // First, generate a new, unique, serial var serial_gen [8]byte if _, err := rand.Read(serial_gen[:]); err != nil { - return nil, err + log.Println("Unable to read enough entropy to generate client certificate:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"}) + return } for fic.ExistingCertSerial(serial_gen) { if _, err := rand.Read(serial_gen[:]); err != nil { - return nil, err + log.Println("Unable to read enough entropy to generate client certificate:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"}) + return } } @@ -253,23 +372,35 @@ func generateClientCert(_ httprouter.Params, _ []byte) (interface{}, error) { // Let's pick a random password password, err := fic.GeneratePassword() if err != nil { - return nil, err + log.Println("Unable to generate password:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate password: " + err.Error()}) + return } // Ok, now load CA capriv, cacert, err := pki.LoadCA() if err != nil { - return nil, err + log.Println("Unable to load the CA:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to load the CA"}) + return } // Generate our privkey if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil { - return nil, err + log.Println("Unable to generate private key:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate private key: " + err.Error()}) + return } // Save in DB cert, err := fic.RegisterCertificate(serial, password) - return CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, nil, cert.Revoked}, err + if err != nil { + log.Println("Unable to register certificate:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register certificate."}) + return + } + + c.JSON(http.StatusOK, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, nil, cert.Revoked}) } type CertExported struct { @@ -280,35 +411,42 @@ type CertExported struct { Revoked *time.Time `json:"revoked"` } -func getCertificates(_ httprouter.Params, _ []byte) (interface{}, error) { - if certificates, err := fic.GetCertificates(); err != nil { - return nil, err - } else { - ret := make([]CertExported, 0) - for _, cert := range certificates { - dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id)) - - var idTeam *int64 = nil - if lnk, err := os.Readlink(dstLinkPath); err == nil { - if tid, err := strconv.ParseInt(lnk, 10, 64); err == nil { - idTeam = &tid - } - } - - ret = append(ret, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, "", idTeam, cert.Revoked}) - } - return ret, nil +func getCertificates(c *gin.Context) { + certificates, err := fic.GetCertificates() + if err != nil { + log.Println("Unable to retrieve certificates list:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during certificates retrieval."}) + return } + ret := make([]CertExported, 0) + for _, cert := range certificates { + dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id)) + + var idTeam *int64 = nil + if lnk, err := os.Readlink(dstLinkPath); err == nil { + if tid, err := strconv.ParseInt(lnk, 10, 64); err == nil { + idTeam = &tid + } + } + + ret = append(ret, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, "", idTeam, cert.Revoked}) + } + + c.JSON(http.StatusOK, ret) } type CertUploaded struct { Team *int64 `json:"id_team"` } -func updateCertificateAssociation(cert *fic.Certificate, body []byte) (interface{}, error) { +func updateCertificateAssociation(c *gin.Context) { + cert := c.MustGet("cert").(*fic.Certificate) + var uc CertUploaded - if err := json.Unmarshal(body, &uc); err != nil { - return nil, err + err := c.ShouldBindJSON(&uc) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id)) @@ -316,19 +454,26 @@ func updateCertificateAssociation(cert *fic.Certificate, body []byte) (interface if uc.Team != nil { srcLinkPath := fmt.Sprintf("%d", *uc.Team) if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil { - return nil, err + log.Println("Unable to create certificate symlink:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create certificate symlink: %s", err.Error())}) + return } // Mark team as active to ensure it'll be generated if ut, err := fic.GetTeam(*uc.Team); err != nil { - return nil, err + log.Println("Unable to GetTeam:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team retrieval."}) + return } else if !ut.Active { ut.Active = true - ut.Update() + _, err := ut.Update() + if err != nil { + log.Println("Unable to UpdateTeam after updateCertificateAssociation:", err.Error()) + } } } else { os.Remove(dstLinkPath) } - return cert, nil + c.JSON(http.StatusOK, cert) } diff --git a/admin/api/claim.go b/admin/api/claim.go index 08f7b8d7..fd0bc89d 100644 --- a/admin/api/claim.go +++ b/admin/api/claim.go @@ -2,62 +2,152 @@ package api import ( "encoding/json" - "errors" "fmt" "io/ioutil" + "log" + "net/http" "path" + "strconv" "time" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/teams/:tid/issue.json", apiHandler(teamHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return team.MyIssueFile() - }))) - +func declareClaimsRoutes(router *gin.RouterGroup) { // Tasks - router.GET("/api/claims", apiHandler(getClaims)) - router.POST("/api/claims", apiHandler(newClaim)) - router.DELETE("/api/claims", apiHandler(clearClaims)) - router.GET("/api/teams/:tid/claims", apiHandler(teamHandler(getTeamClaims))) - router.GET("/api/exercices/:eid/claims", apiHandler(exerciceHandler(getExerciceClaims))) - router.GET("/api/themes/:thid/exercices/:eid/claims", apiHandler(exerciceHandler(getExerciceClaims))) + router.GET("/claims", getClaims) + router.POST("/claims", newClaim) + router.DELETE("/claims", clearClaims) - router.GET("/api/claims/:cid", apiHandler(claimHandler(showClaim))) - router.PUT("/api/claims/:cid", apiHandler(claimHandler(updateClaim))) - router.POST("/api/claims/:cid", apiHandler(claimHandler(addClaimDescription))) - router.DELETE("/api/claims/:cid", apiHandler(claimHandler(deleteClaim))) + apiClaimsRoutes := router.Group("/claims/:cid") + apiClaimsRoutes.Use(ClaimHandler) + apiClaimsRoutes.GET("", showClaim) + apiClaimsRoutes.PUT("", updateClaim) + apiClaimsRoutes.POST("", addClaimDescription) + apiClaimsRoutes.DELETE("", deleteClaim) - router.GET("/api/claims/:cid/last_update", apiHandler(claimHandler(getClaimLastUpdate))) - router.PUT("/api/claims/:cid/descriptions", apiHandler(claimHandler(updateClaimDescription))) + apiClaimsRoutes.GET("/last_update", getClaimLastUpdate) + apiClaimsRoutes.PUT("/descriptions", updateClaimDescription) // Assignees - router.GET("/api/claims-assignees", apiHandler(getAssignees)) - router.POST("/api/claims-assignees", apiHandler(newAssignee)) + router.GET("/claims-assignees", getAssignees) + router.POST("/claims-assignees", newAssignee) - router.GET("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(showClaimAssignee))) - router.PUT("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(updateClaimAssignee))) - router.DELETE("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(deleteClaimAssignee))) + apiClaimAssigneesRoutes := router.Group("/claims-assignees/:aid") + apiClaimAssigneesRoutes.Use(ClaimAssigneeHandler) + router.GET("/claims-assignees/:aid", showClaimAssignee) + router.PUT("/claims-assignees/:aid", updateClaimAssignee) + router.DELETE("/claims-assignees/:aid", deleteClaimAssignee) } -func getClaims(_ httprouter.Params, _ []byte) (interface{}, error) { - return fic.GetClaims() +func declareExerciceClaimsRoutes(router *gin.RouterGroup) { + router.GET("/claims", getExerciceClaims) } -func getTeamClaims(team *fic.Team, _ []byte) (interface{}, error) { - return team.GetClaims() +func declareTeamClaimsRoutes(router *gin.RouterGroup) { + router.GET("/api/teams/:tid/issue.json", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + issues, err := team.MyIssueFile() + if err != nil { + log.Printf("Unable to MyIssueFile(tid=%d): %s", team.Id, err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate issues.json."}) + return + } + + c.JSON(http.StatusOK, issues) + }) + + router.GET("/claims", getTeamClaims) } -func getExerciceClaims(exercice *fic.Exercice, _ []byte) (interface{}, error) { - return exercice.GetClaims() +func ClaimHandler(c *gin.Context) { + cid, err := strconv.ParseInt(string(c.Params.ByName("cid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid claim identifier"}) + return + } + + claim, err := fic.GetClaim(cid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Requested claim not found"}) + return + } + + c.Set("claim", claim) + + c.Next() } -func getClaimLastUpdate(claim *fic.Claim, _ []byte) (interface{}, error) { - return claim.GetLastUpdate() +func ClaimAssigneeHandler(c *gin.Context) { + aid, err := strconv.ParseInt(string(c.Params.ByName("aid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid claim assignee identifier"}) + return + } + + assignee, err := fic.GetAssignee(aid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Requested claim-assignee not found"}) + return + } + + c.Set("claim-assignee", assignee) + + c.Next() +} + +func getClaims(c *gin.Context) { + claims, err := fic.GetClaims() + + if err != nil { + log.Println("Unable to getClaims:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claims retrieval."}) + return + } + + c.JSON(http.StatusOK, claims) +} + +func getTeamClaims(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + claims, err := team.GetClaims() + if err != nil { + log.Printf("Unable to GetClaims(tid=%d): %s", team.Id, err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve claim list."}) + return + } + + c.JSON(http.StatusOK, claims) +} + +func getExerciceClaims(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + claims, err := exercice.GetClaims() + if err != nil { + log.Printf("Unable to GetClaims(eid=%d): %s", exercice.Id, err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve claim list."}) + return + } + + c.JSON(http.StatusOK, claims) +} + +func getClaimLastUpdate(c *gin.Context) { + claim := c.MustGet("claim").(*fic.Claim) + + v, err := claim.GetLastUpdate() + if err != nil { + log.Printf("Unable to GetLastUpdate: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim last update retrieval."}) + return + } + + c.JSON(http.StatusOK, v) } type ClaimExported struct { @@ -76,20 +166,26 @@ type ClaimExported struct { Descriptions []*fic.ClaimDescription `json:"descriptions"` } -func showClaim(claim *fic.Claim, _ []byte) (interface{}, error) { +func showClaim(c *gin.Context) { + claim := c.MustGet("claim").(*fic.Claim) + var e ClaimExported var err error if e.Team, err = claim.GetTeam(); err != nil { - return nil, fmt.Errorf("Unable to find associated team: %w", err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated team: %s", err.Error())}) + return } if e.Exercice, err = claim.GetExercice(); err != nil { - return nil, fmt.Errorf("Unable to find associated exercice: %w", err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated exercice: %s", err.Error())}) + return } if e.Assignee, err = claim.GetAssignee(); err != nil { - return nil, fmt.Errorf("Unable to find associated assignee: %w", err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated assignee: %s", err.Error())}) + return } if e.Descriptions, err = claim.GetDescriptions(); err != nil { - return nil, fmt.Errorf("Unable to find claim's descriptions: %w", err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find claim's descriptions: %s", err.Error())}) + return } e.LastUpdate = e.Creation @@ -107,7 +203,8 @@ func showClaim(claim *fic.Claim, _ []byte) (interface{}, error) { e.Creation = claim.Creation e.State = claim.State e.Priority = claim.Priority - return e, nil + + c.JSON(http.StatusOK, e) } type ClaimUploaded struct { @@ -115,20 +212,24 @@ type ClaimUploaded struct { Whoami *int64 `json:"whoami"` } -func newClaim(_ httprouter.Params, body []byte) (interface{}, error) { +func newClaim(c *gin.Context) { var uc ClaimUploaded - if err := json.Unmarshal(body, &uc); err != nil { - return nil, fmt.Errorf("Unable to decode JSON: %w", err) + err := c.ShouldBindJSON(&uc) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if uc.Subject == "" { - return nil, errors.New("Claim's subject cannot be empty.") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Claim's subject cannot be empty."}) + return } var t *fic.Team if uc.IdTeam != nil { if team, err := fic.GetTeam(*uc.IdTeam); err != nil { - return nil, fmt.Errorf("Unable to get associated team: %w", err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated team: %s", err.Error())}) + return } else { t = team } @@ -139,7 +240,8 @@ func newClaim(_ httprouter.Params, body []byte) (interface{}, error) { var e *fic.Exercice if uc.IdExercice != nil { if exercice, err := fic.GetExercice(*uc.IdExercice); err != nil { - return nil, fmt.Errorf("Unable to get associated exercice: %w", err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated exercice: %s", err.Error())}) + return } else { e = exercice } @@ -150,7 +252,8 @@ func newClaim(_ httprouter.Params, body []byte) (interface{}, error) { var a *fic.ClaimAssignee if uc.IdAssignee != nil { if assignee, err := fic.GetAssignee(*uc.IdAssignee); err != nil { - return nil, fmt.Errorf("Unable to get associated assignee: %w", err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated assignee: %s", err.Error())}) + return } else { a = assignee } @@ -162,11 +265,25 @@ func newClaim(_ httprouter.Params, body []byte) (interface{}, error) { uc.Priority = "medium" } - return fic.NewClaim(uc.Subject, t, e, a, uc.Priority) + claim, err := fic.NewClaim(uc.Subject, t, e, a, uc.Priority) + if err != nil { + log.Println("Unable to newClaim:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register new claim"}) + return + } + + c.JSON(http.StatusOK, claim) } -func clearClaims(_ httprouter.Params, _ []byte) (interface{}, error) { - return fic.ClearClaims() +func clearClaims(c *gin.Context) { + nb, err := fic.ClearClaims() + if err != nil { + log.Printf("Unable to clearClaims: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claims clearing."}) + return + } + + c.JSON(http.StatusOK, nb) } func generateTeamIssuesFile(team fic.Team) error { @@ -180,122 +297,189 @@ func generateTeamIssuesFile(team fic.Team) error { return nil } -func addClaimDescription(claim *fic.Claim, body []byte) (interface{}, error) { +func addClaimDescription(c *gin.Context) { + claim := c.MustGet("claim").(*fic.Claim) + var ud fic.ClaimDescription - if err := json.Unmarshal(body, &ud); err != nil { - return nil, fmt.Errorf("Unable to decode JSON: %w", err) + err := c.ShouldBindJSON(&ud) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } - if assignee, err := fic.GetAssignee(ud.IdAssignee); err != nil { - return nil, fmt.Errorf("Unable to get associated assignee: %w", err) - } else if description, err := claim.AddDescription(ud.Content, assignee, ud.Publish); err != nil { - return nil, fmt.Errorf("Unable to add description: %w", err) - } else { - if team, _ := claim.GetTeam(); team != nil { - err = generateTeamIssuesFile(*team) + assignee, err := fic.GetAssignee(ud.IdAssignee) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated assignee: %s", err.Error())}) + return + } + + description, err := claim.AddDescription(ud.Content, assignee, ud.Publish) + if err != nil { + log.Println("Unable to addClaimDescription:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to add description"}) + return + } + + if team, _ := claim.GetTeam(); team != nil { + err = generateTeamIssuesFile(*team) + if err != nil { + log.Println("Unable to generateTeamIssuesFile after addClaimDescription:", err.Error()) } - - return description, err } + + c.JSON(http.StatusOK, description) } -func updateClaimDescription(claim *fic.Claim, body []byte) (interface{}, error) { +func updateClaimDescription(c *gin.Context) { + claim := c.MustGet("claim").(*fic.Claim) + var ud fic.ClaimDescription - if err := json.Unmarshal(body, &ud); err != nil { - return nil, fmt.Errorf("Unable to decode JSON: %w", err) + err := c.ShouldBindJSON(&ud) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if _, err := ud.Update(); err != nil { - return nil, fmt.Errorf("Unable to update description: %w", err) - } else { - if team, _ := claim.GetTeam(); team != nil { - err = generateTeamIssuesFile(*team) - } - - return ud, err + log.Println("Unable to updateClaimDescription:", err.Error()) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during claim description updating."}) + return } + if team, _ := claim.GetTeam(); team != nil { + err = generateTeamIssuesFile(*team) + if err != nil { + log.Println("Unable to generateTeamIssuesFile:", err.Error()) + } + } + + c.JSON(http.StatusOK, ud) } -func updateClaim(claim *fic.Claim, body []byte) (interface{}, error) { +func updateClaim(c *gin.Context) { + claim := c.MustGet("claim").(*fic.Claim) + var uc ClaimUploaded - if err := json.Unmarshal(body, &uc); err != nil { - return nil, fmt.Errorf("Unable to decode JSON: %w", err) + err := c.ShouldBindJSON(&uc) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } uc.Id = claim.Id - if _, err := uc.Update(); err != nil { - return nil, fmt.Errorf("Unable to update claim: %w", err) - } else { - if claim.State != uc.State { - if uc.Whoami != nil { - if assignee, err := fic.GetAssignee(*uc.Whoami); err == nil { - claim.AddDescription(fmt.Sprintf("%s a changé l'état de la tâche vers %q (était %q).", assignee.Name, uc.State, claim.State), assignee, true) - } + _, err = uc.Update() + if err != nil { + log.Printf("Unable to updateClaim: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim update."}) + return + } + + if claim.State != uc.State { + if uc.Whoami != nil { + if assignee, err := fic.GetAssignee(*uc.Whoami); err == nil { + claim.AddDescription(fmt.Sprintf("%s a changé l'état de la tâche vers %q (était %q).", assignee.Name, uc.State, claim.State), assignee, true) } } + } - if claim.IdAssignee != uc.IdAssignee { - if uc.Whoami != nil { - if whoami, err := fic.GetAssignee(*uc.Whoami); err == nil { - if uc.IdAssignee != nil { - if assignee, err := fic.GetAssignee(*uc.IdAssignee); err == nil { - if assignee.Id != whoami.Id { - claim.AddDescription(fmt.Sprintf("%s a assigné la tâche à %s.", whoami.Name, assignee.Name), whoami, false) - } else { - claim.AddDescription(fmt.Sprintf("%s s'est assigné la tâche.", assignee.Name), whoami, false) - } + if claim.IdAssignee != uc.IdAssignee { + if uc.Whoami != nil { + if whoami, err := fic.GetAssignee(*uc.Whoami); err == nil { + if uc.IdAssignee != nil { + if assignee, err := fic.GetAssignee(*uc.IdAssignee); err == nil { + if assignee.Id != whoami.Id { + claim.AddDescription(fmt.Sprintf("%s a assigné la tâche à %s.", whoami.Name, assignee.Name), whoami, false) + } else { + claim.AddDescription(fmt.Sprintf("%s s'est assigné la tâche.", assignee.Name), whoami, false) } - } else { - claim.AddDescription(fmt.Sprintf("%s a retiré l'attribution de la tâche.", whoami.Name), whoami, false) } + } else { + claim.AddDescription(fmt.Sprintf("%s a retiré l'attribution de la tâche.", whoami.Name), whoami, false) } } } + } - if team, _ := claim.GetTeam(); team != nil { - err = generateTeamIssuesFile(*team) - } + if team, _ := claim.GetTeam(); team != nil { + err = generateTeamIssuesFile(*team) + } - return uc, err + c.JSON(http.StatusOK, uc) +} + +func deleteClaim(c *gin.Context) { + claim := c.MustGet("claim").(*fic.Claim) + + if nb, err := claim.Delete(); err != nil { + log.Println("Unable to deleteClaim:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim deletion."}) + return + } else { + c.JSON(http.StatusOK, nb) } } -func deleteClaim(claim *fic.Claim, _ []byte) (interface{}, error) { - return claim.Delete() -} - -func getAssignees(_ httprouter.Params, _ []byte) (interface{}, error) { - return fic.GetAssignees() -} - -func showClaimAssignee(assignee *fic.ClaimAssignee, _ []byte) (interface{}, error) { - return assignee, nil -} -func newAssignee(_ httprouter.Params, body []byte) (interface{}, error) { - var ua fic.ClaimAssignee - if err := json.Unmarshal(body, &ua); err != nil { - return nil, fmt.Errorf("Unable to decode JSON: %w", err) +func getAssignees(c *gin.Context) { + assignees, err := fic.GetAssignees() + if err != nil { + log.Println("Unable to getAssignees:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during assignees retrieval."}) + return } - return fic.NewClaimAssignee(ua.Name) + c.JSON(http.StatusOK, assignees) } -func updateClaimAssignee(assignee *fic.ClaimAssignee, body []byte) (interface{}, error) { +func showClaimAssignee(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("claim-assignee").(*fic.ClaimAssignee)) +} +func newAssignee(c *gin.Context) { var ua fic.ClaimAssignee - if err := json.Unmarshal(body, &ua); err != nil { - return nil, fmt.Errorf("Unable to decode JSON: %w", err) + err := c.ShouldBindJSON(&ua) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + assignee, err := fic.NewClaimAssignee(ua.Name) + if err != nil { + log.Println("Unable to newAssignee:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during assignee creation."}) + return + } + + c.JSON(http.StatusOK, assignee) +} + +func updateClaimAssignee(c *gin.Context) { + assignee := c.MustGet("claim-assignee").(*fic.ClaimAssignee) + + var ua fic.ClaimAssignee + err := c.ShouldBindJSON(&ua) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } ua.Id = assignee.Id if _, err := ua.Update(); err != nil { - return nil, fmt.Errorf("Unable to update claim assignee: %w", err) - } else { - return ua, nil + log.Println("Unable to updateClaimAssignee:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim assignee update."}) + return } + + c.JSON(http.StatusOK, ua) } -func deleteClaimAssignee(assignee *fic.ClaimAssignee, _ []byte) (interface{}, error) { - return assignee.Delete() +func deleteClaimAssignee(c *gin.Context) { + assignee := c.MustGet("claim-assignee").(*fic.ClaimAssignee) + + if _, err := assignee.Delete(); err != nil { + log.Println("Unable to deleteClaimAssignee:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during claim assignee deletion: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, true) } diff --git a/admin/api/events.go b/admin/api/events.go index 0c35c6b3..17500535 100644 --- a/admin/api/events.go +++ b/admin/api/events.go @@ -3,22 +3,45 @@ package api import ( "encoding/json" "io/ioutil" + "log" + "net/http" "path" + "strconv" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/events/", apiHandler(getEvents)) - router.GET("/api/events.json", apiHandler(getLastEvents)) - router.POST("/api/events/", apiHandler(newEvent)) - router.DELETE("/api/events/", apiHandler(clearEvents)) +func declareEventsRoutes(router *gin.RouterGroup) { + router.GET("/events", getEvents) + router.GET("/events.json", getLastEvents) + router.POST("/events", newEvent) + router.DELETE("/events", clearEvents) - router.GET("/api/events/:evid", apiHandler(eventHandler(showEvent))) - router.PUT("/api/events/:evid", apiHandler(eventHandler(updateEvent))) - router.DELETE("/api/events/:evid", apiHandler(eventHandler(deleteEvent))) + apiEventsRoutes := router.Group("/events/:evid") + apiEventsRoutes.Use(EventHandler) + apiEventsRoutes.GET("", showEvent) + apiEventsRoutes.PUT("", updateEvent) + apiEventsRoutes.DELETE("", deleteEvent) +} + +func EventHandler(c *gin.Context) { + evid, err := strconv.ParseInt(string(c.Params.ByName("evid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid event identifier"}) + return + } + + event, err := fic.GetEvent(evid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Event not found"}) + return + } + + c.Set("event", event) + + c.Next() } func genEventsFile() error { @@ -33,65 +56,99 @@ func genEventsFile() error { return nil } -func getEvents(_ httprouter.Params, _ []byte) (interface{}, error) { - if evts, err := fic.GetEvents(); err != nil { - return nil, err - } else { - return evts, nil +func getEvents(c *gin.Context) { + evts, err := fic.GetEvents() + if err != nil { + log.Println("Unable to GetEvents:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve events list"}) + return } + + c.JSON(http.StatusOK, evts) } -func getLastEvents(_ httprouter.Params, _ []byte) (interface{}, error) { - if evts, err := fic.GetLastEvents(); err != nil { - return nil, err - } else { - return evts, nil +func getLastEvents(c *gin.Context) { + evts, err := fic.GetLastEvents() + + if err != nil { + log.Println("Unable to GetLastEvents:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve last events list"}) + return } + + c.JSON(http.StatusOK, evts) } -func showEvent(event *fic.Event, _ []byte) (interface{}, error) { - return event, nil -} - -func newEvent(_ httprouter.Params, body []byte) (interface{}, error) { +func newEvent(c *gin.Context) { var ue fic.Event - if err := json.Unmarshal(body, &ue); err != nil { - return nil, err + err := c.ShouldBindJSON(&ue) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } - if event, err := fic.NewEvent(ue.Text, ue.Kind); err != nil { - return nil, err - } else { - genEventsFile() - return event, nil + event, err := fic.NewEvent(ue.Text, ue.Kind) + if err != nil { + log.Printf("Unable to newEvent: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event creation."}) + return } + + genEventsFile() + + c.JSON(http.StatusOK, event) } -func clearEvents(_ httprouter.Params, _ []byte) (interface{}, error) { - return fic.ClearEvents() +func clearEvents(c *gin.Context) { + nb, err := fic.ClearEvents() + if err != nil { + log.Printf("Unable to clearEvent: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event clearing."}) + return + } + + c.JSON(http.StatusOK, nb) } -func updateEvent(event *fic.Event, body []byte) (interface{}, error) { +func showEvent(c *gin.Context) { + event := c.MustGet("event").(*fic.Event) + c.JSON(http.StatusOK, event) +} + +func updateEvent(c *gin.Context) { + event := c.MustGet("event").(*fic.Event) + var ue fic.Event - if err := json.Unmarshal(body, &ue); err != nil { - return nil, err + err := c.ShouldBindJSON(&ue) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } ue.Id = event.Id if _, err := ue.Update(); err != nil { - return nil, err - } else { - genEventsFile() - return ue, nil + log.Printf("Unable to updateEvent: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event update."}) + return } + + genEventsFile() + + c.JSON(http.StatusOK, ue) } -func deleteEvent(event *fic.Event, _ []byte) (interface{}, error) { - if i, err := event.Delete(); err != nil { - return i, err - } else { - genEventsFile() - return i, err +func deleteEvent(c *gin.Context) { + event := c.MustGet("event").(*fic.Event) + + _, err := event.Delete() + if err != nil { + log.Printf("Unable to deleteEvent: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event deletion."}) + return } + + genEventsFile() + + c.JSON(http.StatusOK, true) } diff --git a/admin/api/exercice.go b/admin/api/exercice.go index c53dc931..e7b98c43 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -1,121 +1,254 @@ package api import ( - "encoding/json" - "errors" "fmt" + "log" + "net/http" + "strconv" "strings" "time" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/exercices/", apiHandler(listExercices)) - router.GET("/api/resolutions.json", apiHandler(exportResolutionMovies)) - - router.GET("/api/exercices/:eid", apiHandler(exerciceHandler(showExercice))) - router.PUT("/api/exercices/:eid", apiHandler(exerciceHandler(updateExercice))) - router.PATCH("/api/exercices/:eid", apiHandler(exerciceHandler(partUpdateExercice))) - router.DELETE("/api/exercices/:eid", apiHandler(exerciceHandler(deleteExercice))) - - router.GET("/api/exercices/:eid/stats.json", apiHandler(exerciceHandler(getExerciceStats))) - router.GET("/api/exercices_stats.json", apiHandler(getExercicesStats)) - - router.GET("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(getExerciceHistory))) - router.PATCH("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(updateExerciceHistory))) - router.DELETE("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(delExerciceHistory))) - - router.GET("/api/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints))) - router.POST("/api/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint))) - router.GET("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(showExerciceHint))) - router.PUT("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(updateExerciceHint))) - router.DELETE("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(deleteExerciceHint))) - router.GET("/api/exercices/:eid/hints/:hid/dependancies", apiHandler(hintHandler(showExerciceHintDeps))) - - router.GET("/api/exercices/:eid/flags", apiHandler(exerciceHandler(listExerciceFlags))) - router.POST("/api/exercices/:eid/flags", apiHandler(exerciceHandler(createExerciceFlag))) - router.GET("/api/exercices/:eid/flags/:kid", apiHandler(flagKeyHandler(showExerciceFlag))) - router.PUT("/api/exercices/:eid/flags/:kid", apiHandler(flagKeyHandler(updateExerciceFlag))) - router.POST("/api/exercices/:eid/flags/:kid/try", apiHandler(flagKeyHandler(tryExerciceFlag))) - router.DELETE("/api/exercices/:eid/flags/:kid", apiHandler(flagKeyHandler(deleteExerciceFlag))) - router.GET("/api/exercices/:eid/flags/:kid/dependancies", apiHandler(flagKeyHandler(showExerciceFlagDeps))) - router.GET("/api/exercices/:eid/flags/:kid/choices/", apiHandler(flagKeyHandler(listFlagChoices))) - router.GET("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(showFlagChoice))) - router.POST("/api/exercices/:eid/flags/:kid/choices/", apiHandler(flagKeyHandler(createFlagChoice))) - router.PUT("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(updateFlagChoice))) - router.DELETE("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(deleteFlagChoice))) - - router.GET("/api/exercices/:eid/quiz", apiHandler(exerciceHandler(listExerciceQuiz))) - router.GET("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(showExerciceQuiz))) - router.PUT("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(updateExerciceQuiz))) - router.DELETE("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(deleteExerciceQuiz))) - router.GET("/api/exercices/:eid/quiz/:qid/dependancies", apiHandler(quizHandler(showExerciceQuizDeps))) - - router.GET("/api/exercices/:eid/tags", apiHandler(exerciceHandler(listExerciceTags))) - router.POST("/api/exercices/:eid/tags", apiHandler(exerciceHandler(addExerciceTag))) - router.PUT("/api/exercices/:eid/tags", apiHandler(exerciceHandler(updateExerciceTags))) - - // Remote - router.GET("/api/remote/themes/:thid/exercices/:exid", apiHandler(sync.ApiGetRemoteExercice)) - router.GET("/api/remote/themes/:thid/exercices/:exid/hints", apiHandler(sync.ApiGetRemoteExerciceHints)) - router.GET("/api/remote/themes/:thid/exercices/:exid/flags", apiHandler(sync.ApiGetRemoteExerciceFlags)) - - // Synchronize - router.POST("/api/sync/themes/:thid/exercices/:eid", apiHandler(themedExerciceHandler( - func(theme *fic.Theme, exercice *fic.Exercice, _ []byte) (interface{}, error) { - _, _, errs := sync.SyncExercice(sync.GlobalImporter, theme, exercice.Path, nil) - return errs, nil - }))) - router.POST("/api/sync/exercices/:eid/hints", apiHandler(exerciceHandler( - func(exercice *fic.Exercice, _ []byte) (interface{}, error) { - _, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice)) - return errs, nil - }))) - router.POST("/api/sync/exercices/:eid/flags", apiHandler(exerciceHandler( - func(exercice *fic.Exercice, _ []byte) (interface{}, error) { - _, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice) - _, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice)) - return append(errs, herrs...), nil - }))) - - router.POST("/api/sync/exercices/:eid/fixurlid", apiHandler(exerciceHandler( - func(exercice *fic.Exercice, _ []byte) (interface{}, error) { - if exercice.FixURLId() { - return exercice.Update() - } - return 0, nil - }))) +func declareGlobalExercicesRoutes(router *gin.RouterGroup) { + router.GET("/resolutions.json", exportResolutionMovies) + router.GET("/exercices_stats.json", getExercicesStats) } -func listExercices(_ httprouter.Params, body []byte) (interface{}, error) { - // List all exercices - return fic.GetExercices() +func declareExercicesRoutes(router *gin.RouterGroup) { + router.GET("/exercices", listExercices) + + apiExercicesRoutes := router.Group("/exercices/:eid") + apiExercicesRoutes.Use(ExerciceHandler) + apiExercicesRoutes.GET("", showExercice) + apiExercicesRoutes.PUT("", updateExercice) + apiExercicesRoutes.PATCH("", partUpdateExercice) + apiExercicesRoutes.DELETE("", deleteExercice) + + apiExercicesRoutes.GET("/stats.json", getExerciceStats) + + apiExercicesRoutes.GET("/history.json", getExerciceHistory) + apiExercicesRoutes.PATCH("/history.json", updateExerciceHistory) + apiExercicesRoutes.DELETE("/history.json", delExerciceHistory) + + apiExercicesRoutes.GET("/hints", listExerciceHints) + apiExercicesRoutes.POST("/hints", createExerciceHint) + + apiHintsRoutes := apiExercicesRoutes.Group("/hints/:hid") + apiHintsRoutes.Use(HintHandler) + apiHintsRoutes.GET("", showExerciceHint) + apiHintsRoutes.PUT("", updateExerciceHint) + apiHintsRoutes.DELETE("", deleteExerciceHint) + apiHintsRoutes.GET("/dependancies", showExerciceHintDeps) + + apiExercicesRoutes.GET("/flags", listExerciceFlags) + apiExercicesRoutes.POST("/flags", createExerciceFlag) + + apiFlagsRoutes := apiExercicesRoutes.Group("/flags/:kid") + apiFlagsRoutes.Use(FlagKeyHandler) + apiFlagsRoutes.GET("", showExerciceFlag) + apiFlagsRoutes.PUT("", updateExerciceFlag) + apiFlagsRoutes.POST("/try", tryExerciceFlag) + apiFlagsRoutes.DELETE("/", deleteExerciceFlag) + apiFlagsRoutes.GET("/dependancies", showExerciceFlagDeps) + apiFlagsRoutes.GET("/choices/", listFlagChoices) + apiFlagsChoicesRoutes := apiExercicesRoutes.Group("/choices/:cid") + apiFlagsChoicesRoutes.Use(FlagChoiceHandler) + apiFlagsChoicesRoutes.GET("", showFlagChoice) + apiFlagsRoutes.POST("/choices/", createFlagChoice) + apiFlagsChoicesRoutes.PUT("", updateFlagChoice) + apiFlagsChoicesRoutes.DELETE("", deleteFlagChoice) + + apiQuizRoutes := apiExercicesRoutes.Group("/quiz/:qid") + apiQuizRoutes.Use(FlagQuizHandler) + apiExercicesRoutes.GET("/quiz", listExerciceQuiz) + apiQuizRoutes.GET("", showExerciceQuiz) + apiQuizRoutes.PUT("", updateExerciceQuiz) + apiQuizRoutes.DELETE("", deleteExerciceQuiz) + apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps) + + apiExercicesRoutes.GET("/tags", listExerciceTags) + apiExercicesRoutes.POST("/tags", addExerciceTag) + apiExercicesRoutes.PUT("/tags", updateExerciceTags) + + declareFilesRoutes(apiExercicesRoutes) + declareExerciceClaimsRoutes(apiExercicesRoutes) + + // Remote + router.GET("/remote/themes/:thid/exercices/:exid", sync.ApiGetRemoteExercice) + router.GET("/remote/themes/:thid/exercices/:exid/hints", sync.ApiGetRemoteExerciceHints) + router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags) +} + +func ExerciceHandler(c *gin.Context) { + eid, err := strconv.ParseInt(string(c.Params.ByName("eid")), 10, 32) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid exercice identifier"}) + return + } + + var exercice *fic.Exercice + if theme, exists := c.Get("theme"); exists { + exercice, err = theme.(*fic.Theme).GetExercice(int(eid)) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Exercice not found"}) + return + } + } else { + exercice, err = fic.GetExercice(eid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Exercice not found"}) + return + } + } + + c.Set("exercice", exercice) + + c.Next() +} + +func HintHandler(c *gin.Context) { + hid, err := strconv.ParseInt(string(c.Params.ByName("hid")), 10, 32) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid hint identifier"}) + return + } + + exercice := c.MustGet("exercice").(*fic.Exercice) + hint, err := exercice.GetHint(hid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Hint not found"}) + return + } + + c.Set("hint", hint) + + c.Next() +} + +func FlagKeyHandler(c *gin.Context) { + kid, err := strconv.ParseInt(string(c.Params.ByName("kid")), 10, 32) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid flag identifier"}) + return + } + + var flag *fic.FlagKey + if exercice, exists := c.Get("exercice"); exists { + flag, err = exercice.(*fic.Exercice).GetFlagKey(int(kid)) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Flag not found"}) + return + } + } else { + flag, err = fic.GetFlagKey(int(kid)) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Flag not found"}) + return + } + } + + c.Set("flag-key", flag) + + c.Next() +} + +func FlagChoiceHandler(c *gin.Context) { + cid, err := strconv.ParseInt(string(c.Params.ByName("cid")), 10, 32) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid choice identifier"}) + return + } + + flagkey := c.MustGet("flag-key").(*fic.FlagKey) + choice, err := flagkey.GetChoice(int(cid)) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Choice not found"}) + return + } + + c.Set("flag-choice", choice) + + c.Next() +} + +func FlagQuizHandler(c *gin.Context) { + qid, err := strconv.ParseInt(string(c.Params.ByName("qid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid quiz identifier"}) + return + } + + var quiz *fic.MCQ + if exercice, exists := c.Get("exercice"); exists { + quiz, err = exercice.(*fic.Exercice).GetMCQById(int(qid)) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Quiz not found"}) + return + } + } else { + quiz, err = fic.GetMCQ(int(qid)) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Quiz not found"}) + return + } + } + + c.Set("flag-quiz", quiz) + + c.Next() +} + +func listExercices(c *gin.Context) { + if theme, exists := c.Get("theme"); exists { + exercices, err := theme.(*fic.Theme).GetExercices() + if err != nil { + log.Println("Unable to listThemedExercices:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercices listing."}) + return + } + + c.JSON(http.StatusOK, exercices) + } else { + exercices, err := fic.GetExercices() + if err != nil { + log.Println("Unable to listThemedExercices:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercices listing."}) + return + } + + c.JSON(http.StatusOK, exercices) + } } // Generate the csv to export with: // curl -s http://127.0.0.1:8081/api/resolutions.json | jq -r ".[] | [ .theme,.title, @uri \"https://fic.srs.epita.fr/resolution/\\(.videoURI)\" ] | join(\";\")" -func exportResolutionMovies(_ httprouter.Params, body []byte) (interface{}, error) { - if exercices, err := fic.GetExercices(); err != nil { - return nil, err - } else { - export := []map[string]string{} - for _, exercice := range exercices { - if theme, err := fic.GetTheme(exercice.IdTheme); err != nil { - return nil, err - } else { - export = append(export, map[string]string{ - "videoURI": exercice.VideoURI, - "theme": theme.Name, - "title": exercice.Title, - }) - } - } - return export, nil +func exportResolutionMovies(c *gin.Context) { + exercices, err := fic.GetExercices() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return } + + export := []map[string]string{} + for _, exercice := range exercices { + if theme, err := fic.GetTheme(exercice.IdTheme); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } else { + export = append(export, map[string]string{ + "videoURI": exercice.VideoURI, + "theme": theme.Name, + "title": exercice.Title, + }) + } + } + + c.JSON(http.StatusOK, export) } func loadFlags(n func() ([]fic.Flag, error)) (interface{}, error) { @@ -146,28 +279,73 @@ func loadFlags(n func() ([]fic.Flag, error)) (interface{}, error) { } } -func listExerciceHints(exercice *fic.Exercice, body []byte) (interface{}, error) { - return exercice.GetHints() +func listExerciceHints(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + hints, err := exercice.GetHints() + if err != nil { + log.Println("Unable to listExerciceHints:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving hints"}) + return + } + + c.JSON(http.StatusOK, hints) } -func listExerciceFlags(exercice *fic.Exercice, body []byte) (interface{}, error) { - return exercice.GetFlagKeys() +func listExerciceFlags(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + flags, err := exercice.GetFlagKeys() + if err != nil { + log.Println("Unable to listExerciceFlags:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice flags"}) + return + } + + c.JSON(http.StatusOK, flags) } -func listFlagChoices(flag *fic.FlagKey, _ *fic.Exercice, body []byte) (interface{}, error) { - return flag.GetChoices() +func listFlagChoices(c *gin.Context) { + flag := c.MustGet("flag-key").(*fic.FlagKey) + + choices, err := flag.GetChoices() + if err != nil { + log.Println("Unable to listFlagChoices:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag choices"}) + return + } + + c.JSON(http.StatusOK, choices) } -func listExerciceQuiz(exercice *fic.Exercice, body []byte) (interface{}, error) { - return exercice.GetMCQ() +func listExerciceQuiz(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + quiz, err := exercice.GetMCQ() + if err != nil { + log.Println("Unable to listExerciceQuiz:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving quiz list"}) + return + } + + c.JSON(http.StatusOK, quiz) } -func showExercice(exercice *fic.Exercice, body []byte) (interface{}, error) { - return exercice, nil +func showExercice(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("exercice").(*fic.Exercice)) } -func getExerciceHistory(exercice *fic.Exercice, body []byte) (interface{}, error) { - return exercice.GetHistory() +func getExerciceHistory(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + history, err := exercice.GetHistory() + if err != nil { + log.Println("Unable to getExerciceHistory:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"}) + return + } + + c.JSON(http.StatusOK, history) } type exerciceStats struct { @@ -179,33 +357,37 @@ type exerciceStats struct { MCQSolved []int64 `json:"mcq_solved"` } -func getExerciceStats(e *fic.Exercice, body []byte) (interface{}, error) { - return exerciceStats{ +func getExerciceStats(c *gin.Context) { + e := c.MustGet("exercice").(*fic.Exercice) + + c.JSON(http.StatusOK, exerciceStats{ TeamTries: e.TriedTeamCount(), TotalTries: e.TriedCount(), SolvedCount: e.SolvedCount(), FlagSolved: e.FlagSolved(), MCQSolved: e.MCQSolved(), - }, nil + }) } -func getExercicesStats(_ httprouter.Params, body []byte) (interface{}, error) { - if exercices, err := fic.GetExercices(); err != nil { - return nil, err - } else { - ret := []exerciceStats{} - for _, e := range exercices { - ret = append(ret, exerciceStats{ - IdExercice: e.Id, - TeamTries: e.TriedTeamCount(), - TotalTries: e.TriedCount(), - SolvedCount: e.SolvedCount(), - FlagSolved: e.FlagSolved(), - MCQSolved: e.MCQSolved(), - }) - } - return ret, nil +func getExercicesStats(c *gin.Context) { + exercices, err := fic.GetExercices() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) } + + ret := []exerciceStats{} + for _, e := range exercices { + ret = append(ret, exerciceStats{ + IdExercice: e.Id, + TeamTries: e.TriedTeamCount(), + TotalTries: e.TriedCount(), + SolvedCount: e.SolvedCount(), + FlagSolved: e.FlagSolved(), + MCQSolved: e.MCQSolved(), + }) + } + + c.JSON(http.StatusOK, ret) } type uploadedExerciceHistory struct { @@ -216,51 +398,93 @@ type uploadedExerciceHistory struct { Coeff float32 } -func updateExerciceHistory(exercice *fic.Exercice, body []byte) (interface{}, error) { +func updateExerciceHistory(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + var uh uploadedExerciceHistory - if err := json.Unmarshal(body, &uh); err != nil { - return nil, err + err := c.ShouldBindJSON(&uh) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } - return exercice.UpdateHistoryItem(uh.Coeff, uh.IdTeam, uh.Kind, uh.Time, uh.Secondary) -} - -func delExerciceHistory(exercice *fic.Exercice, body []byte) (interface{}, error) { - var uh uploadedExerciceHistory - if err := json.Unmarshal(body, &uh); err != nil { - return nil, err + _, err = exercice.UpdateHistoryItem(uh.Coeff, uh.IdTeam, uh.Kind, uh.Time, uh.Secondary) + if err != nil { + log.Println("Unable to updateExerciceHistory:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history update."}) + return } - return exercice.DelHistoryItem(uh.IdTeam, uh.Kind, uh.Time, uh.Secondary) + c.JSON(http.StatusOK, uh) } -func deleteExercice(exercice *fic.Exercice, _ []byte) (interface{}, error) { - return exercice.DeleteCascade() +func delExerciceHistory(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + var uh uploadedExerciceHistory + err := c.ShouldBindJSON(&uh) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + _, err = exercice.DelHistoryItem(uh.IdTeam, uh.Kind, uh.Time, uh.Secondary) + if err != nil { + log.Println("Unable to delExerciceHistory:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history deletion."}) + return + } + + c.JSON(http.StatusOK, true) } -func updateExercice(exercice *fic.Exercice, body []byte) (interface{}, error) { +func deleteExercice(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + _, err := exercice.DeleteCascade() + if err != nil { + log.Println("Unable to deleteExercice:", err.Error()) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during exercice deletion"}) + return + } + + c.JSON(http.StatusOK, true) +} + +func updateExercice(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + var ue fic.Exercice - if err := json.Unmarshal(body, &ue); err != nil { - return nil, err + err := c.ShouldBindJSON(&ue) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } ue.Id = exercice.Id if len(ue.Title) == 0 { - return nil, errors.New("Exercice's title not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Exercice's title not filled"}) + return } if _, err := ue.Update(); err != nil { - return nil, err + log.Println("Unable to updateExercice:", err.Error()) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during exercice update"}) + return } - return ue, nil + c.JSON(http.StatusOK, ue) } -func partUpdateExercice(exercice *fic.Exercice, body []byte) (interface{}, error) { +func partUpdateExercice(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + var ue fic.Exercice - if err := json.Unmarshal(body, &ue); err != nil { - return nil, err + err := c.ShouldBindJSON(&ue) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(ue.Title) > 0 { @@ -312,33 +536,49 @@ func partUpdateExercice(exercice *fic.Exercice, body []byte) (interface{}, error } if _, err := exercice.Update(); err != nil { - return nil, err + log.Println("Unable to partUpdateExercice:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice update."}) + return } - return exercice, nil + c.JSON(http.StatusOK, exercice) } -func createExercice(theme *fic.Theme, body []byte) (interface{}, error) { +func createExercice(c *gin.Context) { + theme := c.MustGet("theme").(*fic.Theme) + // Create a new exercice var ue fic.Exercice - if err := json.Unmarshal(body, &ue); err != nil { - return nil, err + err := c.ShouldBindJSON(&ue) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(ue.Title) == 0 { - return nil, errors.New("Title not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Title not filled"}) + return } var depend *fic.Exercice = nil if ue.Depend != nil { if d, err := fic.GetExercice(*ue.Depend); err != nil { - return nil, err + log.Println("Unable to createExercice:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice creation."}) + return } else { depend = d } } - return theme.AddExercice(ue.Title, ue.URLId, ue.Path, ue.Statement, ue.Overview, ue.Headline, depend, ue.Gain, ue.VideoURI, ue.Resolution, ue.SeeAlso, ue.Finished) + exercice, err := theme.AddExercice(ue.Title, ue.URLId, ue.Path, ue.Statement, ue.Overview, ue.Headline, depend, ue.Gain, ue.VideoURI, ue.Resolution, ue.SeeAlso, ue.Finished) + if err != nil { + log.Println("Unable to createExercice:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice creation."}) + return + } + + c.JSON(http.StatusOK, exercice) } type uploadedHint struct { @@ -349,53 +589,98 @@ type uploadedHint struct { URI string } -func createExerciceHint(exercice *fic.Exercice, body []byte) (interface{}, error) { +func createExerciceHint(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + var uh uploadedHint - if err := json.Unmarshal(body, &uh); err != nil { - return nil, err + err := c.ShouldBindJSON(&uh) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(uh.Content) != 0 { - return exercice.AddHint(uh.Title, uh.Content, uh.Cost) + hint, err := exercice.AddHint(uh.Title, uh.Content, uh.Cost) + if err != nil { + log.Println("Unable to AddHint in createExerciceHint:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add hint."}) + return + } + + c.JSON(http.StatusOK, hint) } else if len(uh.URI) != 0 { - return sync.ImportFile(sync.GlobalImporter, uh.URI, + hint, err := sync.ImportFile(sync.GlobalImporter, uh.URI, func(filePath string, origin string) (interface{}, error) { return exercice.AddHint(uh.Title, "$FILES"+strings.TrimPrefix(filePath, fic.FilesDir), uh.Cost) }) + + if err != nil { + log.Println("Unable to AddHint (after ImportFile) in createExerciceHint:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add hint."}) + return + } + + c.JSON(http.StatusOK, hint) } else { - return nil, errors.New("Hint's content not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Hint's content not filled"}) + return } } -func showExerciceHint(hint *fic.EHint, body []byte) (interface{}, error) { - return hint, nil +func showExerciceHint(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("hint").(*fic.EHint)) } -func showExerciceHintDeps(hint *fic.EHint, body []byte) (interface{}, error) { - return loadFlags(hint.GetDepends) +func showExerciceHintDeps(c *gin.Context) { + hint := c.MustGet("hint").(*fic.EHint) + + deps, err := loadFlags(hint.GetDepends) + if err != nil { + log.Println("Unable to loaddeps:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve hint dependencies."}) + return + } + + c.JSON(http.StatusOK, deps) } -func updateExerciceHint(hint *fic.EHint, body []byte) (interface{}, error) { +func updateExerciceHint(c *gin.Context) { + hint := c.MustGet("hint").(*fic.EHint) + var uh fic.EHint - if err := json.Unmarshal(body, &uh); err != nil { - return nil, err + err := c.ShouldBindJSON(&uh) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } uh.Id = hint.Id if len(uh.Title) == 0 { - return nil, errors.New("Hint's title not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Hint's title not filled"}) + return } if _, err := uh.Update(); err != nil { - return nil, err + log.Println("Unable to updateExerciceHint:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update hint."}) + return } - return uh, nil + c.JSON(http.StatusOK, uh) } -func deleteExerciceHint(hint *fic.EHint, _ []byte) (interface{}, error) { - return hint.Delete() +func deleteExerciceHint(c *gin.Context) { + hint := c.MustGet("hint").(*fic.EHint) + + _, err := hint.Delete() + if err != nil { + log.Println("Unable to deleteExerciceHint:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete hint."}) + return + } + + c.JSON(http.StatusOK, true) } type uploadedFlag struct { @@ -412,14 +697,17 @@ type uploadedFlag struct { ChoicesCost int64 `json:"choices_cost"` } -func createExerciceFlag(exercice *fic.Exercice, body []byte) (interface{}, error) { +func createExerciceFlag(c *gin.Context) { var uk uploadedFlag - if err := json.Unmarshal(body, &uk); err != nil { - return nil, err + err := c.ShouldBindJSON(&uk) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(uk.Flag) == 0 { - return nil, errors.New("Flag not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Flag not filled"}) + return } var vre *string = nil @@ -427,38 +715,66 @@ func createExerciceFlag(exercice *fic.Exercice, body []byte) (interface{}, error vre = uk.ValidatorRe } - return exercice.AddRawFlagKey(uk.Label, uk.Type, uk.Placeholder, uk.IgnoreCase, uk.NoTrim, uk.Multiline, vre, uk.SortReGroups, []byte(uk.Flag), uk.ChoicesCost) + exercice := c.MustGet("exercice").(*fic.Exercice) + + flag, err := exercice.AddRawFlagKey(uk.Label, uk.Type, uk.Placeholder, uk.IgnoreCase, uk.NoTrim, uk.Multiline, vre, uk.SortReGroups, []byte(uk.Flag), uk.ChoicesCost) + if err != nil { + log.Println("Unable to createExerciceFlag:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to create flag."}) + return + } + + c.JSON(http.StatusOK, flag) } -func showExerciceFlag(flag *fic.FlagKey, _ *fic.Exercice, body []byte) (interface{}, error) { - return flag, nil +func showExerciceFlag(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("flag-key").(*fic.FlagKey)) } -func showExerciceFlagDeps(flag *fic.FlagKey, _ *fic.Exercice, body []byte) (interface{}, error) { - return loadFlags(flag.GetDepends) +func showExerciceFlagDeps(c *gin.Context) { + flag := c.MustGet("flag-key").(*fic.FlagKey) + + deps, err := loadFlags(flag.GetDepends) + if err != nil { + log.Println("Unable to loaddeps:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve hint dependencies."}) + return + } + + c.JSON(http.StatusOK, deps) } -func tryExerciceFlag(flag *fic.FlagKey, _ *fic.Exercice, body []byte) (interface{}, error) { +func tryExerciceFlag(c *gin.Context) { + flag := c.MustGet("flag-key").(*fic.FlagKey) + var uk uploadedFlag - if err := json.Unmarshal(body, &uk); err != nil { - return nil, err + err := c.ShouldBindJSON(&uk) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(uk.Flag) == 0 { - return nil, errors.New("Empty submission") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Empty submission"}) + return } if flag.Check([]byte(uk.Flag)) == 0 { - return true, nil - } else { - return nil, errors.New("Bad submission") + c.AbortWithStatusJSON(http.StatusOK, true) + return } + + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad submission"}) } -func updateExerciceFlag(flag *fic.FlagKey, exercice *fic.Exercice, body []byte) (interface{}, error) { +func updateExerciceFlag(c *gin.Context) { + flag := c.MustGet("flag-key").(*fic.FlagKey) + var uk uploadedFlag - if err := json.Unmarshal(body, &uk); err != nil { - return nil, err + err := c.ShouldBindJSON(&uk) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(uk.Label) == 0 { @@ -474,7 +790,9 @@ func updateExerciceFlag(flag *fic.FlagKey, exercice *fic.Exercice, body []byte) var err error flag.Checksum, err = flag.ComputeChecksum([]byte(uk.Flag)) if err != nil { - return nil, err + log.Println("Unable to ComputeChecksum:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to compute flag checksum"}) + return } } else { flag.Checksum = uk.Value @@ -488,37 +806,63 @@ func updateExerciceFlag(flag *fic.FlagKey, exercice *fic.Exercice, body []byte) } if _, err := flag.Update(); err != nil { - return nil, err + log.Println("Unable to updateExerciceFlag:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update flag."}) + return } - return flag, nil + c.JSON(http.StatusOK, flag) } -func deleteExerciceFlag(flag *fic.FlagKey, _ *fic.Exercice, _ []byte) (interface{}, error) { - return flag.Delete() +func deleteExerciceFlag(c *gin.Context) { + flag := c.MustGet("flag-key").(*fic.FlagKey) + + _, err := flag.Delete() + if err != nil { + log.Println("Unable to deleteExerciceFlag:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete flag."}) + return + } + + c.JSON(http.StatusOK, true) } -func createFlagChoice(flag *fic.FlagKey, exercice *fic.Exercice, body []byte) (interface{}, error) { +func createFlagChoice(c *gin.Context) { + flag := c.MustGet("flag-key").(*fic.FlagKey) + var uc fic.FlagChoice - if err := json.Unmarshal(body, &uc); err != nil { - return nil, err + err := c.ShouldBindJSON(&uc) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(uc.Label) == 0 { uc.Label = uc.Value } - return flag.AddChoice(&uc) + choice, err := flag.AddChoice(&uc) + if err != nil { + log.Println("Unable to createFlagChoice:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to create flag choice."}) + return + } + + c.JSON(http.StatusOK, choice) } -func showFlagChoice(choice *fic.FlagChoice, _ *fic.Exercice, body []byte) (interface{}, error) { - return choice, nil +func showFlagChoice(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("flag-choice").(*fic.FlagChoice)) } -func updateFlagChoice(choice *fic.FlagChoice, _ *fic.Exercice, body []byte) (interface{}, error) { +func updateFlagChoice(c *gin.Context) { + choice := c.MustGet("flag-choice").(*fic.FlagChoice) + var uc fic.FlagChoice - if err := json.Unmarshal(body, &uc); err != nil { - return nil, err + err := c.ShouldBindJSON(&uc) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(uc.Label) == 0 { @@ -530,34 +874,60 @@ func updateFlagChoice(choice *fic.FlagChoice, _ *fic.Exercice, body []byte) (int choice.Value = uc.Value if _, err := choice.Update(); err != nil { - return nil, err + log.Println("Unable to updateFlagChoice:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to update flag choice."}) + return } - return choice, nil + c.JSON(http.StatusOK, choice) } -func deleteFlagChoice(choice *fic.FlagChoice, _ *fic.Exercice, _ []byte) (interface{}, error) { - return choice.Delete() +func deleteFlagChoice(c *gin.Context) { + choice := c.MustGet("flag-choice").(*fic.FlagChoice) + + _, err := choice.Delete() + if err != nil { + log.Println("Unable to deleteExerciceChoice:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete choice."}) + return + } + + c.JSON(http.StatusOK, true) } -func showExerciceQuiz(quiz *fic.MCQ, _ *fic.Exercice, body []byte) (interface{}, error) { - return quiz, nil +func showExerciceQuiz(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("flag-quiz").(*fic.MCQ)) } -func showExerciceQuizDeps(quiz *fic.MCQ, _ *fic.Exercice, body []byte) (interface{}, error) { - return loadFlags(quiz.GetDepends) +func showExerciceQuizDeps(c *gin.Context) { + quiz := c.MustGet("flag-quiz").(*fic.MCQ) + + deps, err := loadFlags(quiz.GetDepends) + if err != nil { + log.Println("Unable to loaddeps:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve quiz dependencies."}) + return + } + + c.JSON(http.StatusOK, deps) } -func updateExerciceQuiz(quiz *fic.MCQ, exercice *fic.Exercice, body []byte) (interface{}, error) { +func updateExerciceQuiz(c *gin.Context) { + quiz := c.MustGet("flag-quiz").(*fic.MCQ) + var uq fic.MCQ - if err := json.Unmarshal(body, &uq); err != nil { - return nil, err + err := c.ShouldBindJSON(&uq) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } quiz.Title = uq.Title if _, err := quiz.Update(); err != nil { - return nil, err + log.Println("Unable to updateExerciceQuiz:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update quiz."}) + return } // Update and remove old entries @@ -572,7 +942,9 @@ func updateExerciceQuiz(quiz *fic.MCQ, exercice *fic.Exercice, body []byte) (int cur.Label = next.Label cur.Response = next.Response if _, err := cur.Update(); err != nil { - return nil, err + log.Println("Unable to update MCQ entry:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to update some MCQ entry"}) + return } } @@ -582,7 +954,9 @@ func updateExerciceQuiz(quiz *fic.MCQ, exercice *fic.Exercice, body []byte) (int if seen == false { if _, err := cur.Delete(); err != nil { - return nil, err + log.Println("Unable to delete MCQ entry:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to delete some MCQ entry"}) + return } else { delete = append(delete, i) } @@ -595,48 +969,78 @@ func updateExerciceQuiz(quiz *fic.MCQ, exercice *fic.Exercice, body []byte) (int // Add new choices for _, choice := range uq.Entries { if choice.Id == 0 { - if c, err := quiz.AddEntry(choice); err != nil { - return nil, err + if ch, err := quiz.AddEntry(choice); err != nil { + log.Println("Unable to add MCQ entry:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to add some MCQ entry"}) + return } else { - quiz.Entries = append(quiz.Entries, c) + quiz.Entries = append(quiz.Entries, ch) } } } - return quiz, nil + c.JSON(http.StatusOK, quiz) } -func deleteExerciceQuiz(quiz *fic.MCQ, _ *fic.Exercice, _ []byte) (interface{}, error) { +func deleteExerciceQuiz(c *gin.Context) { + quiz := c.MustGet("flag-quiz").(*fic.MCQ) + for _, choice := range quiz.Entries { if _, err := choice.Delete(); err != nil { - return nil, err + log.Println("Unable to deleteExerciceQuiz (entry):", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete quiz entry."}) + return } } - return quiz.Delete() + _, err := quiz.Delete() + if err != nil { + log.Println("Unable to deleteExerciceQuiz:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete quiz."}) + return + } + + c.JSON(http.StatusOK, true) } -func listExerciceTags(exercice *fic.Exercice, _ []byte) (interface{}, error) { - return exercice.GetTags() +func listExerciceTags(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + + tags, err := exercice.GetTags() + if err != nil { + log.Println("Unable to listExerciceTags:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to get tags."}) + return + } + + c.JSON(http.StatusOK, tags) } -func addExerciceTag(exercice *fic.Exercice, body []byte) (interface{}, error) { +func addExerciceTag(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + var ut []string - if err := json.Unmarshal(body, &ut); err != nil { - return nil, err + err := c.ShouldBindJSON(&ut) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } // TODO: a DB transaction should be done here: on error we should rollback for _, t := range ut { if _, err := exercice.AddTag(t); err != nil { - return nil, err + log.Println("Unable to addExerciceTag:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add some tag."}) + return } } - return ut, nil + c.JSON(http.StatusOK, ut) } -func updateExerciceTags(exercice *fic.Exercice, body []byte) (interface{}, error) { +func updateExerciceTags(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + exercice.WipeTags() - return addExerciceTag(exercice, body) + addExerciceTag(c) } diff --git a/admin/api/file.go b/admin/api/file.go index 8edd5424..f164ebb2 100644 --- a/admin/api/file.go +++ b/admin/api/file.go @@ -2,43 +2,79 @@ package api import ( "encoding/hex" - "encoding/json" "fmt" + "log" + "net/http" + "strconv" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/files/", apiHandler(listFiles)) - router.DELETE("/api/files/", apiHandler(clearFiles)) - - router.GET("/api/files/:fileid", apiHandler(fileHandler(showFile))) - router.PUT("/api/files/:fileid", apiHandler(fileHandler(updateFile))) - router.DELETE("/api/files/:fileid", apiHandler(fileHandler(deleteFile))) - - router.DELETE("/api/files/:fileid/dependancies/:depid", apiHandler(fileDependancyHandler(deleteFileDep))) - - router.GET("/api/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles))) - router.POST("/api/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile))) - - router.GET("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(showFile))) - router.PUT("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(updateFile))) - router.DELETE("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(deleteFile))) +func declareFilesGlobalRoutes(router *gin.RouterGroup) { + router.DELETE("/files/", clearFiles) // Remote - router.GET("/api/remote/themes/:thid/exercices/:exid/files", apiHandler(sync.ApiGetRemoteExerciceFiles)) + router.GET("/remote/themes/:thid/exercices/:exid/files", sync.ApiGetRemoteExerciceFiles) +} + +func declareFilesRoutes(router *gin.RouterGroup) { + router.GET("/files", listFiles) + router.POST("/files", createExerciceFile) + + apiFilesRoutes := router.Group("/files/:fileid") + apiFilesRoutes.Use(FileHandler) + apiFilesRoutes.GET("", showFile) + apiFilesRoutes.PUT("", updateFile) + apiFilesRoutes.DELETE("", deleteFile) + + apiFileDepsRoutes := apiFilesRoutes.Group("/dependancies/:depid") + apiFileDepsRoutes.Use(FileDepHandler) + apiFileDepsRoutes.DELETE("", deleteFileDep) // Check - router.POST("/api/files/:fileid/check", apiHandler(fileHandler(checkFile))) + apiFilesRoutes.POST("/check", checkFile) +} - // Synchronize - router.POST("/api/sync/exercices/:eid/files", apiHandler(exerciceHandler( - func(exercice *fic.Exercice, _ []byte) (interface{}, error) { - return sync.SyncExerciceFiles(sync.GlobalImporter, exercice), nil - }))) +func FileHandler(c *gin.Context) { + fileid, err := strconv.ParseInt(string(c.Params.ByName("fileid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid file identifier"}) + return + } + + var file *fic.EFile + if exercice, exists := c.Get("exercice"); exists { + file, err = exercice.(*fic.Exercice).GetFile(fileid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "File not found"}) + return + } + } else { + file, err = fic.GetFile(fileid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "File not found"}) + return + } + } + + c.Set("file", file) + + c.Next() +} + +func FileDepHandler(c *gin.Context) { + depid, err := strconv.ParseInt(string(c.Params.ByName("depid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid dependency identifier"}) + return + } + + c.Set("file-depid", depid) + + c.Next() } type APIFile struct { @@ -87,20 +123,35 @@ func genFileList(in []*fic.EFile, e error) (out []APIFile, err error) { return } -func listFiles(_ httprouter.Params, body []byte) (interface{}, error) { - return genFileList(fic.GetFiles()) +func listFiles(c *gin.Context) { + var files []APIFile + var err error + + if exercice, exists := c.Get("exercice"); exists { + files, err = genFileList(exercice.(*fic.Exercice).GetFiles()) + } else { + files, err = genFileList(fic.GetFiles()) + } + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, files) } -func listExerciceFiles(exercice *fic.Exercice, body []byte) (interface{}, error) { - return genFileList(exercice.GetFiles()) +func clearFiles(c *gin.Context) { + _, err := fic.ClearFiles() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, true) } -func clearFiles(_ httprouter.Params, _ []byte) (interface{}, error) { - return fic.ClearFiles() -} - -func showFile(file *fic.EFile, _ []byte) (interface{}, error) { - return file, nil +func showFile(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("file").(*fic.EFile)) } type uploadedFile struct { @@ -108,45 +159,92 @@ type uploadedFile struct { Digest string } -func createExerciceFile(exercice *fic.Exercice, body []byte) (interface{}, error) { - var uf uploadedFile - if err := json.Unmarshal(body, &uf); err != nil { - return nil, err +func createExerciceFile(c *gin.Context) { + exercice, exists := c.Get("exercice") + if !exists { + c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "File can only be added inside an exercice."}) + return } - return sync.ImportFile(sync.GlobalImporter, uf.URI, + var uf uploadedFile + err := c.ShouldBindJSON(&uf) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + ret, err := sync.ImportFile(sync.GlobalImporter, uf.URI, func(filePath string, origin string) (interface{}, error) { if digest, err := hex.DecodeString(uf.Digest); err != nil { return nil, err } else { - return exercice.ImportFile(filePath, origin, digest) + return exercice.(*fic.Exercice).ImportFile(filePath, origin, digest) } }) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, ret) } -func updateFile(file *fic.EFile, body []byte) (interface{}, error) { +func updateFile(c *gin.Context) { + file := c.MustGet("file").(*fic.EFile) + var uf fic.EFile - if err := json.Unmarshal(body, &uf); err != nil { - return nil, err + err := c.ShouldBindJSON(&uf) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } uf.Id = file.Id if _, err := uf.Update(); err != nil { - return nil, err - } else { - return uf, nil + log.Println("Unable to updateFile:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."}) + return } + + c.JSON(http.StatusOK, uf) } -func deleteFile(file *fic.EFile, _ []byte) (interface{}, error) { - return file.Delete() +func deleteFile(c *gin.Context) { + file := c.MustGet("file").(*fic.EFile) + + _, err := file.Delete() + if err != nil { + log.Println("Unable to updateFile:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."}) + return + } + + c.JSON(http.StatusOK, true) } -func deleteFileDep(file *fic.EFile, depid int, _ []byte) (interface{}, error) { - return true, file.DeleteDepend(&fic.FlagKey{Id: depid}) +func deleteFileDep(c *gin.Context) { + file := c.MustGet("file").(*fic.EFile) + depid := c.MustGet("file-depid").(int64) + + err := file.DeleteDepend(&fic.FlagKey{Id: int(depid)}) + if err != nil { + log.Println("Unable to deleteFileDep:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete file dependency."}) + return + } + + c.JSON(http.StatusOK, true) } -func checkFile(file *fic.EFile, _ []byte) (interface{}, error) { - return true, file.CheckFileOnDisk() +func checkFile(c *gin.Context) { + file := c.MustGet("file").(*fic.EFile) + + err := file.CheckFileOnDisk() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, true) } diff --git a/admin/api/handlers.go b/admin/api/handlers.go deleted file mode 100644 index 302ab441..00000000 --- a/admin/api/handlers.go +++ /dev/null @@ -1,352 +0,0 @@ -package api - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - "strconv" - "strings" - "time" - - "srs.epita.fr/fic-server/libfic" - - "github.com/julienschmidt/httprouter" -) - -type DispatchFunction func(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) { - if addr := r.Header.Get("X-Forwarded-For"); addr != "" { - r.RemoteAddr = addr - } - 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{} - var err error = nil - - ret, err = f(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) - } - } -} - -func teamPublicHandler(f func(*fic.Team, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if tid, err := strconv.ParseInt(string(ps.ByName("tid")), 10, 64); err != nil { - return nil, err - } else if tid == 0 { - return f(nil, body) - } else if team, err := fic.GetTeam(tid); err != nil { - return nil, err - } else { - return f(team, body) - } - } -} - -func teamHandler(f func(*fic.Team, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if tid, err := strconv.ParseInt(string(ps.ByName("tid")), 10, 64); err != nil { - return nil, err - } else if team, err := fic.GetTeam(tid); err != nil { - return nil, err - } else { - return f(team, body) - } - } -} - -func teamAssocHandler(f func(*fic.Team, string, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - var team *fic.Team - - teamHandler(func(tm *fic.Team, _ []byte) (interface{}, error) { - team = tm - return nil, nil - })(ps, body) - - return f(team, string(ps.ByName("assoc")), body) - } -} - -func themeHandler(f func(*fic.Theme, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(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(theme, body) - } - } -} - -func exerciceHandler(f func(*fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(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(exercice, body) - } - } -} - -func themedExerciceHandler(f func(*fic.Theme, *fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - var theme *fic.Theme - var exercice *fic.Exercice - - themeHandler(func(th *fic.Theme, _ []byte) (interface{}, error) { - theme = th - return nil, nil - })(ps, body) - - exerciceHandler(func(ex *fic.Exercice, _ []byte) (interface{}, error) { - exercice = ex - return nil, nil - })(ps, body) - - return f(theme, exercice, body) - } -} - -func hintHandler(f func(*fic.EHint, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if hid, err := strconv.ParseInt(string(ps.ByName("hid")), 10, 64); err != nil { - return nil, err - } else if hint, err := fic.GetHint(hid); err != nil { - return nil, err - } else { - return f(hint, body) - } - } -} - -func flagKeyHandler(f func(*fic.FlagKey, *fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - var exercice *fic.Exercice - exerciceHandler(func(ex *fic.Exercice, _ []byte) (interface{}, error) { - exercice = ex - return nil, nil - })(ps, body) - - if kid, err := strconv.ParseInt(string(ps.ByName("kid")), 10, 64); err != nil { - return nil, err - } else if flags, err := exercice.GetFlagKeys(); err != nil { - return nil, err - } else { - for _, flag := range flags { - if flag.Id == int(kid) { - return f(flag, exercice, body) - } - } - return nil, errors.New("Unable to find the requested key") - } - } -} - -func choiceHandler(f func(*fic.FlagChoice, *fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - var exercice *fic.Exercice - var flag *fic.FlagKey - flagKeyHandler(func(fl *fic.FlagKey, ex *fic.Exercice, _ []byte) (interface{}, error) { - exercice = ex - flag = fl - return nil, nil - })(ps, body) - - if cid, err := strconv.ParseInt(string(ps.ByName("cid")), 10, 32); err != nil { - return nil, err - } else if choice, err := flag.GetChoice(int(cid)); err != nil { - return nil, err - } else { - return f(choice, exercice, body) - } - } -} - -func quizHandler(f func(*fic.MCQ, *fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - var exercice *fic.Exercice - exerciceHandler(func(ex *fic.Exercice, _ []byte) (interface{}, error) { - exercice = ex - return nil, nil - })(ps, body) - - if qid, err := strconv.ParseInt(string(ps.ByName("qid")), 10, 64); err != nil { - return nil, err - } else if mcqs, err := exercice.GetMCQ(); err != nil { - return nil, err - } else { - for _, mcq := range mcqs { - if mcq.Id == int(qid) { - return f(mcq, exercice, body) - } - } - return nil, errors.New("Unable to find the requested key") - } - } -} - -func exerciceFileHandler(f func(*fic.EFile, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - var exercice *fic.Exercice - exerciceHandler(func(ex *fic.Exercice, _ []byte) (interface{}, error) { - exercice = ex - return nil, nil - })(ps, body) - - if fid, err := strconv.ParseInt(string(ps.ByName("fid")), 10, 64); err != nil { - return nil, err - } else if files, err := exercice.GetFiles(); err != nil { - return nil, err - } else { - for _, file := range files { - if file.Id == fid { - return f(file, body) - } - } - return nil, errors.New("Unable to find the requested file") - } - } -} - -func eventHandler(f func(*fic.Event, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if evid, err := strconv.ParseInt(string(ps.ByName("evid")), 10, 64); err != nil { - return nil, err - } else if event, err := fic.GetEvent(evid); err != nil { - return nil, err - } else { - return f(event, body) - } - } -} - -func claimHandler(f func(*fic.Claim, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if cid, err := strconv.ParseInt(string(ps.ByName("cid")), 10, 64); err != nil { - return nil, fmt.Errorf("Invalid claim id: %w", err) - } else if claim, err := fic.GetClaim(cid); err != nil { - return nil, fmt.Errorf("Unable to find requested claim (id=%d): %w", cid, err) - } else { - return f(claim, body) - } - } -} - -func claimAssigneeHandler(f func(*fic.ClaimAssignee, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if aid, err := strconv.ParseInt(string(ps.ByName("aid")), 10, 64); err != nil { - return nil, err - } else if assignee, err := fic.GetAssignee(aid); err != nil { - return nil, err - } else { - return f(assignee, body) - } - } -} - -func fileHandler(f func(*fic.EFile, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if fileid, err := strconv.ParseInt(string(ps.ByName("fileid")), 10, 64); err != nil { - return nil, err - } else if file, err := fic.GetFile(fileid); err != nil { - return nil, err - } else { - return f(file, body) - } - } -} - -func fileDependancyHandler(f func(*fic.EFile, int, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if depid, err := strconv.ParseInt(string(ps.ByName("depid")), 10, 64); err != nil { - return nil, err - } else { - return fileHandler(func(file *fic.EFile, b []byte) (interface{}, error) { - return f(file, int(depid), b) - })(ps, body) - } - } -} - -func certificateHandler(f func(*fic.Certificate, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - var cid uint64 - var err error - certid := strings.TrimSuffix(ps.ByName("certid"), ".p12") - if cid, err = strconv.ParseUint(certid, 10, 64); err != nil { - if cid, err = strconv.ParseUint(certid, 16, 64); err != nil { - return nil, err - } - } - - if cert, err := fic.GetCertificate(cid); err != nil { - return nil, err - } else { - return f(cert, body) - } - } -} - -func notFound(ps httprouter.Params, _ []byte) (interface{}, error) { - return nil, nil -} diff --git a/admin/api/health.go b/admin/api/health.go index f3cf2e27..133cf5af 100644 --- a/admin/api/health.go +++ b/admin/api/health.go @@ -3,6 +3,7 @@ package api import ( "fmt" "io/ioutil" + "net/http" "os" "path" "strings" @@ -10,26 +11,26 @@ import ( "srs.epita.fr/fic-server/admin/pki" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) var TimestampCheck = "submissions" -func init() { - router.GET("/api/timestamps.json", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - if stat, err := os.Stat(TimestampCheck); err != nil { - return nil, err - } else { - now := time.Now().UTC() - return map[string]interface{}{ - "frontend": stat.ModTime().UTC(), - "backend": now, - "diffFB": now.Sub(stat.ModTime()), - }, nil - } - })) - router.GET("/api/health.json", apiHandler(GetHealth)) +func declareHealthRoutes(router *gin.RouterGroup) { + router.GET("/timestamps.json", func(c *gin.Context) { + stat, err := os.Stat(TimestampCheck) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("timestamp.json: %s", err.Error())}) + return + } + now := time.Now().UTC() + c.JSON(http.StatusOK, gin.H{ + "frontend": stat.ModTime().UTC(), + "backend": now, + "diffFB": now.Sub(stat.ModTime()), + }) + }) + router.GET("/health.json", GetHealth) } type healthFileReport struct { @@ -41,22 +42,22 @@ type healthFileReport struct { func getHealth(pathname string) (ret []healthFileReport) { if ds, err := ioutil.ReadDir(pathname); err != nil { ret = append(ret, healthFileReport{ - Path: strings.TrimPrefix(pathname, TimestampCheck), - Error: fmt.Sprintf("unable to ReadDir: %s", err), - }) + Path: strings.TrimPrefix(pathname, TimestampCheck), + Error: fmt.Sprintf("unable to ReadDir: %s", err), + }) return } else { for _, d := range ds { p := path.Join(pathname, d.Name()) if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 { ret = append(ret, getHealth(p)...) - } else if !d.IsDir() && d.Mode()&os.ModeSymlink == 0 && time.Since(d.ModTime()) > 2 * time.Second { + } else if !d.IsDir() && d.Mode()&os.ModeSymlink == 0 && time.Since(d.ModTime()) > 2*time.Second { teamDir := strings.TrimPrefix(pathname, TimestampCheck) idteam, _ := pki.GetAssociation(path.Join(TeamsDir, teamDir)) ret = append(ret, healthFileReport{ IdTeam: idteam, - Path: path.Join(teamDir, d.Name()), - Error: "existing untreated file", + Path: path.Join(teamDir, d.Name()), + Error: "existing untreated file", }) } } @@ -64,10 +65,11 @@ func getHealth(pathname string) (ret []healthFileReport) { } } -func GetHealth(httprouter.Params, []byte) (interface{}, error) { +func GetHealth(c *gin.Context) { if _, err := os.Stat(TimestampCheck); err != nil { - return nil, err - } else { - return getHealth(TimestampCheck), nil + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("health.json: %s", err.Error())}) + return } + + c.JSON(http.StatusOK, getHealth(TimestampCheck)) } diff --git a/admin/api/monitor.go b/admin/api/monitor.go index b8af548f..3d6a9114 100644 --- a/admin/api/monitor.go +++ b/admin/api/monitor.go @@ -3,20 +3,20 @@ package api import ( "bufio" "io/ioutil" + "net/http" "os" "strconv" "strings" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/monitor", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return map[string]interface{}{ - "localhost": genLocalConstants(), - }, nil - })) +func declareMonitorRoutes(router *gin.RouterGroup) { + router.GET("/monitor", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "localhost": genLocalConstants(), + }) + }) } func readLoadAvg(fd *os.File) (ret map[string]float64) { @@ -58,7 +58,7 @@ func readCPUStats(fd *os.File) (ret map[string]map[string]uint64) { ret[f[0]] = map[string]uint64{} var total uint64 = 0 for i, k := range []string{"user", "nice", "system", "idle", "iowait", "irq", "softirq"} { - if v, err := strconv.ParseUint(f[i + 1], 10, 64); err == nil { + if v, err := strconv.ParseUint(f[i+1], 10, 64); err == nil { ret[f[0]][k] = v total += v } diff --git a/admin/api/password.go b/admin/api/password.go index 9a20c4a9..38d7896d 100644 --- a/admin/api/password.go +++ b/admin/api/password.go @@ -4,67 +4,97 @@ import ( "bytes" "fmt" "io/ioutil" + "log" + "net/http" "path" "text/template" "srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) var OidcSecret = "" -func init() { - router.POST("/api/password", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - if passwd, err := fic.GeneratePassword(); err != nil { - return nil, err - } else { - return map[string]string{"password": passwd}, nil +func declarePasswordRoutes(router *gin.RouterGroup) { + router.POST("/password", func(c *gin.Context) { + passwd, err := fic.GeneratePassword() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"password": passwd}) + }) + router.GET("/api/dex.yaml", func(c *gin.Context) { + cfg, err := genDexConfig() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.String(http.StatusOK, string(cfg)) + }) + router.POST("/api/dex.yaml", func(c *gin.Context) { + if dexcfg, err := genDexConfig(); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-config.yaml"), []byte(dexcfg), 0644); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, true) + }) + router.GET("/api/dex-password.tpl", func(c *gin.Context) { + passtpl, err := genDexPasswordTpl() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.String(http.StatusOK, string(passtpl)) + }) + router.POST("/api/dex-password.tpl", func(c *gin.Context) { + if dexcfg, err := genDexPasswordTpl(); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-password.tpl"), []byte(dexcfg), 0644); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, true) + }) +} + +func declareTeamsPasswordRoutes(router *gin.RouterGroup) { + router.GET("/password", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + c.String(http.StatusOK, *team.Password) + }) + router.POST("/password", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + if passwd, err := fic.GeneratePassword(); err != nil { + log.Println("Unable to GeneratePassword:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when generating the new team password"}) + return + } else { + team.Password = &passwd + + t, err := team.Update() + if err != nil { + log.Println("Unable to Update Team:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when updating the new team password"}) + return } - })) - router.GET("/api/teams/:tid/password", apiHandler(teamHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return team.Password, nil - }))) - router.POST("/api/teams/:tid/password", apiHandler(teamHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - if passwd, err := fic.GeneratePassword(); err != nil { - return nil, err - } else { - team.Password = &passwd - return team.Update() - } - }))) - router.GET("/api/dex.yaml", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return genDexConfig() - })) - router.POST("/api/dex.yaml", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - if dexcfg, err := genDexConfig(); err != nil { - return nil, err - } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-config.yaml"), []byte(dexcfg), 0644); err != nil { - return nil, err - } else { - return true, nil - } - })) - router.GET("/api/dex-password.tpl", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return genDexPasswordTpl() - })) - router.POST("/api/dex-password.tpl", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - if dexcfg, err := genDexPasswordTpl(); err != nil { - return nil, err - } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-password.tpl"), []byte(dexcfg), 0644); err != nil { - return nil, err - } else { - return true, nil - } - })) + + c.JSON(http.StatusOK, t) + } + }) } const dexcfgtpl = `issuer: https://fic.srs.epita.fr diff --git a/admin/api/public.go b/admin/api/public.go index 595cfba3..09319ac4 100644 --- a/admin/api/public.go +++ b/admin/api/public.go @@ -3,18 +3,20 @@ package api import ( "encoding/json" "fmt" + "log" + "net/http" "os" "path" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) var DashboardDir string -func init() { - router.GET("/api/public/:sid", apiHandler(getPublic)) - router.DELETE("/api/public/:sid", apiHandler(deletePublic)) - router.PUT("/api/public/:sid", apiHandler(savePublic)) +func declarePublicRoutes(router *gin.RouterGroup) { + router.GET("/public/:sid", getPublic) + router.DELETE("/public/:sid", deletePublic) + router.PUT("/public/:sid", savePublic) } type FICPublicScene struct { @@ -62,31 +64,44 @@ func savePublicTo(path string, s FICPublicDisplay) error { } } -func getPublic(ps httprouter.Params, body []byte) (interface{}, error) { - if _, err := os.Stat(path.Join(DashboardDir, fmt.Sprintf("public%s.json", ps.ByName("sid")))); !os.IsNotExist(err) { - return readPublic(path.Join(DashboardDir, fmt.Sprintf("public%s.json", ps.ByName("sid")))) - } else { - return FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}}, nil +func getPublic(c *gin.Context) { + if _, err := os.Stat(path.Join(DashboardDir, fmt.Sprintf("public%s.json", c.Params.ByName("sid")))); !os.IsNotExist(err) { + p, err := readPublic(path.Join(DashboardDir, fmt.Sprintf("public%s.json", c.Params.ByName("sid")))) + if err != nil { + log.Println("Unable to readPublic in getPublic:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene retrieval."}) + return + } + + c.JSON(http.StatusOK, p) } + + c.JSON(http.StatusOK, FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}}) } -func deletePublic(ps httprouter.Params, body []byte) (interface{}, error) { - if err := savePublicTo(path.Join(DashboardDir, fmt.Sprintf("public%s.json", ps.ByName("sid"))), FICPublicDisplay{}); err != nil { - return nil, err - } else { - return FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}}, nil +func deletePublic(c *gin.Context) { + if err := savePublicTo(path.Join(DashboardDir, fmt.Sprintf("public%s.json", c.Params.ByName("sid"))), FICPublicDisplay{}); err != nil { + log.Println("Unable to deletePublic:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene deletion."}) + return } + + c.JSON(http.StatusOK, FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}}) } -func savePublic(ps httprouter.Params, body []byte) (interface{}, error) { +func savePublic(c *gin.Context) { var scenes FICPublicDisplay - if err := json.Unmarshal(body, &scenes); err != nil { - return nil, err + err := c.ShouldBindJSON(&scenes) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } - if err := savePublicTo(path.Join(DashboardDir, fmt.Sprintf("public%s.json", ps.ByName("sid"))), scenes); err != nil { - return nil, err - } else { - return scenes, err + if err := savePublicTo(path.Join(DashboardDir, fmt.Sprintf("public%s.json", c.Params.ByName("sid"))), scenes); err != nil { + log.Println("Unable to savePublicTo:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene saving."}) + return } + + c.JSON(http.StatusOK, scenes) } diff --git a/admin/api/qa.go b/admin/api/qa.go index f24ae7d3..26c4cf63 100644 --- a/admin/api/qa.go +++ b/admin/api/qa.go @@ -1,84 +1,118 @@ package api import ( - "encoding/json" - "errors" + "log" + "net/http" "strconv" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.POST("/api/qa/", apiHandler(importExerciceQA)) - router.POST("/api/qa/:qid/comments", apiHandler(qaHandler(importQAComment))) +func declareQARoutes(router *gin.RouterGroup) { + router.POST("/qa/", importExerciceQA) + + apiQARoutes := router.Group("/qa/:qid") + apiQARoutes.POST("/comments", importQAComment) } -func qaHandler(f func(*fic.QAQuery, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { - return func(ps httprouter.Params, body []byte) (interface{}, error) { - if qid, err := strconv.ParseInt(string(ps.ByName("qid")), 10, 64); err != nil { - return nil, err - } else if query, err := fic.GetQAQuery(qid); err != nil { - return nil, err - } else { - return f(query, body) - } +func QAHandler(c *gin.Context) { + qid, err := strconv.ParseInt(string(c.Params.ByName("qid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid QA identifier"}) + return } + + qa, err := fic.GetQAQuery(qid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "QA query not found"}) + return + } + + c.Set("qa-query", qa) + + c.Next() } -func importExerciceQA(_ httprouter.Params, body []byte) (interface{}, error) { +func importExerciceQA(c *gin.Context) { // Create a new query var uq fic.QAQuery - if err := json.Unmarshal(body, &uq); err != nil { - return nil, err + err := c.ShouldBindJSON(&uq) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } var exercice *fic.Exercice - var err error if uq.IdExercice == 0 { - return nil, errors.New("id_exercice not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "id_exercice not filled"}) + return } else if exercice, err = fic.GetExercice(uq.IdExercice); err != nil { - return nil, err + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unable to find requested exercice"}) + 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 { - 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, uq.IdTeam, uq.User, uq.State); err != nil { - return nil, err + log.Println("Unable to importExerciceQA:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during query creation."}) + return } else { qa.Creation = uq.Creation qa.Solved = uq.Solved qa.Closed = qa.Closed _, err = qa.Update() - return qa, err + if err != nil { + log.Println("Unable to update in importExerciceQA:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during query updating."}) + return + } + + c.JSON(http.StatusOK, qa) } } -func importQAComment(query *fic.QAQuery, body []byte) (interface{}, error) { +func importQAComment(c *gin.Context) { + query := c.MustGet("qa-query").(*fic.QAQuery) + // Create a new query var uc fic.QAComment - if err := json.Unmarshal(body, &uc); err != nil { - return nil, err + err := c.ShouldBindJSON(&uc) + if 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 } if qac, err := query.AddComment(uc.Content, uc.IdTeam, uc.User); err != nil { - return nil, err + log.Println("Unable to AddComment in importQAComment:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during comment creation."}) + return } else { qac.Date = uc.Date _, err = qac.Update() - return qac, err + if err != nil { + log.Println("Unable to Update comment in importQAComment") + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during comment creation."}) + return + } + + c.JSON(http.StatusOK, qac) } } diff --git a/admin/api/router.go b/admin/api/router.go index a6bd873b..991a58f1 100644 --- a/admin/api/router.go +++ b/admin/api/router.go @@ -1,11 +1,26 @@ 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") -func Router() *httprouter.Router { - return router + declareCertificateRoutes(apiRoutes) + declareClaimsRoutes(apiRoutes) + declareEventsRoutes(apiRoutes) + declareExercicesRoutes(apiRoutes) + declareFilesRoutes(apiRoutes) + declareGlobalExercicesRoutes(apiRoutes) + declareHealthRoutes(apiRoutes) + declareMonitorRoutes(apiRoutes) + declarePasswordRoutes(apiRoutes) + declarePublicRoutes(apiRoutes) + declareQARoutes(apiRoutes) + declareTeamsRoutes(apiRoutes) + declareThemesRoutes(apiRoutes) + declareSettingsRoutes(apiRoutes) + declareSyncRoutes(apiRoutes) + DeclareVersionRoutes(apiRoutes) } diff --git a/admin/api/settings.go b/admin/api/settings.go index d3adb39f..2fed7856 100644 --- a/admin/api/settings.go +++ b/admin/api/settings.go @@ -1,35 +1,44 @@ package api import ( - "encoding/json" - "errors" + "fmt" + "log" + "net/http" "path" "reflect" + "time" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/settings" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) var IsProductionEnv = false -func init() { - router.GET("/api/challenge.json", apiHandler(getChallengeInfo)) - router.PUT("/api/challenge.json", apiHandler(saveChallengeInfo)) +func declareSettingsRoutes(router *gin.RouterGroup) { + router.GET("/challenge.json", getChallengeInfo) + router.PUT("/challenge.json", saveChallengeInfo) - router.GET("/api/settings-ro.json", apiHandler(getROSettings)) - router.GET("/api/settings.json", apiHandler(getSettings)) - router.PUT("/api/settings.json", apiHandler(saveSettings)) - router.DELETE("/api/settings.json", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { - return true, ResetSettings() - })) + router.GET("/settings-ro.json", getROSettings) + router.GET("/settings.json", getSettings) + router.PUT("/settings.json", saveSettings) + router.DELETE("/settings.json", func(c *gin.Context) { + err := ResetSettings() + if err != nil { + log.Println("Unable to ResetSettings:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during setting reset."}) + return + } - router.POST("/api/reset", apiHandler(reset)) + c.JSON(http.StatusOK, true) + }) + + router.POST("/reset", reset) } -func getROSettings(_ httprouter.Params, body []byte) (interface{}, error) { +func getROSettings(c *gin.Context) { syncMtd := "Disabled" if sync.GlobalImporter != nil { syncMtd = sync.GlobalImporter.Kind() @@ -40,51 +49,70 @@ func getROSettings(_ httprouter.Params, body []byte) (interface{}, error) { syncId = sync.GlobalImporter.Id() } - return map[string]interface{}{ + c.JSON(http.StatusOK, gin.H{ "sync-type": reflect.TypeOf(sync.GlobalImporter).Name(), "sync-id": syncId, "sync": syncMtd, - }, nil + }) } -func getChallengeInfo(_ httprouter.Params, body []byte) (interface{}, error) { - return settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)) +func getChallengeInfo(c *gin.Context) { + s, err := settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)) + if err != nil { + log.Println("Unable to ReadChallengeInfo:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read challenge info: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, s) } -func saveChallengeInfo(_ httprouter.Params, body []byte) (interface{}, error) { +func saveChallengeInfo(c *gin.Context) { var info *settings.ChallengeInfo - if err := json.Unmarshal(body, &info); err != nil { - return nil, err + err := c.ShouldBindJSON(&info) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if err := settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), info); err != nil { - return nil, err - } else { - return info, err + log.Println("Unable to SaveChallengeInfo:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save challenge info: %s", err.Error())}) + return } + + c.JSON(http.StatusOK, info) } -func getSettings(_ httprouter.Params, body []byte) (interface{}, error) { - if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { - return nil, err - } else { - s.WorkInProgress = !IsProductionEnv - return s, nil +func getSettings(c *gin.Context) { + s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) + if err != nil { + log.Println("Unable to ReadSettings:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read settings: %s", err.Error())}) + return } + + s.WorkInProgress = !IsProductionEnv + c.Writer.Header().Add("X-FIC-Time", fmt.Sprintf("%d", time.Now().Unix())) + c.JSON(http.StatusOK, s) } -func saveSettings(_ httprouter.Params, body []byte) (interface{}, error) { +func saveSettings(c *gin.Context) { var config *settings.Settings - if err := json.Unmarshal(body, &config); err != nil { - return nil, err + err := c.ShouldBindJSON(&config) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if err := settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), config); err != nil { - return nil, err - } else { - ApplySettings(config) - return config, err + log.Println("Unable to SaveSettings:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save settings: %s", err.Error())}) + return } + + ApplySettings(config) + c.JSON(http.StatusOK, config) } func ApplySettings(config *settings.Settings) { @@ -160,25 +188,40 @@ func ResetChallengeInfo() error { }) } -func reset(_ httprouter.Params, body []byte) (interface{}, error) { +func reset(c *gin.Context) { var m map[string]string - if err := json.Unmarshal(body, &m); err != nil { - return nil, err + err := c.ShouldBindJSON(&m) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } - if t, ok := m["type"]; !ok { - return nil, errors.New("Field type not found") - } else if t == "teams" { - return true, fic.ResetTeams() - } else if t == "challenges" { - return true, fic.ResetExercices() - } else if t == "game" { - return true, fic.ResetGame() - } else if t == "settings" { - return true, ResetSettings() - } else if t == "challengeInfo" { - return true, ResetChallengeInfo() - } else { - return nil, errors.New("Unknown reset type") + t, ok := m["type"] + if !ok { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Field type not found"}) } + + switch t { + case "teams": + err = fic.ResetTeams() + case "challenges": + err = fic.ResetExercices() + case "game": + err = fic.ResetGame() + case "settings": + err = ResetSettings() + case "challengeInfo": + err = ResetChallengeInfo() + default: + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unknown reset type"}) + return + } + + if err != nil { + log.Printf("Unable to reset (type=%q): %s", t, err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to performe the reset: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, true) } diff --git a/admin/api/sync.go b/admin/api/sync.go new file mode 100644 index 00000000..d54bbf46 --- /dev/null +++ b/admin/api/sync.go @@ -0,0 +1,191 @@ +package api + +import ( + "fmt" + "log" + "net/http" + "strings" + + "srs.epita.fr/fic-server/admin/sync" + "srs.epita.fr/fic-server/libfic" + "srs.epita.fr/fic-server/settings" + + "github.com/gin-gonic/gin" +) + +func declareSyncRoutes(router *gin.RouterGroup) { + apiSyncRoutes := router.Group("/sync") + + // Base sync checks if the local directory is in sync with remote one. + apiSyncRoutes.POST("/base", func(c *gin.Context) { + err := sync.GlobalImporter.Sync() + if err != nil { + c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()}) + } else { + c.JSON(http.StatusOK, true) + } + }) + + // Speedy sync performs a recursive synchronization without importing files. + apiSyncRoutes.POST("/speed", func(c *gin.Context) { + st := sync.SpeedySyncDeep(sync.GlobalImporter) + sync.EditDeepReport(st, false) + c.JSON(http.StatusOK, st) + }) + + // Deep sync: a fully recursive synchronization (can be limited by theme). + apiSyncRoutes.GET("/deep", func(c *gin.Context) { + if sync.DeepSyncProgress == 0 { + c.AbortWithStatusJSON(http.StatusTooEarly, gin.H{"errmsg": "Pas de synchronisation en cours"}) + return + } + c.JSON(http.StatusOK, gin.H{"progress": sync.DeepSyncProgress}) + }) + apiSyncRoutes.POST("/deep", func(c *gin.Context) { + c.JSON(http.StatusOK, sync.SyncDeep(sync.GlobalImporter)) + }) + + apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid") + apiSyncDeepRoutes.Use(ThemeHandler) + apiSyncDeepRoutes.POST("", func(c *gin.Context) { + theme := c.MustGet("theme").(*fic.Theme) + + st := sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250) + sync.EditDeepReport(map[string][]string{theme.Name: st}, false) + sync.DeepSyncProgress = 255 + c.JSON(http.StatusOK, st) + }) + + // Auto sync: to use with continuous deployment, in a development env + apiSyncRoutes.POST("/auto/*p", autoSync) + + // Themes + apiSyncRoutes.POST("/fixurlids", fixAllURLIds) + + apiSyncRoutes.POST("/themes", func(c *gin.Context) { + c.JSON(http.StatusOK, sync.SyncThemes(sync.GlobalImporter)) + }) + + apiSyncThemesRoutes := apiSyncRoutes.Group("/themes/:thid") + apiSyncThemesRoutes.Use(ThemeHandler) + apiSyncThemesRoutes.POST("/fixurlid", func(c *gin.Context) { + theme := c.MustGet("theme").(*fic.Theme) + if theme.FixURLId() { + v, err := theme.Update() + if err != nil { + log.Println("Unable to UpdateTheme after fixurlid:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the theme."}) + return + } + + c.JSON(http.StatusOK, v) + } else { + c.AbortWithStatusJSON(http.StatusOK, 0) + } + }) + + // Exercices + declareSyncExercicesRoutes(apiSyncRoutes) + declareSyncExercicesRoutes(apiSyncThemesRoutes) +} + +func declareSyncExercicesRoutes(router *gin.RouterGroup) { + router.POST("/exercices", func(c *gin.Context) { + theme := c.MustGet("theme").(*fic.Theme) + c.JSON(http.StatusOK, sync.SyncExercices(sync.GlobalImporter, theme)) + }) + apiSyncExercicesRoutes := router.Group("/exercices/:eid") + apiSyncExercicesRoutes.Use(ExerciceHandler) + apiSyncExercicesRoutes.POST("", func(c *gin.Context) { + theme, exists := c.Get("theme") + if !exists { + c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "You should sync exercice only through a theme."}) + return + } + + exercice := c.MustGet("exercice").(*fic.Exercice) + + _, _, errs := sync.SyncExercice(sync.GlobalImporter, theme.(*fic.Theme), exercice.Path, nil) + c.JSON(http.StatusOK, errs) + }) + apiSyncExercicesRoutes.POST("/files", func(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + c.JSON(http.StatusOK, sync.SyncExerciceFiles(sync.GlobalImporter, exercice)) + }) + apiSyncExercicesRoutes.POST("/fixurlid", func(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + if exercice.FixURLId() { + v, err := exercice.Update() + if err != nil { + log.Println("Unable to UpdateExercice after fixurlid:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the exercice."}) + return + } + + c.JSON(http.StatusOK, v) + } else { + c.AbortWithStatusJSON(http.StatusOK, 0) + } + }) + apiSyncExercicesRoutes.POST("/hints", func(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + _, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice)) + c.JSON(http.StatusOK, errs) + }) + apiSyncExercicesRoutes.POST("/flags", func(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + _, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice) + _, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice)) + c.JSON(http.StatusOK, append(errs, herrs...)) + }) +} + +// autoSync tries to performs a smart synchronization, when in development environment. +// It'll sync most of modified things, and will delete out of sync data. +// Avoid using it in a production environment. +func autoSync(c *gin.Context) { + p := strings.TrimPrefix(c.Params.ByName("p"), "/") + + themes, err := fic.GetThemes() + if err != nil { + log.Println("Unable to GetThemes:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve theme list."}) + return + } + + if p == "" { + if !IsProductionEnv { + for _, theme := range themes { + theme.DeleteDeep() + } + } + + st := sync.SyncDeep(sync.GlobalImporter) + c.JSON(http.StatusOK, st) + return + } + + for _, theme := range themes { + if theme.Path == p { + if !IsProductionEnv { + exercices, err := theme.GetExercices() + if err == nil { + for _, exercice := range exercices { + exercice.DeleteDeep() + } + } + } + + st := sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250) + sync.EditDeepReport(map[string][]string{theme.Name: st}, false) + sync.DeepSyncProgress = 255 + + settings.ForceRegeneration() + + c.JSON(http.StatusOK, st) + return + } + } + + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Theme not found %q", p)}) +} diff --git a/admin/api/team.go b/admin/api/team.go index c6151d3c..b58d8a91 100644 --- a/admin/api/team.go +++ b/admin/api/team.go @@ -3,167 +3,312 @@ package api import ( "encoding/json" "fmt" + "log" "math/rand" + "net/http" + "strconv" "strings" "time" "srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/libfic" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/teams.json", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return fic.ExportTeams(false) - })) - router.GET("/api/teams-members.json", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return fic.ExportTeams(true) - })) - router.GET("/api/teams-binding", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return bindingTeams() - })) - router.GET("/api/teams-nginx", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return nginxGenTeams() - })) - router.POST("/api/disableinactiveteams", apiHandler(disableInactiveTeams)) - router.POST("/api/enableallteams", apiHandler(enableAllTeams)) - router.GET("/api/teams-members-nginx", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return nginxGenMember() - })) - router.GET("/api/teams-tries.json", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return fic.GetTries(nil, nil) - })) - - router.GET("/api/teams/", apiHandler( - func(httprouter.Params, []byte) (interface{}, error) { - return fic.GetTeams() - })) - router.POST("/api/teams/", apiHandler(createTeam)) - - router.GET("/api/teams/:tid/", apiHandler(teamHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return team, nil - }))) - router.PUT("/api/teams/:tid/", apiHandler(teamHandler(updateTeam))) - router.POST("/api/teams/:tid/", apiHandler(teamHandler(addTeamMember))) - router.DELETE("/api/teams/:tid/", apiHandler(teamHandler(deleteTeam))) - router.GET("/api/teams/:tid/score-grid.json", apiHandler(teamHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return team.ScoreGrid() - }))) - router.GET("/api/teams/:tid/my.json", apiHandler(teamPublicHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return fic.MyJSONTeam(team, true) - }))) - router.GET("/api/teams/:tid/wait.json", apiHandler(teamPublicHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return fic.MyJSONTeam(team, false) - }))) - router.GET("/api/teams/:tid/stats.json", apiHandler(teamPublicHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - if team != nil { - return team.GetStats() - } else { - return fic.GetTeamsStats(nil) - } - }))) - router.GET("/api/teams/:tid/history.json", apiHandler(teamPublicHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - if team != nil { - return team.GetHistory() - } else { - return fic.GetTeamsStats(nil) - } - }))) - router.DELETE("/api/teams/:tid/history.json", apiHandler(teamPublicHandler(delHistory))) - router.GET("/api/teams/:tid/tries", apiHandler(teamPublicHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return fic.GetTries(team, nil) - }))) - router.GET("/api/teams/:tid/members", apiHandler(teamHandler( - func(team *fic.Team, _ []byte) (interface{}, error) { - return team.GetMembers() - }))) - router.POST("/api/teams/:tid/members", apiHandler(teamHandler(addTeamMember))) - router.PUT("/api/teams/:tid/members", apiHandler(teamHandler(setTeamMember))) -} - -func nginxGenTeams() (string, error) { - if teams, err := fic.GetTeams(); err != nil { - return "", err - } else { - ret := "" - for _, team := range teams { - ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", strings.ToLower(team.Name), team.Id) +func declareTeamsRoutes(router *gin.RouterGroup) { + router.GET("/teams.json", func(c *gin.Context) { + teams, err := fic.ExportTeams(false) + if err != nil { + log.Println("Unable to ExportTeams:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."}) + return } - return ret, nil - } -} - -func nginxGenMember() (string, error) { - if teams, err := fic.GetTeams(); err != nil { - return "", err - } else { - ret := "" - for _, team := range teams { - if members, err := team.GetMembers(); err == nil { - for _, member := range members { - ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id) - } - } else { - return "", err - } + c.JSON(http.StatusOK, teams) + }) + router.GET("/teams-members.json", func(c *gin.Context) { + teams, err := fic.ExportTeams(true) + if err != nil { + log.Println("Unable to ExportTeams:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."}) + return } - return ret, nil - } -} - -func bindingTeams() (string, error) { - if teams, err := fic.GetTeams(); err != nil { - return "", err - } else { - ret := "" - for _, team := range teams { - if members, err := team.GetMembers(); err != nil { - return "", err - } else { - var mbs []string - for _, member := range members { - mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname)) - } - ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";")) - } + c.JSON(http.StatusOK, teams) + }) + router.GET("/teams-binding", bindingTeams) + router.GET("/teams-nginx", nginxGenTeams) + router.POST("/disableinactiveteams", disableInactiveTeams) + router.POST("/enableallteams", enableAllTeams) + router.GET("/teams-members-nginx", nginxGenMember) + router.GET("/teams-tries.json", func(c *gin.Context) { + tries, err := fic.GetTries(nil, nil) + if err != nil { + log.Println("Unable to GetTries:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."}) + return } - return ret, nil - } + + c.JSON(http.StatusOK, tries) + }) + + router.GET("/teams", func(c *gin.Context) { + teams, err := fic.GetTeams() + if err != nil { + log.Println("Unable to GetTeams:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams listing."}) + return + } + + c.JSON(http.StatusOK, teams) + }) + router.POST("/teams", createTeam) + + apiTeamsRoutes := router.Group("/teams/:tid") + apiTeamsRoutes.Use(TeamHandler) + apiTeamsRoutes.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("team").(*fic.Team)) + }) + apiTeamsRoutes.PUT("/", updateTeam) + apiTeamsRoutes.POST("/", addTeamMember) + apiTeamsRoutes.DELETE("/", deleteTeam) + apiTeamsRoutes.GET("/score-grid.json", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + sg, err := team.ScoreGrid() + if err != nil { + log.Printf("Unable to get ScoreGrid(tid=%d): %s", team.Id, err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during score grid calculation."}) + return + } + + c.JSON(http.StatusOK, sg) + }) + + apiTeamsPublicRoutes := router.Group("/teams/:tid") + apiTeamsPublicRoutes.Use(TeamPublicHandler) + apiTeamsPublicRoutes.GET("/my.json", func(c *gin.Context) { + tfile, err := fic.MyJSONTeam(c.MustGet("team").(*fic.Team), true) + if err != nil { + log.Println("Unable to get MyJSONTeam:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."}) + return + } + + c.JSON(http.StatusOK, tfile) + }) + apiTeamsPublicRoutes.GET("/wait.json", func(c *gin.Context) { + tfile, err := fic.MyJSONTeam(c.MustGet("team").(*fic.Team), false) + if err != nil { + log.Println("Unable to get MyJSONTeam:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."}) + return + } + + c.JSON(http.StatusOK, tfile) + }) + apiTeamsPublicRoutes.GET("/stats.json", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + if team != nil { + stats, err := team.GetStats() + if err != nil { + log.Println("Unable to get GetStats:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during stats calculation."}) + return + } + + c.JSON(http.StatusOK, stats) + } else { + stats, err := fic.GetTeamsStats(nil) + if err != nil { + log.Println("Unable to get GetTeamsStats:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during global stats calculation."}) + return + } + + c.JSON(http.StatusOK, stats) + } + }) + apiTeamsRoutes.GET("/history.json", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + history, err := team.GetHistory() + if err != nil { + log.Println("Unable to get GetHistory:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history calculation."}) + return + } + + c.JSON(http.StatusOK, history) + }) + apiTeamsRoutes.DELETE("/history.json", delHistory) + apiTeamsPublicRoutes.GET("/tries", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + tries, err := fic.GetTries(team, nil) + if err != nil { + log.Println("Unable to GetTries:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during tries calculation."}) + return + } + + c.JSON(http.StatusOK, tries) + }) + apiTeamsRoutes.GET("/members", func(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + members, err := team.GetMembers() + if err != nil { + log.Println("Unable to GetMembers:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during members retrieval."}) + return + } + + c.JSON(http.StatusOK, members) + }) + apiTeamsRoutes.POST("/members", addTeamMember) + apiTeamsRoutes.PUT("/members", setTeamMember) + + declareTeamsPasswordRoutes(apiTeamsRoutes) + declareTeamClaimsRoutes(apiTeamsRoutes) + declareTeamCertificateRoutes(apiTeamsRoutes) } -func createTeam(_ httprouter.Params, body []byte) (interface{}, error) { +func TeamHandler(c *gin.Context) { + tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"}) + return + } + + team, err := fic.GetTeam(tid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"}) + return + } + + c.Set("team", team) + + c.Next() +} + +func TeamPublicHandler(c *gin.Context) { + tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"}) + return + } + + if tid != 0 { + team, err := fic.GetTeam(tid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"}) + return + } + + c.Set("team", team) + } else { + c.Set("team", nil) + } + + c.Next() +} + +func nginxGenTeams(c *gin.Context) { + teams, err := fic.GetTeams() + if err != nil { + log.Println("Unable to GetTeams:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + ret := "" + for _, team := range teams { + ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", strings.ToLower(team.Name), team.Id) + } + + c.String(http.StatusOK, ret) +} + +func nginxGenMember(c *gin.Context) { + teams, err := fic.GetTeams() + if err != nil { + log.Println("Unable to GetTeams:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + ret := "" + for _, team := range teams { + if members, err := team.GetMembers(); err == nil { + for _, member := range members { + ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id) + } + } else { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + c.String(http.StatusOK, ret) +} + +func bindingTeams(c *gin.Context) { + teams, err := fic.GetTeams() + if err != nil { + log.Println("Unable to GetTeams:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + ret := "" + for _, team := range teams { + if members, err := team.GetMembers(); err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } else { + var mbs []string + for _, member := range members { + mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname)) + } + ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";")) + } + } + + c.String(http.StatusOK, ret) +} + +func createTeam(c *gin.Context) { var ut fic.Team - if err := json.Unmarshal(body, &ut); err != nil { - return nil, err + err := c.ShouldBindJSON(&ut) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if ut.Color == 0 { ut.Color = fic.HSL{rand.Float64(), 1, 0.5}.ToRGB() } - return fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId) + team, err := fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId) + if err != nil { + log.Println("Unable to CreateTeam:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team creation."}) + return + } + + c.JSON(http.StatusOK, team) } -func updateTeam(team *fic.Team, body []byte) (interface{}, error) { +func updateTeam(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + var ut fic.Team - if err := json.Unmarshal(body, &ut); err != nil { - return nil, err + err := c.ShouldBindJSON(&ut) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } ut.Id = team.Id @@ -172,102 +317,132 @@ func updateTeam(team *fic.Team, body []byte) (interface{}, error) { ut.Password = nil } - if _, err := ut.Update(); err != nil { - return nil, err + _, err = ut.Update() + if err != nil { + log.Println("Unable to updateTeam:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team updating."}) + return } - return ut, nil + c.JSON(http.StatusOK, team) } -func disableInactiveTeams(_ httprouter.Params, _ []byte) (interface{}, error) { - if teams, err := fic.GetTeams(); err != nil { - return nil, err - } else { - for _, team := range teams { - var serials []uint64 - serials, err = pki.GetTeamSerials(TeamsDir, team.Id) - if err != nil { - return nil, err - } +func disableInactiveTeams(c *gin.Context) { + teams, err := fic.GetTeams() + if err != nil { + log.Println("Unable to GetTeams:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, err) + return + } - var assocs []string - assocs, err = pki.GetTeamAssociations(TeamsDir, team.Id) - if err != nil { - return nil, err - } - - if len(serials) == 0 && len(assocs) == 0 { - if team.Active { - team.Active = false - team.Update() - } - } else if !team.Active { - team.Active = true - team.Update() - } + for _, team := range teams { + var serials []uint64 + serials, err = pki.GetTeamSerials(TeamsDir, team.Id) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return } - return true, nil - } -} - -func enableAllTeams(_ httprouter.Params, _ []byte) (interface{}, error) { - if teams, err := fic.GetTeams(); err != nil { - return nil, err - } else { - for _, team := range teams { - if !team.Active { - team.Active = true - team.Update() - } + var assocs []string + assocs, err = pki.GetTeamAssociations(TeamsDir, team.Id) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return } - return true, nil + if len(serials) == 0 && len(assocs) == 0 { + if team.Active { + team.Active = false + team.Update() + } + } else if !team.Active { + team.Active = true + team.Update() + } } + + c.JSON(http.StatusOK, true) } -func deleteTeam(team *fic.Team, _ []byte) (interface{}, error) { +func enableAllTeams(c *gin.Context) { + teams, err := fic.GetTeams() + if err != nil { + log.Println("Unable to GetTeams:", err.Error()) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + for _, team := range teams { + if !team.Active { + team.Active = true + team.Update() + } + } + + c.JSON(http.StatusOK, true) +} + +func deleteTeam(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id) if err != nil { - return nil, err + log.Printf("Unable to GetTeamAssociations(tid=%s): %s", team.Id, err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve team association."}) + return } for _, assoc := range assocs { err = pki.DeleteTeamAssociation(TeamsDir, assoc) + if err != nil { + log.Printf("Unable to DeleteTeamAssociation(assoc=%s): %s", assoc, err.Error()) + return + } } + _, err = team.Delete() if err != nil { - return nil, err + log.Println("Unable to deleteTeam:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team deletion."}) + return } - return team.Delete() + c.JSON(http.StatusOK, true) } -func addTeamMember(team *fic.Team, body []byte) (interface{}, error) { +func addTeamMember(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + var members []fic.Member - if err := json.Unmarshal(body, &members); err != nil { - return nil, err + err := c.ShouldBindJSON(&members) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } for _, member := range members { - team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company)) + _, err := team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company)) + if err != nil { + log.Println("Unable to AddMember:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during member creation."}) + return + } } - return team.GetMembers() + mmbrs, err := team.GetMembers() + if err != nil { + log.Println("Unable to retrieve members list:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve members list."}) + return + } + + c.JSON(http.StatusOK, mmbrs) } -func setTeamMember(team *fic.Team, body []byte) (interface{}, error) { - var members []fic.Member - if err := json.Unmarshal(body, &members); err != nil { - return nil, err - } - +func setTeamMember(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) team.ClearMembers() - for _, member := range members { - team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company)) - } - - return team.GetMembers() + addTeamMember(c) } type uploadedHistory struct { @@ -294,11 +469,21 @@ func updateHistory(team *fic.Team, body []byte) (interface{}, error) { return team.UpdateHistoryCoeff(uh.Kind, uh.Time, givenId, uh.Coefficient) } -func delHistory(team *fic.Team, body []byte) (interface{}, error) { +func delHistory(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + var uh uploadedHistory - if err := json.Unmarshal(body, &uh); err != nil { - return nil, err + err := c.ShouldBindJSON(&uh) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } - return team.DelHistoryItem(uh.Kind, uh.Time, uh.Primary, uh.Secondary) + _, err = team.DelHistoryItem(uh.Kind, uh.Time, uh.Primary, uh.Secondary) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete this history line: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, true) } diff --git a/admin/api/theme.go b/admin/api/theme.go index 42af449b..71fe0211 100644 --- a/admin/api/theme.go +++ b/admin/api/theme.go @@ -1,171 +1,75 @@ package api import ( - "encoding/json" - "errors" "fmt" + "log" + "net/http" "path" "strconv" - "strings" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/settings" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) -func init() { - router.GET("/api/themes", apiHandler(listThemes)) - router.POST("/api/themes", apiHandler(createTheme)) - router.GET("/api/themes.json", apiHandler(exportThemes)) - router.GET("/api/session-forensic.yaml", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { +func declareThemesRoutes(router *gin.RouterGroup) { + router.GET("/themes", listThemes) + router.POST("/themes", createTheme) + router.GET("/themes.json", exportThemes) + router.GET("/session-forensic.yaml", func(c *gin.Context) { if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { - return nil, err - } else if c, err := settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil { - return nil, err + log.Printf("Unable to ReadSettings: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during settings reading."}) + return + } else if ch, err := settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil { + log.Printf("Unable to ReadChallengeInfo: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during challenge info reading."}) + return + } else if sf, err := fic.GenZQDSSessionFile(ch, s); err != nil { + log.Printf("Unable to GenZQDSSessionFile: %s", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during session file generation."}) + return } else { - return fic.GenZQDSSessionFile(c, s) + c.JSON(http.StatusOK, sf) } - })) - router.GET("/api/files-bindings", apiHandler(bindingFiles)) + }) + router.GET("/files-bindings", bindingFiles) - router.GET("/api/themes/:thid", apiHandler(themeHandler(showTheme))) - router.PUT("/api/themes/:thid", apiHandler(themeHandler(updateTheme))) - router.DELETE("/api/themes/:thid", apiHandler(themeHandler(deleteTheme))) + apiThemesRoutes := router.Group("/themes/:thid") + apiThemesRoutes.Use(ThemeHandler) + apiThemesRoutes.GET("", showTheme) + apiThemesRoutes.PUT("", updateTheme) + apiThemesRoutes.DELETE("", deleteTheme) - router.GET("/api/themes/:thid/exercices", apiHandler(themeHandler(listThemedExercices))) - router.POST("/api/themes/:thid/exercices", apiHandler(themeHandler(createExercice))) - - router.GET("/api/themes/:thid/exercices_stats.json", apiHandler(themeHandler(getThemedExercicesStats))) - - router.GET("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(showExercice))) - router.PUT("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(updateExercice))) - router.DELETE("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(deleteExercice))) - - router.GET("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles))) - router.POST("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile))) - - router.GET("/api/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints))) - router.POST("/api/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint))) - - router.GET("/api/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(listExerciceFlags))) - router.POST("/api/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(createExerciceFlag))) + declareExercicesRoutes(apiThemesRoutes) // Remote - router.GET("/api/remote/themes", apiHandler(sync.ApiListRemoteThemes)) - router.GET("/api/remote/themes/:thid", apiHandler(sync.ApiGetRemoteTheme)) - router.GET("/api/remote/themes/:thid/exercices", apiHandler(sync.ApiListRemoteExercices)) - - // Synchronize - router.GET("/api/sync/deep", apiHandler( - func(_ httprouter.Params, _ []byte) (interface{}, error) { - if sync.DeepSyncProgress == 0 { - return nil, errors.New("Pas de synchronisation en cours") - } else { - return map[string]interface{}{"progress": sync.DeepSyncProgress}, nil - } - })) - router.POST("/api/sync/base", apiHandler( - func(_ httprouter.Params, _ []byte) (interface{}, error) { - err := sync.GlobalImporter.Sync() - return true, err - })) - router.POST("/api/sync/speed", apiHandler( - func(_ httprouter.Params, _ []byte) (interface{}, error) { - st := sync.SpeedySyncDeep(sync.GlobalImporter) - sync.EditDeepReport(st, false) - return st, nil - })) - router.POST("/api/sync/deep", apiHandler( - func(_ httprouter.Params, _ []byte) (interface{}, error) { - return sync.SyncDeep(sync.GlobalImporter), nil - })) - router.POST("/api/sync/deep/:thid", apiHandler(themeHandler( - func(theme *fic.Theme, _ []byte) (interface{}, error) { - st := sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250) - sync.EditDeepReport(map[string][]string{theme.Name: st}, false) - sync.DeepSyncProgress = 255 - return st, nil - }))) - router.POST("/api/sync/auto/*p", apiHandler( - func(ps httprouter.Params, _ []byte) (interface{}, error) { - p := strings.TrimPrefix(ps.ByName("p"), "/") - - themes, err := fic.GetThemes() - if err != nil { - return nil, err - } - - if p == "" { - if !IsProductionEnv { - for _, theme := range themes { - theme.DeleteDeep() - } - } - - st := sync.SyncDeep(sync.GlobalImporter) - return st, nil - } - - for _, theme := range themes { - if theme.Path == p { - if !IsProductionEnv { - exercices, err := theme.GetExercices() - if err == nil { - for _, exercice := range exercices { - exercice.DeleteDeep() - } - } - } - - st := sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250) - sync.EditDeepReport(map[string][]string{theme.Name: st}, false) - sync.DeepSyncProgress = 255 - - settings.ForceRegeneration() - - return st, nil - } - } - - return nil, fmt.Errorf("Theme not found %q", p) - })) - router.POST("/api/sync/themes", apiHandler( - func(_ httprouter.Params, _ []byte) (interface{}, error) { - return sync.SyncThemes(sync.GlobalImporter), nil - })) - router.POST("/api/sync/themes/:thid/exercices", apiHandler(themeHandler( - func(theme *fic.Theme, _ []byte) (interface{}, error) { - return sync.SyncExercices(sync.GlobalImporter, theme), nil - }))) - router.POST("/api/sync/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler( - func(exercice *fic.Exercice, _ []byte) (interface{}, error) { - return sync.SyncExerciceFiles(sync.GlobalImporter, exercice), nil - }))) - router.POST("/api/sync/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler( - func(exercice *fic.Exercice, _ []byte) (interface{}, error) { - _, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice)) - return errs, nil - }))) - router.POST("/api/sync/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler( - func(exercice *fic.Exercice, _ []byte) (interface{}, error) { - _, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice) - _, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice)) - return append(errs, herrs...), nil - }))) - - router.POST("/api/sync/themes/:thid/fixurlid", apiHandler(themeHandler( - func(theme *fic.Theme, _ []byte) (interface{}, error) { - if theme.FixURLId() { - return theme.Update() - } - return 0, nil - }))) - router.POST("/api/sync/fixurlids", apiHandler(fixAllURLIds)) + router.GET("/remote/themes", sync.ApiListRemoteThemes) + router.GET("/remote/themes/:thid", sync.ApiGetRemoteTheme) + router.GET("/remote/themes/:thid/exercices", sync.ApiListRemoteExercices) } -func fixAllURLIds(_ httprouter.Params, _ []byte) (interface{}, error) { +func ThemeHandler(c *gin.Context) { + thid, err := strconv.ParseInt(string(c.Params.ByName("thid")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid theme identifier"}) + return + } + + theme, err := fic.GetTheme(thid) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"}) + return + } + + c.Set("theme", theme) + + c.Next() +} + +func fixAllURLIds(c *gin.Context) { nbFix := 0 if themes, err := fic.GetThemes(); err == nil { for _, theme := range themes { @@ -185,19 +89,22 @@ func fixAllURLIds(_ httprouter.Params, _ []byte) (interface{}, error) { } } - return nbFix, nil + c.JSON(http.StatusOK, nbFix) } -func bindingFiles(_ httprouter.Params, body []byte) (interface{}, error) { - if files, err := fic.GetFiles(); err != nil { - return "", err - } else { - ret := "" - for _, file := range files { - ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path) - } - return ret, nil +func bindingFiles(c *gin.Context) { + files, err := fic.GetFiles() + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return } + + ret := "" + for _, file := range files { + ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path) + } + + c.String(http.StatusOK, ret) } func getExercice(args []string) (*fic.Exercice, error) { @@ -212,60 +119,92 @@ func getExercice(args []string) (*fic.Exercice, error) { } } -func listThemes(_ httprouter.Params, _ []byte) (interface{}, error) { - return fic.GetThemes() +func listThemes(c *gin.Context) { + themes, err := fic.GetThemes() + if err != nil { + log.Println("Unable to listThemes:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to list themes."}) + return + } + + c.JSON(http.StatusOK, themes) } -func exportThemes(_ httprouter.Params, _ []byte) (interface{}, error) { - return fic.ExportThemes() +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": "An error occurs when trying to export themes."}) + return + } + + c.JSON(http.StatusOK, themes) } -func showTheme(theme *fic.Theme, _ []byte) (interface{}, error) { - return theme, nil +func showTheme(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("theme").(*fic.Theme)) } -func listThemedExercices(theme *fic.Theme, _ []byte) (interface{}, error) { - return theme.GetExercices() -} - -func showThemedExercice(theme *fic.Theme, exercice fic.Exercice, body []byte) (interface{}, error) { - return exercice, nil -} - -func createTheme(_ httprouter.Params, body []byte) (interface{}, error) { +func createTheme(c *gin.Context) { var ut fic.Theme - if err := json.Unmarshal(body, &ut); err != nil { - return nil, err + err := c.ShouldBindJSON(&ut) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } if len(ut.Name) == 0 { - return nil, errors.New("Theme's name not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"}) + return } - return fic.CreateTheme(&ut) + th, err := fic.CreateTheme(&ut) + if err != nil { + log.Println("Unable to createTheme:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during theme creation."}) + return + } + + c.JSON(http.StatusOK, th) } -func updateTheme(theme *fic.Theme, body []byte) (interface{}, error) { +func updateTheme(c *gin.Context) { + theme := c.MustGet("theme").(*fic.Theme) + var ut fic.Theme - if err := json.Unmarshal(body, &ut); err != nil { - return nil, err + err := c.ShouldBindJSON(&ut) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return } ut.Id = theme.Id if len(ut.Name) == 0 { - return nil, errors.New("Theme's name not filled") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"}) + return } if _, err := ut.Update(); err != nil { - return nil, err - } else { - return ut, nil + log.Println("Unable to updateTheme:", err.Error()) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme update."}) + return } + + c.JSON(http.StatusOK, ut) } -func deleteTheme(theme *fic.Theme, _ []byte) (interface{}, error) { - return theme.Delete() +func deleteTheme(c *gin.Context) { + theme := c.MustGet("theme").(*fic.Theme) + + _, err := theme.Delete() + if err != nil { + log.Println("Unable to deleteTheme:", err.Error()) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme deletion."}) + return + } + + c.JSON(http.StatusOK, true) } func getThemedExercicesStats(theme *fic.Theme, body []byte) (interface{}, error) { diff --git a/admin/api/version.go b/admin/api/version.go index c31f4343..52cb0726 100644 --- a/admin/api/version.go +++ b/admin/api/version.go @@ -1,13 +1,15 @@ 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(_ httprouter.Params, body []byte) (interface{}, error) { - return map[string]interface{}{"version": 1.0}, nil +func showVersion(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"version": 1.0}) } diff --git a/admin/app.go b/admin/app.go new file mode 100644 index 00000000..0352cf2b --- /dev/null +++ b/admin/app.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + + "srs.epita.fr/fic-server/admin/api" + "srs.epita.fr/fic-server/settings" +) + +type App struct { + router *gin.Engine + srv *http.Server + cfg *settings.Settings + bind string +} + +func NewApp(cfg *settings.Settings, baseURL string, bind string) App { + if !cfg.WorkInProgress { + gin.SetMode(gin.ReleaseMode) + } + gin.ForceConsoleColor() + router := gin.Default() + + 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, cfg, baseURL) + } else { + declareStaticRoutes(router.Group(""), cfg, "") + } + + app := App{ + router: router, + bind: bind, + } + + return app +} + +func (app *App) Start() { + app.srv = &http.Server{ + Addr: app.bind, + Handler: app.router, + } + + log.Printf("Ready, listening on %s\n", app.bind) + 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/admin/main.go b/admin/main.go index 7e3765d8..cacd1961 100644 --- a/admin/main.go +++ b/admin/main.go @@ -1,9 +1,7 @@ package main import ( - "context" "flag" - "fmt" "io/fs" "log" "net/http" @@ -198,12 +196,12 @@ func main() { os.MkdirAll(settings.SettingsDir, 0777) // Initialize settings and load them + var config *settings.Settings if !settings.ExistsSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) { if err = api.ResetSettings(); err != nil { log.Fatal("Unable to initialize settings.json:", err) } } else { - var config *settings.Settings if config, err = settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { log.Fatal("Unable to read settings.json:", err) } else { @@ -231,21 +229,13 @@ func main() { 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)) + app := NewApp(config, baseURL, *bind) + go app.Start() // Wait shutdown signal <-interrupt log.Print("The service is shutting down...") - srv.Shutdown(context.Background()) + app.Stop() log.Println("done") } diff --git a/admin/static.go b/admin/static.go index 2b548fa5..8a0f837f 100644 --- a/admin/static.go +++ b/admin/static.go @@ -3,6 +3,7 @@ package main import ( "bytes" "embed" + "errors" "log" "net/http" "path" @@ -12,8 +13,9 @@ import ( "srs.epita.fr/fic-server/admin/api" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" + "srs.epita.fr/fic-server/settings" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" ) //go:embed static @@ -33,83 +35,83 @@ func genIndex(baseURL string) { } } -func serveIndex(w http.ResponseWriter, r *http.Request) { - w.Write(indexPage) +func serveIndex(c *gin.Context) { + c.Writer.Write(indexPage) } var staticFS http.FileSystem -func serveFile(w http.ResponseWriter, r *http.Request, url string) { - r.URL.Path = url - http.FileServer(staticFS).ServeHTTP(w, r) +func serveFile(c *gin.Context, url string) { + c.Request.URL.Path = url + http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request) } -func init() { - api.Router().GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) +func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseURL string) { + router.GET("/", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/claims/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/claims/*_", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/exercices/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/exercices/*_", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/events/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/events/*_", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/files", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/files", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/public/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/public/*_", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/pki/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/pki/*_", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/settings/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/settings/*_", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/teams/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/teams/*_", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/themes/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveIndex(w, r) + router.GET("/themes/*_", func(c *gin.Context) { + serveIndex(c) }) - api.Router().GET("/css/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveFile(w, r, 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) { - serveFile(w, r, 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) { - serveFile(w, r, 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) { - serveFile(w, r, 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) { - serveFile(w, r, r.URL.Path) + router.GET("/views/*_", func(c *gin.Context) { + serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL)) }) - api.Router().GET("/files/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.ServeFile(w, r, path.Join(fic.FilesDir, strings.TrimPrefix(r.URL.Path, "/files"))) + router.GET("/files/*_", func(c *gin.Context) { + http.ServeFile(c.Writer, c.Request, path.Join(fic.FilesDir, strings.TrimPrefix(c.Request.URL.Path, "/files"))) }) - api.Router().GET("/submissions/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.ServeFile(w, r, path.Join(api.TimestampCheck, strings.TrimPrefix(r.URL.Path, "/submissions"))) + router.GET("/submissions/*_", func(c *gin.Context) { + http.ServeFile(c.Writer, c.Request, path.Join(api.TimestampCheck, strings.TrimPrefix(c.Request.URL.Path, "/submissions"))) }) - api.Router().GET("/vids/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + router.GET("/vids/*_", func(c *gin.Context) { if importer, ok := sync.GlobalImporter.(sync.LocalImporter); ok { - http.ServeFile(w, r, path.Join(importer.Base, strings.TrimPrefix(r.URL.Path, "/vids"))) + http.ServeFile(c.Writer, c.Request, path.Join(importer.Base, strings.TrimPrefix(c.Request.URL.Path, "/vids"))) } else { - http.Error(w, "Only available with local importer.", 400) + c.AbortWithError(http.StatusBadRequest, errors.New("Only available with local importer.")) } }) - api.Router().GET("/check_import.html", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - serveFile(w, r, "check_import.html") + router.GET("/check_import.html", func(c *gin.Context) { + serveFile(c, "check_import.html") }) - api.Router().GET("/full_import_report.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.ServeFile(w, r, sync.DeepReportPath) + router.GET("/full_import_report.json", func(c *gin.Context) { + http.ServeFile(c.Writer, c.Request, sync.DeepReportPath) }) } diff --git a/admin/static/js/app.js b/admin/static/js/app.js index 47ac1810..f1f0f610 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -300,7 +300,7 @@ angular.module("FICApp") return $resource("api/exercices/:exerciceId/history.json", { exerciceId: '@id' }) }) .factory("ExercicesStats", function($resource) { - return $resource("api/themes/:themeId/exercices_stats.json", { themeId: '@id' }) + return $resource("api/exercices_stats.json", { themeId: '@id' }) }) .factory("ExerciceStats", function($resource) { return $resource("api/exercices/:exerciceId/stats.json", { exerciceId: '@id' }) @@ -1653,7 +1653,7 @@ angular.module("FICApp") }) .controller("ExercicesStatsController", function($scope, ExercicesStats) { - $scope.exercices = ExercicesStats.query({ themeId: $scope.theme.id }); + $scope.exercices = ExercicesStats.query(); }) .controller("ExerciceStatsController", function($scope, ExerciceStats, $routeParams) { diff --git a/admin/sync/exercice_files.go b/admin/sync/exercice_files.go index 3f189904..3d852bdd 100644 --- a/admin/sync/exercice_files.go +++ b/admin/sync/exercice_files.go @@ -4,11 +4,12 @@ import ( "bufio" "encoding/hex" "fmt" + "net/http" "path" "strings" "unicode" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" "srs.epita.fr/fic-server/libfic" ) @@ -146,10 +147,10 @@ func SyncExerciceFiles(i Importer, exercice *fic.Exercice) (errs []string) { } // ApiGetRemoteExerciceFiles is an accessor to remote exercice files list. -func ApiGetRemoteExerciceFiles(ps httprouter.Params, _ []byte) (interface{}, error) { - theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) +func ApiGetRemoteExerciceFiles(c *gin.Context) { + theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if theme != nil { - exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, ps.ByName("exid")), nil) + exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil) if exercice != nil { files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files") if files != nil { @@ -164,14 +165,17 @@ func ApiGetRemoteExerciceFiles(ps httprouter.Params, _ []byte) (interface{}, err Size: fSize, }) } - return ret, nil + c.JSON(http.StatusOK, ret) } else { - return nil, fmt.Errorf("%q", errs) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)}) + return } } else { - return nil, fmt.Errorf("%q", errs) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)}) + return } } else { - return nil, fmt.Errorf("%q", errs) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)}) + return } } diff --git a/admin/sync/exercice_hints.go b/admin/sync/exercice_hints.go index c0a03a8e..a0f4edc8 100644 --- a/admin/sync/exercice_hints.go +++ b/admin/sync/exercice_hints.go @@ -6,11 +6,12 @@ import ( "encoding/hex" "fmt" "io" + "net/http" "os" "path" "strings" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" _ "golang.org/x/crypto/blake2b" "srs.epita.fr/fic-server/libfic" @@ -140,21 +141,24 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int } // ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints. -func ApiGetRemoteExerciceHints(ps httprouter.Params, _ []byte) (interface{}, error) { - theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) +func ApiGetRemoteExerciceHints(c *gin.Context) { + theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if theme != nil { - exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, ps.ByName("exid")), nil) + exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil) if exercice != nil { hints, errs := CheckExerciceHints(GlobalImporter, exercice) if hints != nil { - return hints, nil - } else { - return hints, fmt.Errorf("%q", errs) + c.JSON(http.StatusOK, hints) + return } - } else { - return exercice, fmt.Errorf("%q", errs) + + c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) + return } - } else { - return nil, fmt.Errorf("%q", errs) + + c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) + return } + + c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) } diff --git a/admin/sync/exercice_keys.go b/admin/sync/exercice_keys.go index c7ffc972..d8b9a4e6 100644 --- a/admin/sync/exercice_keys.go +++ b/admin/sync/exercice_keys.go @@ -3,13 +3,14 @@ package sync import ( "fmt" "math/rand" + "net/http" "path" "sort" "strconv" "strings" "unicode" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" "srs.epita.fr/fic-server/libfic" ) @@ -554,21 +555,25 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice) (kmap map[int64]fic.F } // ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags. -func ApiGetRemoteExerciceFlags(ps httprouter.Params, _ []byte) (interface{}, error) { - theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) +func ApiGetRemoteExerciceFlags(c *gin.Context) { + theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if theme != nil { - exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, ps.ByName("exid")), nil) + exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil) if exercice != nil { flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}) if flags != nil { - return flags, nil - } else { - return flags, fmt.Errorf("%q", errs) + c.JSON(http.StatusOK, flags) + return } - } else { - return exercice, fmt.Errorf("%q", errs) + + c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) + return } - } else { - return nil, fmt.Errorf("%q", errs) + + c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) + return } + + c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) + return } diff --git a/admin/sync/exercices.go b/admin/sync/exercices.go index f337f128..0563664c 100644 --- a/admin/sync/exercices.go +++ b/admin/sync/exercices.go @@ -3,12 +3,13 @@ package sync import ( "fmt" "log" + "net/http" "path" "strconv" "strings" "github.com/BurntSushi/toml" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" "github.com/russross/blackfriday/v2" "srs.epita.fr/fic-server/libfic" @@ -308,26 +309,36 @@ func SyncExercices(i Importer, theme *fic.Theme) (errs []string) { } // ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list. -func ApiListRemoteExercices(ps httprouter.Params, _ []byte) (interface{}, error) { - theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) +func ApiListRemoteExercices(c *gin.Context) { + theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if theme != nil { - return GetExercices(GlobalImporter, theme) + exercices, err := GetExercices(GlobalImporter, theme) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, exercices) } else { - return nil, fmt.Errorf("%q", errs) + c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) + return } } // ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes. -func ApiGetRemoteExercice(ps httprouter.Params, _ []byte) (interface{}, error) { - theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) +func ApiGetRemoteExercice(c *gin.Context) { + theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if theme != nil { - exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, ps.ByName("exid")), nil) + exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil) if exercice != nil { - return exercice, nil + c.JSON(http.StatusOK, exercice) + return } else { - return exercice, fmt.Errorf("%q", errs) + c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)}) + return } } else { - return nil, fmt.Errorf("%q", errs) + c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)}) + return } } diff --git a/admin/sync/themes.go b/admin/sync/themes.go index d6ee3421..bbadaa4e 100644 --- a/admin/sync/themes.go +++ b/admin/sync/themes.go @@ -5,13 +5,14 @@ import ( "image" "image/jpeg" "math/rand" + "net/http" "os" "path" "regexp" "strings" "unicode" - "github.com/julienschmidt/httprouter" + "github.com/gin-gonic/gin" "github.com/russross/blackfriday/v2" "golang.org/x/image/draw" @@ -235,16 +236,23 @@ func SyncThemes(i Importer) (errs []string) { } // ApiListRemoteThemes is an accessor letting foreign packages to access remote themes list. -func ApiListRemoteThemes(_ httprouter.Params, _ []byte) (interface{}, error) { - return GetThemes(GlobalImporter) +func ApiListRemoteThemes(c *gin.Context) { + themes, err := GetThemes(GlobalImporter) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + c.JSON(http.StatusOK, themes) } // ApiListRemoteTheme is an accessor letting foreign packages to access remote main theme attributes. -func ApiGetRemoteTheme(ps httprouter.Params, _ []byte) (interface{}, error) { - r, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) +func ApiGetRemoteTheme(c *gin.Context) { + r, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if r == nil { - return r, fmt.Errorf("%q", errs) - } else { - return r, nil + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)}) + return } + + c.JSON(http.StatusOK, r) } diff --git a/go.mod b/go.mod index 568f246a..f6846f7b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/BurntSushi/toml v1.1.0 github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/gin-gonic/gin v1.7.7 // indirect github.com/go-git/go-git/v5 v5.4.2 github.com/go-sql-driver/mysql v1.6.0 github.com/julienschmidt/httprouter v1.3.0 diff --git a/go.sum b/go.sum index 3a792777..e13a032d 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,10 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +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.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= @@ -78,6 +82,13 @@ github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -114,6 +125,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -134,6 +146,8 @@ github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= @@ -147,9 +161,17 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -164,6 +186,7 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8 h1:ipNUBPHSUmHhhcLhvqC2vGZsJPzVuJap8rJx3uGAEco= @@ -172,6 +195,10 @@ github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df h1:C+J/LwTqP8g github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 h1:b2nJXyPCa9HY7giGM+kYcnQ71m14JnGdQabMPmyt++8= github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -345,6 +372,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -509,6 +537,8 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/libfic/file.go b/libfic/file.go index ff9c83de..d60813ed 100644 --- a/libfic/file.go +++ b/libfic/file.go @@ -74,6 +74,12 @@ func GetFile(id int64) (f *EFile, err error) { return } +func (e *Exercice) GetFile(id int64) (f *EFile, err error) { + f = &EFile{} + err = DBQueryRow("SELECT id_file, origin, path, name, cksum, size FROM exercice_files WHERE id_file = ? AND id_exercice = ?", id, e.Id).Scan(&f.Id, &f.origin, &f.Path, &f.Name, &f.Checksum, &f.Size) + return +} + // GetFileByPath retrieves the file that should be found at the given location. func GetFileByPath(path string) (*EFile, error) { path = strings.TrimPrefix(path, FilesDir) diff --git a/libfic/flag_key.go b/libfic/flag_key.go index 1ba98246..6e225526 100644 --- a/libfic/flag_key.go +++ b/libfic/flag_key.go @@ -78,6 +78,13 @@ func GetFlagKey(id int) (k *FlagKey, err error) { return } +// GetFlagKey returns a flag. +func (e *Exercice) GetFlagKey(id int) (k *FlagKey, err error) { + k = &FlagKey{} + err = DBQueryRow("SELECT id_flag, id_exercice, ordre, label, type, placeholder, help, unit, ignorecase, notrim, multiline, validator_regexp, sort_re_grps, cksum, choices_cost FROM exercice_flags WHERE id_flag = ? AND id_exercice = ?", id, e.Id).Scan(&k.Id, &k.IdExercice, &k.Order, &k.Label, &k.Type, &k.Placeholder, &k.Help, &k.Unit, &k.IgnoreCase, &k.NoTrim, &k.Multiline, &k.ValidatorRegexp, &k.SortReGroups, &k.Checksum, &k.ChoicesCost) + return +} + // GetFlagKeyByLabel returns a flag matching the given label. func (e *Exercice) GetFlagKeyByLabel(label string) (k *FlagKey, err error) { k = &FlagKey{} diff --git a/libfic/hint.go b/libfic/hint.go index a36132af..db754e07 100644 --- a/libfic/hint.go +++ b/libfic/hint.go @@ -46,6 +46,17 @@ func GetHint(id int64) (*EHint, error) { return h, nil } +// GetHint retrieves the hint with the given id. +func (e *Exercice) GetHint(id int64) (*EHint, error) { + h := &EHint{} + if err := DBQueryRow("SELECT id_hint, id_exercice, title, content, cost FROM exercice_hints WHERE id_hint = ? AND id_exercice = ?", id, e.Id).Scan(&h.Id, &h.IdExercice, &h.Title, &h.Content, &h.Cost); err != nil { + return nil, err + } + treatHintContent(h) + + return h, nil +} + // GetHintByTitle retrieves the hint with the given id. func (e *Exercice) GetHintByTitle(id int64) (*EHint, error) { h := &EHint{} diff --git a/libfic/mcq.go b/libfic/mcq.go index d1ea52f3..d9770616 100644 --- a/libfic/mcq.go +++ b/libfic/mcq.go @@ -31,7 +31,7 @@ type MCQ_entry struct { // GetMCQ returns a list of flags comming with the challenge. func GetMCQ(id int) (m *MCQ, err error) { m = &MCQ{} - err = DBQueryRow("SELECT id_mcq, id_exercice, order, title FROM exercice_mcq WHERE id_mcq = ?", id).Scan(&m.Id, &m.IdExercice, &m.Order, &m.Title) + err = DBQueryRow("SELECT id_mcq, id_exercice, ordre, title FROM exercice_mcq WHERE id_mcq = ?", id).Scan(&m.Id, &m.IdExercice, &m.Order, &m.Title) m.fillEntries() return } @@ -84,6 +84,14 @@ func (e *Exercice) GetMCQ() ([]*MCQ, error) { } } +// GetMCQById returns a MCQs. +func (e *Exercice) GetMCQById(id int) (m *MCQ, err error) { + m = &MCQ{} + err = DBQueryRow("SELECT id_mcq, id_exercice, ordre, title FROM exercice_mcq WHERE id_mcq = ? AND id_exercice = ?", id, e.Id).Scan(&m.Id, &m.IdExercice, &m.Order, &m.Title) + m.fillEntries() + return +} + // GetMCQbyChoice returns the MCQ corresponding to a choice ID. func GetMCQbyChoice(cid int) (m *MCQ, c *MCQ_entry, err error) { m = &MCQ{}