admin: Use gin-gonic as router

This commit is contained in:
nemunaire 2022-05-16 11:38:46 +02:00
parent 83468ad723
commit 8b3fbdb64a
32 changed files with 2785 additions and 1635 deletions

View file

@ -7,13 +7,13 @@ import (
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/base32" "encoding/base32"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math" "math"
"math/big" "math/big"
"net/http"
"os" "os"
"path" "path"
"strconv" "strconv"
@ -23,53 +23,106 @@ import (
"srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var TeamsDir string var TeamsDir string
func init() { func declareCertificateRoutes(router *gin.RouterGroup) {
router.GET("/api/htpasswd", apiHandler( router.GET("/htpasswd", func(c *gin.Context) {
func(httprouter.Params, []byte) (interface{}, error) { ret, err := genHtpasswd(true)
return genHtpasswd(true) if err != nil {
})) c.AbortWithError(http.StatusInternalServerError, err)
router.POST("/api/htpasswd", apiHandler( return
func(httprouter.Params, []byte) (interface{}, error) { }
c.String(http.StatusOK, ret)
})
router.POST("/htpasswd", func(c *gin.Context) {
if htpasswd, err := genHtpasswd(true); err != nil { if htpasswd, err := genHtpasswd(true); err != nil {
return nil, err 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 { } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "ficpasswd"), []byte(htpasswd), 0644); err != nil {
return nil, err log.Println("Unable to write htpasswd:", err)
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return true, nil return
} }
})) c.AbortWithStatus(http.StatusOK)
router.DELETE("/api/htpasswd", apiHandler( })
func(httprouter.Params, []byte) (interface{}, error) { router.DELETE("/htpasswd", func(c *gin.Context) {
if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil { if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil {
return nil, err log.Println("Unable to remove htpasswd:", err)
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return true, nil return
} }
})) c.AbortWithStatus(http.StatusOK)
router.GET("/api/htpasswd.apr1", apiHandler( })
func(httprouter.Params, []byte) (interface{}, error) { router.GET("/htpasswd.apr1", func(c *gin.Context) {
return genHtpasswd(false) ret, err := genHtpasswd(false)
})) if err != nil {
router.GET("/api/ca/", apiHandler(infoCA)) c.AbortWithError(http.StatusInternalServerError, err)
router.GET("/api/ca.pem", apiHandler(getCAPEM)) return
router.POST("/api/ca/new", apiHandler( }
func(_ httprouter.Params, body []byte) (interface{}, error) { 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 var upki PKISettings
if err := json.Unmarshal(body, &upki); err != nil { err := c.ShouldBindJSON(&upki)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
return true, pki.GenerateCA(upki.NotBefore, upki.NotAfter)
}))
router.GET("/api/teams/:tid/certificates", apiHandler(teamHandler( if err := pki.GenerateCA(upki.NotBefore, upki.NotAfter); err != nil {
func(team *fic.Team, _ []byte) (interface{}, error) { 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 { if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil {
return nil, err log.Println("Unable to GetTeamSerials:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else { } else {
var certs []CertExported var certs []CertExported
for _, serial := range serials { for _, serial := range serials {
@ -79,35 +132,69 @@ func init() {
log.Println("Unable to get back certificate, whereas an association exists on disk: ", err) 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( router.GET("/associations", func(c *gin.Context) {
func(team *fic.Team, _ []byte) (interface{}, error) { team := c.MustGet("team").(*fic.Team)
return pki.GetTeamAssociations(TeamsDir, team.Id)
}))) assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
router.POST("/api/teams/:tid/associations/:assoc", apiHandler(teamAssocHandler( if err != nil {
func(team *fic.Team, assoc string, _ []byte) (interface{}, error) { log.Println("Unable to GetTeamAssociations:", err.Error())
if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, assoc)); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return nil, err return
} }
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("/api/certs/", apiHandler(getCertificates)) c.JSON(http.StatusOK, assocs)
router.POST("/api/certs/", apiHandler(generateClientCert)) })
router.DELETE("/api/certs/", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { return fic.ClearCertificates() }))
router.HEAD("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File))) apiTeamAssociationsRoutes := router.Group("/associations/:assoc")
router.GET("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File))) apiTeamAssociationsRoutes.POST("", func(c *gin.Context) {
router.PUT("/api/certs/:certid", apiHandler(certificateHandler(updateCertificateAssociation))) team := c.MustGet("team").(*fic.Team)
router.DELETE("/api/certs/:certid", apiHandler(certificateHandler(
func(cert *fic.Certificate, _ []byte) (interface{}, error) { return cert.Revoke() }))) 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) { func genHtpasswd(ssha bool) (ret string, err error) {
@ -187,13 +274,14 @@ type PKISettings struct {
PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"publicKeyAlgorithm"` PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"publicKeyAlgorithm"`
} }
func infoCA(_ httprouter.Params, _ []byte) (interface{}, error) { func infoCA(c *gin.Context) {
_, cacert, err := pki.LoadCA() _, cacert, err := pki.LoadCA()
if err != nil { 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, Version: cacert.Version,
SerialNumber: cacert.SerialNumber, SerialNumber: cacert.SerialNumber,
Issuer: cacert.Issuer, Issuer: cacert.Issuer,
@ -202,47 +290,78 @@ func infoCA(_ httprouter.Params, _ []byte) (interface{}, error) {
NotAfter: cacert.NotAfter, NotAfter: cacert.NotAfter,
SignatureAlgorithm: cacert.SignatureAlgorithm, SignatureAlgorithm: cacert.SignatureAlgorithm,
PublicKeyAlgorithm: cacert.PublicKeyAlgorithm, 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) { 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 { } 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 { } else {
defer fd.Close() 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 // Create p12 if necessary
if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) { if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
if err := pki.WriteP12(cert.Id, cert.Password); err != nil { 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) { 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 { } 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 { } else {
defer fd.Close() 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 // First, generate a new, unique, serial
var serial_gen [8]byte var serial_gen [8]byte
if _, err := rand.Read(serial_gen[:]); err != nil { 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) { for fic.ExistingCertSerial(serial_gen) {
if _, err := rand.Read(serial_gen[:]); err != nil { 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 // Let's pick a random password
password, err := fic.GeneratePassword() password, err := fic.GeneratePassword()
if err != nil { 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 // Ok, now load CA
capriv, cacert, err := pki.LoadCA() capriv, cacert, err := pki.LoadCA()
if err != nil { 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 // Generate our privkey
if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil { 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 // Save in DB
cert, err := fic.RegisterCertificate(serial, password) 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 { type CertExported struct {
@ -280,10 +411,13 @@ type CertExported struct {
Revoked *time.Time `json:"revoked"` Revoked *time.Time `json:"revoked"`
} }
func getCertificates(_ httprouter.Params, _ []byte) (interface{}, error) { func getCertificates(c *gin.Context) {
if certificates, err := fic.GetCertificates(); err != nil { certificates, err := fic.GetCertificates()
return nil, err if err != nil {
} else { 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) ret := make([]CertExported, 0)
for _, cert := range certificates { for _, cert := range certificates {
dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id)) dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))
@ -297,18 +431,22 @@ func getCertificates(_ httprouter.Params, _ []byte) (interface{}, error) {
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}) 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
} c.JSON(http.StatusOK, ret)
} }
type CertUploaded struct { type CertUploaded struct {
Team *int64 `json:"id_team"` 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 var uc CertUploaded
if err := json.Unmarshal(body, &uc); err != nil { err := c.ShouldBindJSON(&uc)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id)) dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))
@ -316,19 +454,26 @@ func updateCertificateAssociation(cert *fic.Certificate, body []byte) (interface
if uc.Team != nil { if uc.Team != nil {
srcLinkPath := fmt.Sprintf("%d", *uc.Team) srcLinkPath := fmt.Sprintf("%d", *uc.Team)
if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil { 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 // Mark team as active to ensure it'll be generated
if ut, err := fic.GetTeam(*uc.Team); err != nil { 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 { } else if !ut.Active {
ut.Active = true ut.Active = true
ut.Update() _, err := ut.Update()
if err != nil {
log.Println("Unable to UpdateTeam after updateCertificateAssociation:", err.Error())
}
} }
} else { } else {
os.Remove(dstLinkPath) os.Remove(dstLinkPath)
} }
return cert, nil c.JSON(http.StatusOK, cert)
} }

View file

@ -2,62 +2,152 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http"
"path" "path"
"strconv"
"time" "time"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareClaimsRoutes(router *gin.RouterGroup) {
router.GET("/api/teams/:tid/issue.json", apiHandler(teamHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return team.MyIssueFile()
})))
// Tasks // Tasks
router.GET("/api/claims", apiHandler(getClaims)) router.GET("/claims", getClaims)
router.POST("/api/claims", apiHandler(newClaim)) router.POST("/claims", newClaim)
router.DELETE("/api/claims", apiHandler(clearClaims)) router.DELETE("/claims", 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("/api/claims/:cid", apiHandler(claimHandler(showClaim))) apiClaimsRoutes := router.Group("/claims/:cid")
router.PUT("/api/claims/:cid", apiHandler(claimHandler(updateClaim))) apiClaimsRoutes.Use(ClaimHandler)
router.POST("/api/claims/:cid", apiHandler(claimHandler(addClaimDescription))) apiClaimsRoutes.GET("", showClaim)
router.DELETE("/api/claims/:cid", apiHandler(claimHandler(deleteClaim))) apiClaimsRoutes.PUT("", updateClaim)
apiClaimsRoutes.POST("", addClaimDescription)
apiClaimsRoutes.DELETE("", deleteClaim)
router.GET("/api/claims/:cid/last_update", apiHandler(claimHandler(getClaimLastUpdate))) apiClaimsRoutes.GET("/last_update", getClaimLastUpdate)
router.PUT("/api/claims/:cid/descriptions", apiHandler(claimHandler(updateClaimDescription))) apiClaimsRoutes.PUT("/descriptions", updateClaimDescription)
// Assignees // Assignees
router.GET("/api/claims-assignees", apiHandler(getAssignees)) router.GET("/claims-assignees", getAssignees)
router.POST("/api/claims-assignees", apiHandler(newAssignee)) router.POST("/claims-assignees", newAssignee)
router.GET("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(showClaimAssignee))) apiClaimAssigneesRoutes := router.Group("/claims-assignees/:aid")
router.PUT("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(updateClaimAssignee))) apiClaimAssigneesRoutes.Use(ClaimAssigneeHandler)
router.DELETE("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(deleteClaimAssignee))) 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) { func declareExerciceClaimsRoutes(router *gin.RouterGroup) {
return fic.GetClaims() router.GET("/claims", getExerciceClaims)
} }
func getTeamClaims(team *fic.Team, _ []byte) (interface{}, error) { func declareTeamClaimsRoutes(router *gin.RouterGroup) {
return team.GetClaims() 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
} }
func getExerciceClaims(exercice *fic.Exercice, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, issues)
return exercice.GetClaims() })
router.GET("/claims", getTeamClaims)
} }
func getClaimLastUpdate(claim *fic.Claim, _ []byte) (interface{}, error) { func ClaimHandler(c *gin.Context) {
return claim.GetLastUpdate() 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 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 { type ClaimExported struct {
@ -76,20 +166,26 @@ type ClaimExported struct {
Descriptions []*fic.ClaimDescription `json:"descriptions"` 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 e ClaimExported
var err error var err error
if e.Team, err = claim.GetTeam(); err != nil { 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 { 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 { 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 { 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 e.LastUpdate = e.Creation
@ -107,7 +203,8 @@ func showClaim(claim *fic.Claim, _ []byte) (interface{}, error) {
e.Creation = claim.Creation e.Creation = claim.Creation
e.State = claim.State e.State = claim.State
e.Priority = claim.Priority e.Priority = claim.Priority
return e, nil
c.JSON(http.StatusOK, e)
} }
type ClaimUploaded struct { type ClaimUploaded struct {
@ -115,20 +212,24 @@ type ClaimUploaded struct {
Whoami *int64 `json:"whoami"` Whoami *int64 `json:"whoami"`
} }
func newClaim(_ httprouter.Params, body []byte) (interface{}, error) { func newClaim(c *gin.Context) {
var uc ClaimUploaded var uc ClaimUploaded
if err := json.Unmarshal(body, &uc); err != nil { err := c.ShouldBindJSON(&uc)
return nil, fmt.Errorf("Unable to decode JSON: %w", err) if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
if uc.Subject == "" { 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 var t *fic.Team
if uc.IdTeam != nil { if uc.IdTeam != nil {
if team, err := fic.GetTeam(*uc.IdTeam); err != 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 { } else {
t = team t = team
} }
@ -139,7 +240,8 @@ func newClaim(_ httprouter.Params, body []byte) (interface{}, error) {
var e *fic.Exercice var e *fic.Exercice
if uc.IdExercice != nil { if uc.IdExercice != nil {
if exercice, err := fic.GetExercice(*uc.IdExercice); err != 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 { } else {
e = exercice e = exercice
} }
@ -150,7 +252,8 @@ func newClaim(_ httprouter.Params, body []byte) (interface{}, error) {
var a *fic.ClaimAssignee var a *fic.ClaimAssignee
if uc.IdAssignee != nil { if uc.IdAssignee != nil {
if assignee, err := fic.GetAssignee(*uc.IdAssignee); err != 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 { } else {
a = assignee a = assignee
} }
@ -162,11 +265,25 @@ func newClaim(_ httprouter.Params, body []byte) (interface{}, error) {
uc.Priority = "medium" 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
} }
func clearClaims(_ httprouter.Params, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, claim)
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 { func generateTeamIssuesFile(team fic.Team) error {
@ -180,53 +297,83 @@ func generateTeamIssuesFile(team fic.Team) error {
return nil 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 var ud fic.ClaimDescription
if err := json.Unmarshal(body, &ud); err != nil { err := c.ShouldBindJSON(&ud)
return nil, fmt.Errorf("Unable to decode JSON: %w", err) if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
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 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 { if team, _ := claim.GetTeam(); team != nil {
err = generateTeamIssuesFile(*team) err = generateTeamIssuesFile(*team)
} if err != nil {
log.Println("Unable to generateTeamIssuesFile after addClaimDescription:", err.Error())
return description, err
} }
} }
func updateClaimDescription(claim *fic.Claim, body []byte) (interface{}, error) { c.JSON(http.StatusOK, description)
}
func updateClaimDescription(c *gin.Context) {
claim := c.MustGet("claim").(*fic.Claim)
var ud fic.ClaimDescription var ud fic.ClaimDescription
if err := json.Unmarshal(body, &ud); err != nil { err := c.ShouldBindJSON(&ud)
return nil, fmt.Errorf("Unable to decode JSON: %w", err) if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
if _, err := ud.Update(); err != nil { if _, err := ud.Update(); err != nil {
return nil, fmt.Errorf("Unable to update description: %w", err) log.Println("Unable to updateClaimDescription:", err.Error())
} else { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during claim description updating."})
return
}
if team, _ := claim.GetTeam(); team != nil { if team, _ := claim.GetTeam(); team != nil {
err = generateTeamIssuesFile(*team) err = generateTeamIssuesFile(*team)
} if err != nil {
log.Println("Unable to generateTeamIssuesFile:", err.Error())
return ud, err
} }
} }
func updateClaim(claim *fic.Claim, body []byte) (interface{}, error) { c.JSON(http.StatusOK, ud)
}
func updateClaim(c *gin.Context) {
claim := c.MustGet("claim").(*fic.Claim)
var uc ClaimUploaded var uc ClaimUploaded
if err := json.Unmarshal(body, &uc); err != nil { err := c.ShouldBindJSON(&uc)
return nil, fmt.Errorf("Unable to decode JSON: %w", err) if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
uc.Id = claim.Id uc.Id = claim.Id
if _, err := uc.Update(); err != nil { _, err = uc.Update()
return nil, fmt.Errorf("Unable to update claim: %w", err) if err != nil {
} else { 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 claim.State != uc.State {
if uc.Whoami != nil { if uc.Whoami != nil {
if assignee, err := fic.GetAssignee(*uc.Whoami); err == nil { if assignee, err := fic.GetAssignee(*uc.Whoami); err == nil {
@ -257,45 +404,82 @@ func updateClaim(claim *fic.Claim, body []byte) (interface{}, error) {
err = generateTeamIssuesFile(*team) 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) { func getAssignees(c *gin.Context) {
return claim.Delete() 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
} }
func getAssignees(_ httprouter.Params, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, assignees)
return fic.GetAssignees()
} }
func showClaimAssignee(assignee *fic.ClaimAssignee, _ []byte) (interface{}, error) { func showClaimAssignee(c *gin.Context) {
return assignee, nil c.JSON(http.StatusOK, c.MustGet("claim-assignee").(*fic.ClaimAssignee))
} }
func newAssignee(_ httprouter.Params, body []byte) (interface{}, error) { func newAssignee(c *gin.Context) {
var ua fic.ClaimAssignee var ua fic.ClaimAssignee
if err := json.Unmarshal(body, &ua); err != nil { err := c.ShouldBindJSON(&ua)
return nil, fmt.Errorf("Unable to decode JSON: %w", err) if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
return fic.NewClaimAssignee(ua.Name) 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
} }
func updateClaimAssignee(assignee *fic.ClaimAssignee, body []byte) (interface{}, error) { c.JSON(http.StatusOK, assignee)
}
func updateClaimAssignee(c *gin.Context) {
assignee := c.MustGet("claim-assignee").(*fic.ClaimAssignee)
var ua fic.ClaimAssignee var ua fic.ClaimAssignee
if err := json.Unmarshal(body, &ua); err != nil { err := c.ShouldBindJSON(&ua)
return nil, fmt.Errorf("Unable to decode JSON: %w", err) if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
ua.Id = assignee.Id ua.Id = assignee.Id
if _, err := ua.Update(); err != nil { if _, err := ua.Update(); err != nil {
return nil, fmt.Errorf("Unable to update claim assignee: %w", err) log.Println("Unable to updateClaimAssignee:", err.Error())
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim assignee update."})
return ua, nil return
}
} }
func deleteClaimAssignee(assignee *fic.ClaimAssignee, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, ua)
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)
} }

View file

@ -3,22 +3,45 @@ package api
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"net/http"
"path" "path"
"strconv"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareEventsRoutes(router *gin.RouterGroup) {
router.GET("/api/events/", apiHandler(getEvents)) router.GET("/events", getEvents)
router.GET("/api/events.json", apiHandler(getLastEvents)) router.GET("/events.json", getLastEvents)
router.POST("/api/events/", apiHandler(newEvent)) router.POST("/events", newEvent)
router.DELETE("/api/events/", apiHandler(clearEvents)) router.DELETE("/events", clearEvents)
router.GET("/api/events/:evid", apiHandler(eventHandler(showEvent))) apiEventsRoutes := router.Group("/events/:evid")
router.PUT("/api/events/:evid", apiHandler(eventHandler(updateEvent))) apiEventsRoutes.Use(EventHandler)
router.DELETE("/api/events/:evid", apiHandler(eventHandler(deleteEvent))) 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 { func genEventsFile() error {
@ -33,65 +56,99 @@ func genEventsFile() error {
return nil return nil
} }
func getEvents(_ httprouter.Params, _ []byte) (interface{}, error) { func getEvents(c *gin.Context) {
if evts, err := fic.GetEvents(); err != nil { evts, err := fic.GetEvents()
return nil, err if err != nil {
} else { log.Println("Unable to GetEvents:", err.Error())
return evts, nil c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve events list"})
} return
} }
func getLastEvents(_ httprouter.Params, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, evts)
if evts, err := fic.GetLastEvents(); err != nil {
return nil, err
} else {
return evts, nil
}
} }
func showEvent(event *fic.Event, _ []byte) (interface{}, error) { func getLastEvents(c *gin.Context) {
return event, nil 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
} }
func newEvent(_ httprouter.Params, body []byte) (interface{}, error) { c.JSON(http.StatusOK, evts)
}
func newEvent(c *gin.Context) {
var ue fic.Event var ue fic.Event
if err := json.Unmarshal(body, &ue); err != nil { err := c.ShouldBindJSON(&ue)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
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
} }
if event, err := fic.NewEvent(ue.Text, ue.Kind); err != nil {
return nil, err
} else {
genEventsFile() genEventsFile()
return event, nil
} c.JSON(http.StatusOK, event)
} }
func clearEvents(_ httprouter.Params, _ []byte) (interface{}, error) { func clearEvents(c *gin.Context) {
return fic.ClearEvents() 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
} }
func updateEvent(event *fic.Event, body []byte) (interface{}, error) { c.JSON(http.StatusOK, nb)
}
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 var ue fic.Event
if err := json.Unmarshal(body, &ue); err != nil { err := c.ShouldBindJSON(&ue)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
ue.Id = event.Id ue.Id = event.Id
if _, err := ue.Update(); err != nil { if _, err := ue.Update(); err != nil {
return nil, err log.Printf("Unable to updateEvent: %s", err.Error())
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event update."})
genEventsFile() return
return ue, nil
}
} }
func deleteEvent(event *fic.Event, _ []byte) (interface{}, error) {
if i, err := event.Delete(); err != nil {
return i, err
} else {
genEventsFile() genEventsFile()
return i, err
c.JSON(http.StatusOK, ue)
} }
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)
} }

File diff suppressed because it is too large Load diff

View file

@ -2,43 +2,79 @@ package api
import ( import (
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"log"
"net/http"
"strconv"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareFilesGlobalRoutes(router *gin.RouterGroup) {
router.GET("/api/files/", apiHandler(listFiles)) router.DELETE("/files/", clearFiles)
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)))
// Remote // 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 // Check
router.POST("/api/files/:fileid/check", apiHandler(fileHandler(checkFile))) apiFilesRoutes.POST("/check", checkFile)
}
// Synchronize func FileHandler(c *gin.Context) {
router.POST("/api/sync/exercices/:eid/files", apiHandler(exerciceHandler( fileid, err := strconv.ParseInt(string(c.Params.ByName("fileid")), 10, 64)
func(exercice *fic.Exercice, _ []byte) (interface{}, error) { if err != nil {
return sync.SyncExerciceFiles(sync.GlobalImporter, exercice), 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 { type APIFile struct {
@ -87,20 +123,35 @@ func genFileList(in []*fic.EFile, e error) (out []APIFile, err error) {
return return
} }
func listFiles(_ httprouter.Params, body []byte) (interface{}, error) { func listFiles(c *gin.Context) {
return genFileList(fic.GetFiles()) 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
} }
func listExerciceFiles(exercice *fic.Exercice, body []byte) (interface{}, error) { c.JSON(http.StatusOK, files)
return genFileList(exercice.GetFiles())
} }
func clearFiles(_ httprouter.Params, _ []byte) (interface{}, error) { func clearFiles(c *gin.Context) {
return fic.ClearFiles() _, err := fic.ClearFiles()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} }
func showFile(file *fic.EFile, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, true)
return file, nil }
func showFile(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("file").(*fic.EFile))
} }
type uploadedFile struct { type uploadedFile struct {
@ -108,45 +159,92 @@ type uploadedFile struct {
Digest string Digest string
} }
func createExerciceFile(exercice *fic.Exercice, body []byte) (interface{}, error) { func createExerciceFile(c *gin.Context) {
var uf uploadedFile exercice, exists := c.Get("exercice")
if err := json.Unmarshal(body, &uf); err != nil { if !exists {
return nil, err 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) { func(filePath string, origin string) (interface{}, error) {
if digest, err := hex.DecodeString(uf.Digest); err != nil { if digest, err := hex.DecodeString(uf.Digest); err != nil {
return nil, err return nil, err
} else { } 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
} }
func updateFile(file *fic.EFile, body []byte) (interface{}, error) { c.JSON(http.StatusOK, ret)
}
func updateFile(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
var uf fic.EFile var uf fic.EFile
if err := json.Unmarshal(body, &uf); err != nil { err := c.ShouldBindJSON(&uf)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
uf.Id = file.Id uf.Id = file.Id
if _, err := uf.Update(); err != nil { if _, err := uf.Update(); err != nil {
return nil, err log.Println("Unable to updateFile:", err.Error())
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."})
return uf, nil return
}
} }
func deleteFile(file *fic.EFile, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, uf)
return file.Delete()
} }
func deleteFileDep(file *fic.EFile, depid int, _ []byte) (interface{}, error) { func deleteFile(c *gin.Context) {
return true, file.DeleteDepend(&fic.FlagKey{Id: depid}) 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
} }
func checkFile(file *fic.EFile, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, true)
return true, file.CheckFileOnDisk() }
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(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)
} }

View file

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

View file

@ -3,6 +3,7 @@ package api
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"os" "os"
"path" "path"
"strings" "strings"
@ -10,26 +11,26 @@ import (
"srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/admin/pki"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var TimestampCheck = "submissions" var TimestampCheck = "submissions"
func init() { func declareHealthRoutes(router *gin.RouterGroup) {
router.GET("/api/timestamps.json", apiHandler( router.GET("/timestamps.json", func(c *gin.Context) {
func(httprouter.Params, []byte) (interface{}, error) { stat, err := os.Stat(TimestampCheck)
if stat, err := os.Stat(TimestampCheck); err != nil { if err != nil {
return nil, err c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("timestamp.json: %s", err.Error())})
} else { return
}
now := time.Now().UTC() now := time.Now().UTC()
return map[string]interface{}{ c.JSON(http.StatusOK, gin.H{
"frontend": stat.ModTime().UTC(), "frontend": stat.ModTime().UTC(),
"backend": now, "backend": now,
"diffFB": now.Sub(stat.ModTime()), "diffFB": now.Sub(stat.ModTime()),
}, nil })
} })
})) router.GET("/health.json", GetHealth)
router.GET("/api/health.json", apiHandler(GetHealth))
} }
type healthFileReport struct { type healthFileReport struct {
@ -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 { if _, err := os.Stat(TimestampCheck); err != nil {
return nil, err c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("health.json: %s", err.Error())})
} else { return
return getHealth(TimestampCheck), nil
} }
c.JSON(http.StatusOK, getHealth(TimestampCheck))
} }

View file

@ -3,20 +3,20 @@ package api
import ( import (
"bufio" "bufio"
"io/ioutil" "io/ioutil"
"net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareMonitorRoutes(router *gin.RouterGroup) {
router.GET("/api/monitor", apiHandler( router.GET("/monitor", func(c *gin.Context) {
func(httprouter.Params, []byte) (interface{}, error) { c.JSON(http.StatusOK, gin.H{
return map[string]interface{}{
"localhost": genLocalConstants(), "localhost": genLocalConstants(),
}, nil })
})) })
} }
func readLoadAvg(fd *os.File) (ret map[string]float64) { func readLoadAvg(fd *os.File) (ret map[string]float64) {

View file

@ -4,67 +4,97 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http"
"path" "path"
"text/template" "text/template"
"srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var OidcSecret = "" var OidcSecret = ""
func init() { func declarePasswordRoutes(router *gin.RouterGroup) {
router.POST("/api/password", apiHandler( router.POST("/password", func(c *gin.Context) {
func(httprouter.Params, []byte) (interface{}, error) { passwd, err := fic.GeneratePassword()
if passwd, err := fic.GeneratePassword(); err != nil { if err != nil {
return nil, err c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
} else { return
return map[string]string{"password": passwd}, nil
} }
}))
router.GET("/api/teams/:tid/password", apiHandler(teamHandler( c.JSON(http.StatusOK, gin.H{"password": passwd})
func(team *fic.Team, _ []byte) (interface{}, error) { })
return team.Password, nil router.GET("/api/dex.yaml", func(c *gin.Context) {
}))) cfg, err := genDexConfig()
router.POST("/api/teams/:tid/password", apiHandler(teamHandler( if err != nil {
func(team *fic.Team, _ []byte) (interface{}, error) { 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 { if passwd, err := fic.GeneratePassword(); err != nil {
return nil, err 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 { } else {
team.Password = &passwd team.Password = &passwd
return team.Update()
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/dex.yaml", apiHandler( c.JSON(http.StatusOK, t)
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
}
}))
} }
const dexcfgtpl = `issuer: https://fic.srs.epita.fr const dexcfgtpl = `issuer: https://fic.srs.epita.fr

View file

@ -3,18 +3,20 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net/http"
"os" "os"
"path" "path"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var DashboardDir string var DashboardDir string
func init() { func declarePublicRoutes(router *gin.RouterGroup) {
router.GET("/api/public/:sid", apiHandler(getPublic)) router.GET("/public/:sid", getPublic)
router.DELETE("/api/public/:sid", apiHandler(deletePublic)) router.DELETE("/public/:sid", deletePublic)
router.PUT("/api/public/:sid", apiHandler(savePublic)) router.PUT("/public/:sid", savePublic)
} }
type FICPublicScene struct { type FICPublicScene struct {
@ -62,31 +64,44 @@ func savePublicTo(path string, s FICPublicDisplay) error {
} }
} }
func getPublic(ps httprouter.Params, body []byte) (interface{}, error) { func getPublic(c *gin.Context) {
if _, err := os.Stat(path.Join(DashboardDir, fmt.Sprintf("public%s.json", ps.ByName("sid")))); !os.IsNotExist(err) { if _, err := os.Stat(path.Join(DashboardDir, fmt.Sprintf("public%s.json", c.Params.ByName("sid")))); !os.IsNotExist(err) {
return readPublic(path.Join(DashboardDir, fmt.Sprintf("public%s.json", ps.ByName("sid")))) p, err := readPublic(path.Join(DashboardDir, fmt.Sprintf("public%s.json", c.Params.ByName("sid"))))
} else { if err != nil {
return FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}}, nil log.Println("Unable to readPublic in getPublic:", err.Error())
} c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene retrieval."})
return
} }
func deletePublic(ps httprouter.Params, body []byte) (interface{}, error) { c.JSON(http.StatusOK, p)
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 savePublic(ps httprouter.Params, body []byte) (interface{}, error) { c.JSON(http.StatusOK, FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}})
}
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(c *gin.Context) {
var scenes FICPublicDisplay var scenes FICPublicDisplay
if err := json.Unmarshal(body, &scenes); err != nil { err := c.ShouldBindJSON(&scenes)
return nil, err 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 { if err := savePublicTo(path.Join(DashboardDir, fmt.Sprintf("public%s.json", c.Params.ByName("sid"))), scenes); err != nil {
return nil, err log.Println("Unable to savePublicTo:", err.Error())
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene saving."})
return scenes, err return
} }
c.JSON(http.StatusOK, scenes)
} }

View file

@ -1,84 +1,118 @@
package api package api
import ( import (
"encoding/json" "log"
"errors" "net/http"
"strconv" "strconv"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareQARoutes(router *gin.RouterGroup) {
router.POST("/api/qa/", apiHandler(importExerciceQA)) router.POST("/qa/", importExerciceQA)
router.POST("/api/qa/:qid/comments", apiHandler(qaHandler(importQAComment)))
apiQARoutes := router.Group("/qa/:qid")
apiQARoutes.POST("/comments", importQAComment)
} }
func qaHandler(f func(*fic.QAQuery, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { func QAHandler(c *gin.Context) {
return func(ps httprouter.Params, body []byte) (interface{}, error) { qid, err := strconv.ParseInt(string(c.Params.ByName("qid")), 10, 64)
if qid, err := strconv.ParseInt(string(ps.ByName("qid")), 10, 64); err != nil { if err != nil {
return nil, err c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid QA identifier"})
} else if query, err := fic.GetQAQuery(qid); err != nil { return
return nil, err
} else {
return f(query, body)
}
}
} }
func importExerciceQA(_ httprouter.Params, body []byte) (interface{}, error) { 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(c *gin.Context) {
// Create a new query // Create a new query
var uq fic.QAQuery var uq fic.QAQuery
if err := json.Unmarshal(body, &uq); err != nil { err := c.ShouldBindJSON(&uq)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
var exercice *fic.Exercice var exercice *fic.Exercice
var err error
if uq.IdExercice == 0 { 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 { } 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 { if len(uq.State) == 0 {
return nil, errors.New("State not filled") c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "State not filled"})
return
} }
if len(uq.Subject) == 0 { if 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 { 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 { } else {
qa.Creation = uq.Creation qa.Creation = uq.Creation
qa.Solved = uq.Solved qa.Solved = uq.Solved
qa.Closed = qa.Closed qa.Closed = qa.Closed
_, err = qa.Update() _, 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 // Create a new query
var uc fic.QAComment var uc fic.QAComment
if err := json.Unmarshal(body, &uc); err != nil { err := c.ShouldBindJSON(&uc)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
if len(uc.Content) == 0 { 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 { 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 { } else {
qac.Date = uc.Date qac.Date = uc.Date
_, err = qac.Update() _, 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)
} }
} }

View file

@ -1,11 +1,26 @@
package api package api
import ( 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 { declareCertificateRoutes(apiRoutes)
return router 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)
} }

View file

@ -1,35 +1,44 @@
package api package api
import ( import (
"encoding/json" "fmt"
"errors" "log"
"net/http"
"path" "path"
"reflect" "reflect"
"time"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings" "srs.epita.fr/fic-server/settings"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var IsProductionEnv = false var IsProductionEnv = false
func init() { func declareSettingsRoutes(router *gin.RouterGroup) {
router.GET("/api/challenge.json", apiHandler(getChallengeInfo)) router.GET("/challenge.json", getChallengeInfo)
router.PUT("/api/challenge.json", apiHandler(saveChallengeInfo)) router.PUT("/challenge.json", saveChallengeInfo)
router.GET("/api/settings-ro.json", apiHandler(getROSettings)) router.GET("/settings-ro.json", getROSettings)
router.GET("/api/settings.json", apiHandler(getSettings)) router.GET("/settings.json", getSettings)
router.PUT("/api/settings.json", apiHandler(saveSettings)) router.PUT("/settings.json", saveSettings)
router.DELETE("/api/settings.json", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { router.DELETE("/settings.json", func(c *gin.Context) {
return true, ResetSettings() err := ResetSettings()
})) if err != nil {
log.Println("Unable to ResetSettings:", err.Error())
router.POST("/api/reset", apiHandler(reset)) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during setting reset."})
return
} }
func getROSettings(_ httprouter.Params, body []byte) (interface{}, error) { c.JSON(http.StatusOK, true)
})
router.POST("/reset", reset)
}
func getROSettings(c *gin.Context) {
syncMtd := "Disabled" syncMtd := "Disabled"
if sync.GlobalImporter != nil { if sync.GlobalImporter != nil {
syncMtd = sync.GlobalImporter.Kind() syncMtd = sync.GlobalImporter.Kind()
@ -40,51 +49,70 @@ func getROSettings(_ httprouter.Params, body []byte) (interface{}, error) {
syncId = sync.GlobalImporter.Id() syncId = sync.GlobalImporter.Id()
} }
return map[string]interface{}{ c.JSON(http.StatusOK, gin.H{
"sync-type": reflect.TypeOf(sync.GlobalImporter).Name(), "sync-type": reflect.TypeOf(sync.GlobalImporter).Name(),
"sync-id": syncId, "sync-id": syncId,
"sync": syncMtd, "sync": syncMtd,
}, nil })
} }
func getChallengeInfo(_ httprouter.Params, body []byte) (interface{}, error) { func getChallengeInfo(c *gin.Context) {
return settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)) 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
} }
func saveChallengeInfo(_ httprouter.Params, body []byte) (interface{}, error) { c.JSON(http.StatusOK, s)
}
func saveChallengeInfo(c *gin.Context) {
var info *settings.ChallengeInfo var info *settings.ChallengeInfo
if err := json.Unmarshal(body, &info); err != nil { err := c.ShouldBindJSON(&info)
return nil, err 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 { if err := settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), info); err != nil {
return nil, err log.Println("Unable to SaveChallengeInfo:", err.Error())
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save challenge info: %s", err.Error())})
return info, err return
} }
c.JSON(http.StatusOK, info)
}
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
} }
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 s.WorkInProgress = !IsProductionEnv
return s, nil 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 var config *settings.Settings
if err := json.Unmarshal(body, &config); err != nil { err := c.ShouldBindJSON(&config)
return nil, err 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 { if err := settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), config); err != nil {
return nil, err log.Println("Unable to SaveSettings:", err.Error())
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save settings: %s", err.Error())})
ApplySettings(config) return
return config, err
} }
ApplySettings(config)
c.JSON(http.StatusOK, config)
} }
func ApplySettings(config *settings.Settings) { 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 var m map[string]string
if err := json.Unmarshal(body, &m); err != nil { err := c.ShouldBindJSON(&m)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
if t, ok := m["type"]; !ok { t, ok := m["type"]
return nil, errors.New("Field type not found") if !ok {
} else if t == "teams" { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Field type not found"})
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")
} }
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)
} }

191
admin/api/sync.go Normal file
View file

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

View file

@ -3,115 +3,242 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"math/rand" "math/rand"
"net/http"
"strconv"
"strings" "strings"
"time" "time"
"srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareTeamsRoutes(router *gin.RouterGroup) {
router.GET("/api/teams.json", apiHandler( router.GET("/teams.json", func(c *gin.Context) {
func(httprouter.Params, []byte) (interface{}, error) { teams, err := fic.ExportTeams(false)
return fic.ExportTeams(false) if err != nil {
})) log.Println("Unable to ExportTeams:", err.Error())
router.GET("/api/teams-members.json", apiHandler( c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."})
func(httprouter.Params, []byte) (interface{}, error) { return
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( c.JSON(http.StatusOK, teams)
func(httprouter.Params, []byte) (interface{}, error) { })
return fic.GetTeams() router.GET("/teams-members.json", func(c *gin.Context) {
})) teams, err := fic.ExportTeams(true)
router.POST("/api/teams/", apiHandler(createTeam)) if err != nil {
log.Println("Unable to ExportTeams:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."})
return
}
router.GET("/api/teams/:tid/", apiHandler(teamHandler( c.JSON(http.StatusOK, teams)
func(team *fic.Team, _ []byte) (interface{}, error) { })
return team, nil router.GET("/teams-binding", bindingTeams)
}))) router.GET("/teams-nginx", nginxGenTeams)
router.PUT("/api/teams/:tid/", apiHandler(teamHandler(updateTeam))) router.POST("/disableinactiveteams", disableInactiveTeams)
router.POST("/api/teams/:tid/", apiHandler(teamHandler(addTeamMember))) router.POST("/enableallteams", enableAllTeams)
router.DELETE("/api/teams/:tid/", apiHandler(teamHandler(deleteTeam))) router.GET("/teams-members-nginx", nginxGenMember)
router.GET("/api/teams/:tid/score-grid.json", apiHandler(teamHandler( router.GET("/teams-tries.json", func(c *gin.Context) {
func(team *fic.Team, _ []byte) (interface{}, error) { tries, err := fic.GetTries(nil, nil)
return team.ScoreGrid() if err != nil {
}))) log.Println("Unable to GetTries:", err.Error())
router.GET("/api/teams/:tid/my.json", apiHandler(teamPublicHandler( c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."})
func(team *fic.Team, _ []byte) (interface{}, error) { return
return fic.MyJSONTeam(team, true) }
})))
router.GET("/api/teams/:tid/wait.json", apiHandler(teamPublicHandler( c.JSON(http.StatusOK, tries)
func(team *fic.Team, _ []byte) (interface{}, error) { })
return fic.MyJSONTeam(team, false)
}))) router.GET("/teams", func(c *gin.Context) {
router.GET("/api/teams/:tid/stats.json", apiHandler(teamPublicHandler( teams, err := fic.GetTeams()
func(team *fic.Team, _ []byte) (interface{}, error) { 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 { if team != nil {
return team.GetStats() stats, err := team.GetStats()
} else { if err != nil {
return fic.GetTeamsStats(nil) log.Println("Unable to get GetStats:", err.Error())
} c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during stats calculation."})
}))) return
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) { c.JSON(http.StatusOK, stats)
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else { } 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 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 := "" ret := ""
for _, team := range teams { for _, team := range teams {
ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", strings.ToLower(team.Name), team.Id) ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", strings.ToLower(team.Name), team.Id)
} }
return ret, nil 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
} }
func nginxGenMember() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := "" ret := ""
for _, team := range teams { for _, team := range teams {
if members, err := team.GetMembers(); err == nil { if members, err := team.GetMembers(); err == nil {
@ -119,22 +246,27 @@ func nginxGenMember() (string, error) {
ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id) ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id)
} }
} else { } else {
return "", err c.AbortWithError(http.StatusInternalServerError, err)
return
} }
} }
return ret, nil 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
} }
func bindingTeams() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := "" ret := ""
for _, team := range teams { for _, team := range teams {
if members, err := team.GetMembers(); err != nil { if members, err := team.GetMembers(); err != nil {
return "", err c.AbortWithError(http.StatusInternalServerError, err)
return
} else { } else {
var mbs []string var mbs []string
for _, member := range members { for _, member := range members {
@ -143,27 +275,40 @@ func bindingTeams() (string, error) {
ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";")) ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";"))
} }
} }
return ret, nil
} c.String(http.StatusOK, ret)
} }
func createTeam(_ httprouter.Params, body []byte) (interface{}, error) { func createTeam(c *gin.Context) {
var ut fic.Team var ut fic.Team
if err := json.Unmarshal(body, &ut); err != nil { err := c.ShouldBindJSON(&ut)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
if ut.Color == 0 { if ut.Color == 0 {
ut.Color = fic.HSL{rand.Float64(), 1, 0.5}.ToRGB() 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
} }
func updateTeam(team *fic.Team, body []byte) (interface{}, error) { c.JSON(http.StatusOK, team)
}
func updateTeam(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var ut fic.Team var ut fic.Team
if err := json.Unmarshal(body, &ut); err != nil { err := c.ShouldBindJSON(&ut)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
ut.Id = team.Id ut.Id = team.Id
@ -172,28 +317,37 @@ func updateTeam(team *fic.Team, body []byte) (interface{}, error) {
ut.Password = nil ut.Password = nil
} }
if _, err := ut.Update(); err != nil { _, err = ut.Update()
return nil, err 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(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
} }
func disableInactiveTeams(_ httprouter.Params, _ []byte) (interface{}, error) {
if teams, err := fic.GetTeams(); err != nil {
return nil, err
} else {
for _, team := range teams { for _, team := range teams {
var serials []uint64 var serials []uint64
serials, err = pki.GetTeamSerials(TeamsDir, team.Id) serials, err = pki.GetTeamSerials(TeamsDir, team.Id)
if err != nil { if err != nil {
return nil, err c.AbortWithError(http.StatusInternalServerError, err)
return
} }
var assocs []string var assocs []string
assocs, err = pki.GetTeamAssociations(TeamsDir, team.Id) assocs, err = pki.GetTeamAssociations(TeamsDir, team.Id)
if err != nil { if err != nil {
return nil, err c.AbortWithError(http.StatusInternalServerError, err)
return
} }
if len(serials) == 0 && len(assocs) == 0 { if len(serials) == 0 && len(assocs) == 0 {
@ -207,14 +361,17 @@ func disableInactiveTeams(_ httprouter.Params, _ []byte) (interface{}, error) {
} }
} }
return true, nil c.JSON(http.StatusOK, true)
} }
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
} }
func enableAllTeams(_ httprouter.Params, _ []byte) (interface{}, error) {
if teams, err := fic.GetTeams(); err != nil {
return nil, err
} else {
for _, team := range teams { for _, team := range teams {
if !team.Active { if !team.Active {
team.Active = true team.Active = true
@ -222,52 +379,70 @@ func enableAllTeams(_ httprouter.Params, _ []byte) (interface{}, error) {
} }
} }
return true, nil c.JSON(http.StatusOK, true)
}
} }
func deleteTeam(team *fic.Team, _ []byte) (interface{}, error) { func deleteTeam(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id) assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
if err != nil { 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 { for _, assoc := range assocs {
err = pki.DeleteTeamAssociation(TeamsDir, assoc) err = pki.DeleteTeamAssociation(TeamsDir, assoc)
}
if err != nil { if err != nil {
return nil, err log.Printf("Unable to DeleteTeamAssociation(assoc=%s): %s", assoc, err.Error())
return
}
} }
return team.Delete() _, err = team.Delete()
if err != nil {
log.Println("Unable to deleteTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team deletion."})
return
} }
func addTeamMember(team *fic.Team, body []byte) (interface{}, error) { c.JSON(http.StatusOK, true)
}
func addTeamMember(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var members []fic.Member var members []fic.Member
if err := json.Unmarshal(body, &members); err != nil { err := c.ShouldBindJSON(&members)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
for _, member := range members { 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
} }
func setTeamMember(team *fic.Team, body []byte) (interface{}, error) { c.JSON(http.StatusOK, mmbrs)
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() team.ClearMembers()
for _, member := range members { addTeamMember(c)
team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company))
}
return team.GetMembers()
} }
type uploadedHistory struct { 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) 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 var uh uploadedHistory
if err := json.Unmarshal(body, &uh); err != nil { err := c.ShouldBindJSON(&uh)
return nil, err 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)
} }

View file

@ -1,171 +1,75 @@
package api package api
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"log"
"net/http"
"path" "path"
"strconv" "strconv"
"strings"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings" "srs.epita.fr/fic-server/settings"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareThemesRoutes(router *gin.RouterGroup) {
router.GET("/api/themes", apiHandler(listThemes)) router.GET("/themes", listThemes)
router.POST("/api/themes", apiHandler(createTheme)) router.POST("/themes", createTheme)
router.GET("/api/themes.json", apiHandler(exportThemes)) router.GET("/themes.json", exportThemes)
router.GET("/api/session-forensic.yaml", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { router.GET("/session-forensic.yaml", func(c *gin.Context) {
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
return nil, err log.Printf("Unable to ReadSettings: %s", err.Error())
} else if c, err := settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during settings reading."})
return nil, err 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 { } 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))) apiThemesRoutes := router.Group("/themes/:thid")
router.PUT("/api/themes/:thid", apiHandler(themeHandler(updateTheme))) apiThemesRoutes.Use(ThemeHandler)
router.DELETE("/api/themes/:thid", apiHandler(themeHandler(deleteTheme))) apiThemesRoutes.GET("", showTheme)
apiThemesRoutes.PUT("", updateTheme)
apiThemesRoutes.DELETE("", deleteTheme)
router.GET("/api/themes/:thid/exercices", apiHandler(themeHandler(listThemedExercices))) declareExercicesRoutes(apiThemesRoutes)
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)))
// Remote // Remote
router.GET("/api/remote/themes", apiHandler(sync.ApiListRemoteThemes)) router.GET("/remote/themes", sync.ApiListRemoteThemes)
router.GET("/api/remote/themes/:thid", apiHandler(sync.ApiGetRemoteTheme)) router.GET("/remote/themes/:thid", sync.ApiGetRemoteTheme)
router.GET("/api/remote/themes/:thid/exercices", apiHandler(sync.ApiListRemoteExercices)) router.GET("/remote/themes/:thid/exercices", 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() func ThemeHandler(c *gin.Context) {
thid, err := strconv.ParseInt(string(c.Params.ByName("thid")), 10, 64)
if err != nil { if err != nil {
return nil, err c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid theme identifier"})
return
} }
if p == "" { theme, err := fic.GetTheme(thid)
if !IsProductionEnv { if err != nil {
for _, theme := range themes { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
theme.DeleteDeep() return
}
} }
st := sync.SyncDeep(sync.GlobalImporter) c.Set("theme", theme)
return st, nil
c.Next()
} }
for _, theme := range themes { func fixAllURLIds(c *gin.Context) {
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))
}
func fixAllURLIds(_ httprouter.Params, _ []byte) (interface{}, error) {
nbFix := 0 nbFix := 0
if themes, err := fic.GetThemes(); err == nil { if themes, err := fic.GetThemes(); err == nil {
for _, theme := range themes { 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(c *gin.Context) {
files, err := fic.GetFiles()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
} }
func bindingFiles(_ httprouter.Params, body []byte) (interface{}, error) {
if files, err := fic.GetFiles(); err != nil {
return "", err
} else {
ret := "" ret := ""
for _, file := range files { for _, file := range files {
ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path) ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
} }
return ret, nil
} c.String(http.StatusOK, ret)
} }
func getExercice(args []string) (*fic.Exercice, error) { 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) { func listThemes(c *gin.Context) {
return fic.GetThemes() 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
} }
func exportThemes(_ httprouter.Params, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, themes)
return fic.ExportThemes()
} }
func showTheme(theme *fic.Theme, _ []byte) (interface{}, error) { func exportThemes(c *gin.Context) {
return theme, nil 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
} }
func listThemedExercices(theme *fic.Theme, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, themes)
return theme.GetExercices()
} }
func showThemedExercice(theme *fic.Theme, exercice fic.Exercice, body []byte) (interface{}, error) { func showTheme(c *gin.Context) {
return exercice, nil c.JSON(http.StatusOK, c.MustGet("theme").(*fic.Theme))
} }
func createTheme(_ httprouter.Params, body []byte) (interface{}, error) { func createTheme(c *gin.Context) {
var ut fic.Theme var ut fic.Theme
if err := json.Unmarshal(body, &ut); err != nil { err := c.ShouldBindJSON(&ut)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
if len(ut.Name) == 0 { 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
} }
func updateTheme(theme *fic.Theme, body []byte) (interface{}, error) { c.JSON(http.StatusOK, th)
}
func updateTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
var ut fic.Theme var ut fic.Theme
if err := json.Unmarshal(body, &ut); err != nil { err := c.ShouldBindJSON(&ut)
return nil, err if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
ut.Id = theme.Id ut.Id = theme.Id
if len(ut.Name) == 0 { 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 { if _, err := ut.Update(); err != nil {
return nil, err log.Println("Unable to updateTheme:", err.Error())
} else { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme update."})
return ut, nil return
}
} }
func deleteTheme(theme *fic.Theme, _ []byte) (interface{}, error) { c.JSON(http.StatusOK, ut)
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) { func getThemedExercicesStats(theme *fic.Theme, body []byte) (interface{}, error) {

View file

@ -1,13 +1,15 @@
package api package api
import ( import (
"github.com/julienschmidt/httprouter" "net/http"
"github.com/gin-gonic/gin"
) )
func init() { func DeclareVersionRoutes(router *gin.RouterGroup) {
router.GET("/api/version", apiHandler(showVersion)) router.GET("/version", showVersion)
} }
func showVersion(_ httprouter.Params, body []byte) (interface{}, error) { func showVersion(c *gin.Context) {
return map[string]interface{}{"version": 1.0}, nil c.JSON(http.StatusOK, gin.H{"version": 1.0})
} }

71
admin/app.go Normal file
View file

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

View file

@ -1,9 +1,7 @@
package main package main
import ( import (
"context"
"flag" "flag"
"fmt"
"io/fs" "io/fs"
"log" "log"
"net/http" "net/http"
@ -198,12 +196,12 @@ func main() {
os.MkdirAll(settings.SettingsDir, 0777) os.MkdirAll(settings.SettingsDir, 0777)
// Initialize settings and load them // Initialize settings and load them
var config *settings.Settings
if !settings.ExistsSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) { if !settings.ExistsSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) {
if err = api.ResetSettings(); err != nil { if err = api.ResetSettings(); err != nil {
log.Fatal("Unable to initialize settings.json:", err) log.Fatal("Unable to initialize settings.json:", err)
} }
} else { } else {
var config *settings.Settings
if config, err = settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { if config, err = settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
log.Fatal("Unable to read settings.json:", err) log.Fatal("Unable to read settings.json:", err)
} else { } else {
@ -231,21 +229,13 @@ func main() {
interrupt := make(chan os.Signal, 1) interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
srv := &http.Server{ app := NewApp(config, baseURL, *bind)
Addr: *bind, go app.Start()
Handler: StripPrefix(baseURL, api.Router()),
}
// Serve content
go func() {
log.Fatal(srv.ListenAndServe())
}()
log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
// Wait shutdown signal // Wait shutdown signal
<-interrupt <-interrupt
log.Print("The service is shutting down...") log.Print("The service is shutting down...")
srv.Shutdown(context.Background()) app.Stop()
log.Println("done") log.Println("done")
} }

View file

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"embed" "embed"
"errors"
"log" "log"
"net/http" "net/http"
"path" "path"
@ -12,8 +13,9 @@ import (
"srs.epita.fr/fic-server/admin/api" "srs.epita.fr/fic-server/admin/api"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
//go:embed static //go:embed static
@ -33,83 +35,83 @@ func genIndex(baseURL string) {
} }
} }
func serveIndex(w http.ResponseWriter, r *http.Request) { func serveIndex(c *gin.Context) {
w.Write(indexPage) c.Writer.Write(indexPage)
} }
var staticFS http.FileSystem var staticFS http.FileSystem
func serveFile(w http.ResponseWriter, r *http.Request, url string) { func serveFile(c *gin.Context, url string) {
r.URL.Path = url c.Request.URL.Path = url
http.FileServer(staticFS).ServeHTTP(w, r) http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request)
} }
func init() { func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseURL string) {
api.Router().GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/claims/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/claims/*_", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/exercices/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/exercices/*_", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/events/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/events/*_", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/files", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/files", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/public/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/public/*_", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/pki/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/pki/*_", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/settings/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/settings/*_", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/teams/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/teams/*_", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/themes/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/themes/*_", func(c *gin.Context) {
serveIndex(w, r) serveIndex(c)
}) })
api.Router().GET("/css/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/css/*_", func(c *gin.Context) {
serveFile(w, r, r.URL.Path) serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
}) })
api.Router().GET("/fonts/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/fonts/*_", func(c *gin.Context) {
serveFile(w, r, r.URL.Path) serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
}) })
api.Router().GET("/img/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/img/*_", func(c *gin.Context) {
serveFile(w, r, r.URL.Path) serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
}) })
api.Router().GET("/js/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/js/*_", func(c *gin.Context) {
serveFile(w, r, r.URL.Path) serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
}) })
api.Router().GET("/views/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/views/*_", func(c *gin.Context) {
serveFile(w, r, r.URL.Path) serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
}) })
api.Router().GET("/files/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/files/*_", func(c *gin.Context) {
http.ServeFile(w, r, path.Join(fic.FilesDir, strings.TrimPrefix(r.URL.Path, "/files"))) 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) { router.GET("/submissions/*_", func(c *gin.Context) {
http.ServeFile(w, r, path.Join(api.TimestampCheck, strings.TrimPrefix(r.URL.Path, "/submissions"))) 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 { 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 { } 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) { router.GET("/check_import.html", func(c *gin.Context) {
serveFile(w, r, "check_import.html") serveFile(c, "check_import.html")
}) })
api.Router().GET("/full_import_report.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { router.GET("/full_import_report.json", func(c *gin.Context) {
http.ServeFile(w, r, sync.DeepReportPath) http.ServeFile(c.Writer, c.Request, sync.DeepReportPath)
}) })
} }

View file

@ -300,7 +300,7 @@ angular.module("FICApp")
return $resource("api/exercices/:exerciceId/history.json", { exerciceId: '@id' }) return $resource("api/exercices/:exerciceId/history.json", { exerciceId: '@id' })
}) })
.factory("ExercicesStats", function($resource) { .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) { .factory("ExerciceStats", function($resource) {
return $resource("api/exercices/:exerciceId/stats.json", { exerciceId: '@id' }) return $resource("api/exercices/:exerciceId/stats.json", { exerciceId: '@id' })
@ -1653,7 +1653,7 @@ angular.module("FICApp")
}) })
.controller("ExercicesStatsController", function($scope, ExercicesStats) { .controller("ExercicesStatsController", function($scope, ExercicesStats) {
$scope.exercices = ExercicesStats.query({ themeId: $scope.theme.id }); $scope.exercices = ExercicesStats.query();
}) })
.controller("ExerciceStatsController", function($scope, ExerciceStats, $routeParams) { .controller("ExerciceStatsController", function($scope, ExerciceStats, $routeParams) {

View file

@ -4,11 +4,12 @@ import (
"bufio" "bufio"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/http"
"path" "path"
"strings" "strings"
"unicode" "unicode"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
"srs.epita.fr/fic-server/libfic" "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. // ApiGetRemoteExerciceFiles is an accessor to remote exercice files list.
func ApiGetRemoteExerciceFiles(ps httprouter.Params, _ []byte) (interface{}, error) { func ApiGetRemoteExerciceFiles(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil { 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 { if exercice != nil {
files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files") files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files")
if files != nil { if files != nil {
@ -164,14 +165,17 @@ func ApiGetRemoteExerciceFiles(ps httprouter.Params, _ []byte) (interface{}, err
Size: fSize, Size: fSize,
}) })
} }
return ret, nil c.JSON(http.StatusOK, ret)
} else { } else {
return nil, fmt.Errorf("%q", errs) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
} }
} else { } else {
return nil, fmt.Errorf("%q", errs) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
} }
} else { } else {
return nil, fmt.Errorf("%q", errs) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
} }
} }

View file

@ -6,11 +6,12 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"path" "path"
"strings" "strings"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
_ "golang.org/x/crypto/blake2b" _ "golang.org/x/crypto/blake2b"
"srs.epita.fr/fic-server/libfic" "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. // ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints.
func ApiGetRemoteExerciceHints(ps httprouter.Params, _ []byte) (interface{}, error) { func ApiGetRemoteExerciceHints(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil { 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 { if exercice != nil {
hints, errs := CheckExerciceHints(GlobalImporter, exercice) hints, errs := CheckExerciceHints(GlobalImporter, exercice)
if hints != nil { if hints != nil {
return hints, nil c.JSON(http.StatusOK, hints)
} else { return
return hints, fmt.Errorf("%q", errs)
} }
} 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))
} }

View file

@ -3,13 +3,14 @@ package sync
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"net/http"
"path" "path"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"unicode" "unicode"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
"srs.epita.fr/fic-server/libfic" "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. // ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags.
func ApiGetRemoteExerciceFlags(ps httprouter.Params, _ []byte) (interface{}, error) { func ApiGetRemoteExerciceFlags(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil { 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 { if exercice != nil {
flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}) flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{})
if flags != nil { if flags != nil {
return flags, nil c.JSON(http.StatusOK, flags)
} else { return
return flags, fmt.Errorf("%q", errs)
} }
} 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
} }

View file

@ -3,12 +3,13 @@ package sync
import ( import (
"fmt" "fmt"
"log" "log"
"net/http"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
"github.com/russross/blackfriday/v2" "github.com/russross/blackfriday/v2"
"srs.epita.fr/fic-server/libfic" "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. // ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list.
func ApiListRemoteExercices(ps httprouter.Params, _ []byte) (interface{}, error) { func ApiListRemoteExercices(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil { 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 { } 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. // ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
func ApiGetRemoteExercice(ps httprouter.Params, _ []byte) (interface{}, error) { func ApiGetRemoteExercice(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil { 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 { if exercice != nil {
return exercice, nil c.JSON(http.StatusOK, exercice)
return
} else { } else {
return exercice, fmt.Errorf("%q", errs) c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
} }
} else { } else {
return nil, fmt.Errorf("%q", errs) c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
} }
} }

View file

@ -5,13 +5,14 @@ import (
"image" "image"
"image/jpeg" "image/jpeg"
"math/rand" "math/rand"
"net/http"
"os" "os"
"path" "path"
"regexp" "regexp"
"strings" "strings"
"unicode" "unicode"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
"github.com/russross/blackfriday/v2" "github.com/russross/blackfriday/v2"
"golang.org/x/image/draw" "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. // ApiListRemoteThemes is an accessor letting foreign packages to access remote themes list.
func ApiListRemoteThemes(_ httprouter.Params, _ []byte) (interface{}, error) { func ApiListRemoteThemes(c *gin.Context) {
return GetThemes(GlobalImporter) 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. // ApiListRemoteTheme is an accessor letting foreign packages to access remote main theme attributes.
func ApiGetRemoteTheme(ps httprouter.Params, _ []byte) (interface{}, error) { func ApiGetRemoteTheme(c *gin.Context) {
r, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) r, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if r == nil { if r == nil {
return r, fmt.Errorf("%q", errs) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
} else { return
return r, nil
} }
c.JSON(http.StatusOK, r)
} }

1
go.mod
View file

@ -5,6 +5,7 @@ go 1.16
require ( require (
github.com/BurntSushi/toml v1.1.0 github.com/BurntSushi/toml v1.1.0
github.com/fsnotify/fsnotify v1.4.9 // indirect 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-git/go-git/v5 v5.4.2
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0

30
go.sum
View file

@ -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/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 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 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/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 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 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 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-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-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 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 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= 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.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.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/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 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/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= 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 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 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/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.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/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 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/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.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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/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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/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.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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= 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-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 h1:b2nJXyPCa9HY7giGM+kYcnQ71m14JnGdQabMPmyt++8=
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= 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 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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-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-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-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-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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/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/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.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.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.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= 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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -74,6 +74,12 @@ func GetFile(id int64) (f *EFile, err error) {
return 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. // GetFileByPath retrieves the file that should be found at the given location.
func GetFileByPath(path string) (*EFile, error) { func GetFileByPath(path string) (*EFile, error) {
path = strings.TrimPrefix(path, FilesDir) path = strings.TrimPrefix(path, FilesDir)

View file

@ -78,6 +78,13 @@ func GetFlagKey(id int) (k *FlagKey, err error) {
return 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. // GetFlagKeyByLabel returns a flag matching the given label.
func (e *Exercice) GetFlagKeyByLabel(label string) (k *FlagKey, err error) { func (e *Exercice) GetFlagKeyByLabel(label string) (k *FlagKey, err error) {
k = &FlagKey{} k = &FlagKey{}

View file

@ -46,6 +46,17 @@ func GetHint(id int64) (*EHint, error) {
return h, nil 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. // GetHintByTitle retrieves the hint with the given id.
func (e *Exercice) GetHintByTitle(id int64) (*EHint, error) { func (e *Exercice) GetHintByTitle(id int64) (*EHint, error) {
h := &EHint{} h := &EHint{}

View file

@ -31,7 +31,7 @@ type MCQ_entry struct {
// GetMCQ returns a list of flags comming with the challenge. // GetMCQ returns a list of flags comming with the challenge.
func GetMCQ(id int) (m *MCQ, err error) { func GetMCQ(id int) (m *MCQ, err error) {
m = &MCQ{} 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() m.fillEntries()
return 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. // GetMCQbyChoice returns the MCQ corresponding to a choice ID.
func GetMCQbyChoice(cid int) (m *MCQ, c *MCQ_entry, err error) { func GetMCQbyChoice(cid int) (m *MCQ, c *MCQ_entry, err error) {
m = &MCQ{} m = &MCQ{}