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"
"encoding/base32"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"math/big"
"net/http"
"os"
"path"
"strconv"
@ -23,53 +23,106 @@ import (
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
var TeamsDir string
func init() {
router.GET("/api/htpasswd", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return genHtpasswd(true)
}))
router.POST("/api/htpasswd", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
func declareCertificateRoutes(router *gin.RouterGroup) {
router.GET("/htpasswd", func(c *gin.Context) {
ret, err := genHtpasswd(true)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, ret)
})
router.POST("/htpasswd", func(c *gin.Context) {
if htpasswd, err := genHtpasswd(true); err != nil {
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 {
return nil, err
} else {
return true, nil
log.Println("Unable to write htpasswd:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
}))
router.DELETE("/api/htpasswd", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
c.AbortWithStatus(http.StatusOK)
})
router.DELETE("/htpasswd", func(c *gin.Context) {
if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil {
return nil, err
} else {
return true, nil
log.Println("Unable to remove htpasswd:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
}))
router.GET("/api/htpasswd.apr1", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return genHtpasswd(false)
}))
router.GET("/api/ca/", apiHandler(infoCA))
router.GET("/api/ca.pem", apiHandler(getCAPEM))
router.POST("/api/ca/new", apiHandler(
func(_ httprouter.Params, body []byte) (interface{}, error) {
c.AbortWithStatus(http.StatusOK)
})
router.GET("/htpasswd.apr1", func(c *gin.Context) {
ret, err := genHtpasswd(false)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, ret)
})
router.GET("/ca", infoCA)
router.GET("/ca.pem", getCAPEM)
router.POST("/ca/new", func(c *gin.Context) {
var upki PKISettings
if err := json.Unmarshal(body, &upki); err != nil {
return nil, err
err := c.ShouldBindJSON(&upki)
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(
func(team *fic.Team, _ []byte) (interface{}, error) {
if err := pki.GenerateCA(upki.NotBefore, upki.NotAfter); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusCreated, true)
})
router.GET("/certs", getCertificates)
router.POST("/certs", generateClientCert)
router.DELETE("/certs", func(c *gin.Context) {
v, err := fic.ClearCertificates()
if err != nil {
log.Println("Unable to ClearCertificates:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, v)
})
apiCertificatesRoutes := router.Group("/certs/:certid")
apiCertificatesRoutes.Use(CertificateHandler)
apiCertificatesRoutes.HEAD("", getTeamP12File)
apiCertificatesRoutes.GET("", getTeamP12File)
apiCertificatesRoutes.PUT("", updateCertificateAssociation)
apiCertificatesRoutes.DELETE("", func(c *gin.Context) {
cert := c.MustGet("cert").(*fic.Certificate)
v, err := cert.Revoke()
if err != nil {
log.Println("Unable to Revoke:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, v)
})
}
func declareTeamCertificateRoutes(router *gin.RouterGroup) {
router.GET("/certificates", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil {
return nil, err
log.Println("Unable to GetTeamSerials:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else {
var certs []CertExported
for _, serial := range serials {
@ -79,35 +132,69 @@ func init() {
log.Println("Unable to get back certificate, whereas an association exists on disk: ", err)
}
}
return certs, nil
c.JSON(http.StatusOK, certs)
}
})))
})
router.GET("/api/teams/:tid/associations", apiHandler(teamHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return pki.GetTeamAssociations(TeamsDir, team.Id)
})))
router.POST("/api/teams/:tid/associations/:assoc", apiHandler(teamAssocHandler(
func(team *fic.Team, assoc string, _ []byte) (interface{}, error) {
if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, assoc)); err != nil {
return nil, err
router.GET("/associations", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
if err != nil {
log.Println("Unable to GetTeamAssociations:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
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))
router.POST("/api/certs/", apiHandler(generateClientCert))
router.DELETE("/api/certs/", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { return fic.ClearCertificates() }))
c.JSON(http.StatusOK, assocs)
})
router.HEAD("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File)))
router.GET("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File)))
router.PUT("/api/certs/:certid", apiHandler(certificateHandler(updateCertificateAssociation)))
router.DELETE("/api/certs/:certid", apiHandler(certificateHandler(
func(cert *fic.Certificate, _ []byte) (interface{}, error) { return cert.Revoke() })))
apiTeamAssociationsRoutes := router.Group("/associations/:assoc")
apiTeamAssociationsRoutes.POST("", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, c.Params.ByName("assoc"))); err != nil {
log.Println("Unable to create association symlink:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create association symlink: %s", err.Error())})
return
}
c.JSON(http.StatusOK, c.Params.ByName("assoc"))
})
apiTeamAssociationsRoutes.DELETE("", func(c *gin.Context) {
err := pki.DeleteTeamAssociation(TeamsDir, c.Params.ByName("assoc"))
if err != nil {
log.Printf("Unable to DeleteTeamAssociation(%s): %s", c.Params.ByName("assoc"), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete association symlink: %s", err.Error())})
return
}
c.JSON(http.StatusOK, nil)
})
}
func CertificateHandler(c *gin.Context) {
var certid uint64
var err error
cid := strings.TrimSuffix(string(c.Params.ByName("certid")), ".p12")
if certid, err = strconv.ParseUint(cid, 10, 64); err != nil {
if certid, err = strconv.ParseUint(cid, 16, 64); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid certficate identifier"})
return
}
}
cert, err := fic.GetCertificate(certid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Certificate not found"})
return
}
c.Set("cert", cert)
c.Next()
}
func genHtpasswd(ssha bool) (ret string, err error) {
@ -187,13 +274,14 @@ type PKISettings struct {
PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"publicKeyAlgorithm"`
}
func infoCA(_ httprouter.Params, _ []byte) (interface{}, error) {
func infoCA(c *gin.Context) {
_, cacert, err := pki.LoadCA()
if err != nil {
return nil, err
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "CA not found"})
return
}
return PKISettings{
c.JSON(http.StatusOK, PKISettings{
Version: cacert.Version,
SerialNumber: cacert.SerialNumber,
Issuer: cacert.Issuer,
@ -202,47 +290,78 @@ func infoCA(_ httprouter.Params, _ []byte) (interface{}, error) {
NotAfter: cacert.NotAfter,
SignatureAlgorithm: cacert.SignatureAlgorithm,
PublicKeyAlgorithm: cacert.PublicKeyAlgorithm,
}, nil
})
}
func getCAPEM(_ httprouter.Params, _ []byte) (interface{}, error) {
func getCAPEM(c *gin.Context) {
if _, err := os.Stat(pki.CACertPath()); os.IsNotExist(err) {
return nil, errors.New("Unable to locate the CA root certificate. Have you generated it?")
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Unable to locate the CA root certificate. Have you generated it?"})
return
} else if fd, err := os.Open(pki.CACertPath()); err != nil {
return nil, err
log.Println("Unable to open CA root certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else {
defer fd.Close()
return ioutil.ReadAll(fd)
cnt, err := ioutil.ReadAll(fd)
if err != nil {
log.Println("Unable to read CA root certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.String(http.StatusOK, string(cnt))
}
}
func getTeamP12File(cert *fic.Certificate, _ []byte) (interface{}, error) {
func getTeamP12File(c *gin.Context) {
cert := c.MustGet("cert").(*fic.Certificate)
// Create p12 if necessary
if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
if err := pki.WriteP12(cert.Id, cert.Password); err != nil {
return nil, err
log.Println("Unable to WriteP12:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
return nil, errors.New("Unable to locate the p12. Have you generated it?")
log.Println("Unable to compute ClientP12Path:", err.Error())
c.AbortWithError(http.StatusInternalServerError, errors.New("Unable to locate the p12. Have you generated it?"))
return
} else if fd, err := os.Open(pki.ClientP12Path(cert.Id)); err != nil {
return nil, err
log.Println("Unable to open ClientP12Path:", err.Error())
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err.Error()))
return
} else {
defer fd.Close()
return ioutil.ReadAll(fd)
data, err := ioutil.ReadAll(fd)
if err != nil {
log.Println("Unable to open ClientP12Path:", err.Error())
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err.Error()))
return
}
c.Data(http.StatusOK, "application/x-pkcs12", data)
}
}
func generateClientCert(_ httprouter.Params, _ []byte) (interface{}, error) {
func generateClientCert(c *gin.Context) {
// First, generate a new, unique, serial
var serial_gen [8]byte
if _, err := rand.Read(serial_gen[:]); err != nil {
return nil, err
log.Println("Unable to read enough entropy to generate client certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"})
return
}
for fic.ExistingCertSerial(serial_gen) {
if _, err := rand.Read(serial_gen[:]); err != nil {
return nil, err
log.Println("Unable to read enough entropy to generate client certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"})
return
}
}
@ -253,23 +372,35 @@ func generateClientCert(_ httprouter.Params, _ []byte) (interface{}, error) {
// Let's pick a random password
password, err := fic.GeneratePassword()
if err != nil {
return nil, err
log.Println("Unable to generate password:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate password: " + err.Error()})
return
}
// Ok, now load CA
capriv, cacert, err := pki.LoadCA()
if err != nil {
return nil, err
log.Println("Unable to load the CA:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to load the CA"})
return
}
// Generate our privkey
if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil {
return nil, err
log.Println("Unable to generate private key:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate private key: " + err.Error()})
return
}
// Save in DB
cert, err := fic.RegisterCertificate(serial, password)
return CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, nil, cert.Revoked}, err
if err != nil {
log.Println("Unable to register certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register certificate."})
return
}
c.JSON(http.StatusOK, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, nil, cert.Revoked})
}
type CertExported struct {
@ -280,10 +411,13 @@ type CertExported struct {
Revoked *time.Time `json:"revoked"`
}
func getCertificates(_ httprouter.Params, _ []byte) (interface{}, error) {
if certificates, err := fic.GetCertificates(); err != nil {
return nil, err
} else {
func getCertificates(c *gin.Context) {
certificates, err := fic.GetCertificates()
if err != nil {
log.Println("Unable to retrieve certificates list:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during certificates retrieval."})
return
}
ret := make([]CertExported, 0)
for _, cert := range certificates {
dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))
@ -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})
}
return ret, nil
}
c.JSON(http.StatusOK, ret)
}
type CertUploaded struct {
Team *int64 `json:"id_team"`
}
func updateCertificateAssociation(cert *fic.Certificate, body []byte) (interface{}, error) {
func updateCertificateAssociation(c *gin.Context) {
cert := c.MustGet("cert").(*fic.Certificate)
var uc CertUploaded
if err := json.Unmarshal(body, &uc); err != nil {
return nil, err
err := c.ShouldBindJSON(&uc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))
@ -316,19 +454,26 @@ func updateCertificateAssociation(cert *fic.Certificate, body []byte) (interface
if uc.Team != nil {
srcLinkPath := fmt.Sprintf("%d", *uc.Team)
if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil {
return nil, err
log.Println("Unable to create certificate symlink:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create certificate symlink: %s", err.Error())})
return
}
// Mark team as active to ensure it'll be generated
if ut, err := fic.GetTeam(*uc.Team); err != nil {
return nil, err
log.Println("Unable to GetTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team retrieval."})
return
} else if !ut.Active {
ut.Active = true
ut.Update()
_, err := ut.Update()
if err != nil {
log.Println("Unable to UpdateTeam after updateCertificateAssociation:", err.Error())
}
}
} else {
os.Remove(dstLinkPath)
}
return cert, nil
c.JSON(http.StatusOK, cert)
}

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -2,43 +2,79 @@ package api
import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/files/", apiHandler(listFiles))
router.DELETE("/api/files/", apiHandler(clearFiles))
router.GET("/api/files/:fileid", apiHandler(fileHandler(showFile)))
router.PUT("/api/files/:fileid", apiHandler(fileHandler(updateFile)))
router.DELETE("/api/files/:fileid", apiHandler(fileHandler(deleteFile)))
router.DELETE("/api/files/:fileid/dependancies/:depid", apiHandler(fileDependancyHandler(deleteFileDep)))
router.GET("/api/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles)))
router.POST("/api/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile)))
router.GET("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(showFile)))
router.PUT("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(updateFile)))
router.DELETE("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(deleteFile)))
func declareFilesGlobalRoutes(router *gin.RouterGroup) {
router.DELETE("/files/", clearFiles)
// Remote
router.GET("/api/remote/themes/:thid/exercices/:exid/files", apiHandler(sync.ApiGetRemoteExerciceFiles))
router.GET("/remote/themes/:thid/exercices/:exid/files", sync.ApiGetRemoteExerciceFiles)
}
func declareFilesRoutes(router *gin.RouterGroup) {
router.GET("/files", listFiles)
router.POST("/files", createExerciceFile)
apiFilesRoutes := router.Group("/files/:fileid")
apiFilesRoutes.Use(FileHandler)
apiFilesRoutes.GET("", showFile)
apiFilesRoutes.PUT("", updateFile)
apiFilesRoutes.DELETE("", deleteFile)
apiFileDepsRoutes := apiFilesRoutes.Group("/dependancies/:depid")
apiFileDepsRoutes.Use(FileDepHandler)
apiFileDepsRoutes.DELETE("", deleteFileDep)
// Check
router.POST("/api/files/:fileid/check", apiHandler(fileHandler(checkFile)))
apiFilesRoutes.POST("/check", checkFile)
}
// Synchronize
router.POST("/api/sync/exercices/:eid/files", apiHandler(exerciceHandler(
func(exercice *fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceFiles(sync.GlobalImporter, exercice), nil
})))
func FileHandler(c *gin.Context) {
fileid, err := strconv.ParseInt(string(c.Params.ByName("fileid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid file identifier"})
return
}
var file *fic.EFile
if exercice, exists := c.Get("exercice"); exists {
file, err = exercice.(*fic.Exercice).GetFile(fileid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "File not found"})
return
}
} else {
file, err = fic.GetFile(fileid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "File not found"})
return
}
}
c.Set("file", file)
c.Next()
}
func FileDepHandler(c *gin.Context) {
depid, err := strconv.ParseInt(string(c.Params.ByName("depid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid dependency identifier"})
return
}
c.Set("file-depid", depid)
c.Next()
}
type APIFile struct {
@ -87,20 +123,35 @@ func genFileList(in []*fic.EFile, e error) (out []APIFile, err error) {
return
}
func listFiles(_ httprouter.Params, body []byte) (interface{}, error) {
return genFileList(fic.GetFiles())
func listFiles(c *gin.Context) {
var files []APIFile
var err error
if exercice, exists := c.Get("exercice"); exists {
files, err = genFileList(exercice.(*fic.Exercice).GetFiles())
} else {
files, err = genFileList(fic.GetFiles())
}
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, files)
}
func listExerciceFiles(exercice *fic.Exercice, body []byte) (interface{}, error) {
return genFileList(exercice.GetFiles())
func clearFiles(c *gin.Context) {
_, err := fic.ClearFiles()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, true)
}
func clearFiles(_ httprouter.Params, _ []byte) (interface{}, error) {
return fic.ClearFiles()
}
func showFile(file *fic.EFile, _ []byte) (interface{}, error) {
return file, nil
func showFile(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("file").(*fic.EFile))
}
type uploadedFile struct {
@ -108,45 +159,92 @@ type uploadedFile struct {
Digest string
}
func createExerciceFile(exercice *fic.Exercice, body []byte) (interface{}, error) {
var uf uploadedFile
if err := json.Unmarshal(body, &uf); err != nil {
return nil, err
func createExerciceFile(c *gin.Context) {
exercice, exists := c.Get("exercice")
if !exists {
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "File can only be added inside an exercice."})
return
}
return sync.ImportFile(sync.GlobalImporter, uf.URI,
var uf uploadedFile
err := c.ShouldBindJSON(&uf)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ret, err := sync.ImportFile(sync.GlobalImporter, uf.URI,
func(filePath string, origin string) (interface{}, error) {
if digest, err := hex.DecodeString(uf.Digest); err != nil {
return nil, err
} else {
return exercice.ImportFile(filePath, origin, digest)
return exercice.(*fic.Exercice).ImportFile(filePath, origin, digest)
}
})
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, ret)
}
func updateFile(file *fic.EFile, body []byte) (interface{}, error) {
func updateFile(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
var uf fic.EFile
if err := json.Unmarshal(body, &uf); err != nil {
return nil, err
err := c.ShouldBindJSON(&uf)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
uf.Id = file.Id
if _, err := uf.Update(); err != nil {
return nil, err
} else {
return uf, nil
log.Println("Unable to updateFile:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."})
return
}
c.JSON(http.StatusOK, uf)
}
func deleteFile(file *fic.EFile, _ []byte) (interface{}, error) {
return file.Delete()
func deleteFile(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
_, err := file.Delete()
if err != nil {
log.Println("Unable to updateFile:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."})
return
}
c.JSON(http.StatusOK, true)
}
func deleteFileDep(file *fic.EFile, depid int, _ []byte) (interface{}, error) {
return true, file.DeleteDepend(&fic.FlagKey{Id: depid})
func deleteFileDep(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
depid := c.MustGet("file-depid").(int64)
err := file.DeleteDepend(&fic.FlagKey{Id: int(depid)})
if err != nil {
log.Println("Unable to deleteFileDep:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete file dependency."})
return
}
c.JSON(http.StatusOK, true)
}
func checkFile(file *fic.EFile, _ []byte) (interface{}, error) {
return true, file.CheckFileOnDisk()
func checkFile(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
err := file.CheckFileOnDisk()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, true)
}

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 (
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
@ -10,26 +11,26 @@ import (
"srs.epita.fr/fic-server/admin/pki"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
var TimestampCheck = "submissions"
func init() {
router.GET("/api/timestamps.json", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
if stat, err := os.Stat(TimestampCheck); err != nil {
return nil, err
} else {
func declareHealthRoutes(router *gin.RouterGroup) {
router.GET("/timestamps.json", func(c *gin.Context) {
stat, err := os.Stat(TimestampCheck)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("timestamp.json: %s", err.Error())})
return
}
now := time.Now().UTC()
return map[string]interface{}{
c.JSON(http.StatusOK, gin.H{
"frontend": stat.ModTime().UTC(),
"backend": now,
"diffFB": now.Sub(stat.ModTime()),
}, nil
}
}))
router.GET("/api/health.json", apiHandler(GetHealth))
})
})
router.GET("/health.json", GetHealth)
}
type healthFileReport struct {
@ -50,7 +51,7 @@ func getHealth(pathname string) (ret []healthFileReport) {
p := path.Join(pathname, d.Name())
if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 {
ret = append(ret, getHealth(p)...)
} else if !d.IsDir() && d.Mode()&os.ModeSymlink == 0 && time.Since(d.ModTime()) > 2 * time.Second {
} else if !d.IsDir() && d.Mode()&os.ModeSymlink == 0 && time.Since(d.ModTime()) > 2*time.Second {
teamDir := strings.TrimPrefix(pathname, TimestampCheck)
idteam, _ := pki.GetAssociation(path.Join(TeamsDir, teamDir))
ret = append(ret, healthFileReport{
@ -64,10 +65,11 @@ func getHealth(pathname string) (ret []healthFileReport) {
}
}
func GetHealth(httprouter.Params, []byte) (interface{}, error) {
func GetHealth(c *gin.Context) {
if _, err := os.Stat(TimestampCheck); err != nil {
return nil, err
} else {
return getHealth(TimestampCheck), nil
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("health.json: %s", err.Error())})
return
}
c.JSON(http.StatusOK, getHealth(TimestampCheck))
}

View file

@ -3,20 +3,20 @@ package api
import (
"bufio"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/monitor", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return map[string]interface{}{
func declareMonitorRoutes(router *gin.RouterGroup) {
router.GET("/monitor", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"localhost": genLocalConstants(),
}, nil
}))
})
})
}
func readLoadAvg(fd *os.File) (ret map[string]float64) {
@ -58,7 +58,7 @@ func readCPUStats(fd *os.File) (ret map[string]map[string]uint64) {
ret[f[0]] = map[string]uint64{}
var total uint64 = 0
for i, k := range []string{"user", "nice", "system", "idle", "iowait", "irq", "softirq"} {
if v, err := strconv.ParseUint(f[i + 1], 10, 64); err == nil {
if v, err := strconv.ParseUint(f[i+1], 10, 64); err == nil {
ret[f[0]][k] = v
total += v
}

View file

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

View file

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

View file

@ -1,84 +1,118 @@
package api
import (
"encoding/json"
"errors"
"log"
"net/http"
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.POST("/api/qa/", apiHandler(importExerciceQA))
router.POST("/api/qa/:qid/comments", apiHandler(qaHandler(importQAComment)))
func declareQARoutes(router *gin.RouterGroup) {
router.POST("/qa/", importExerciceQA)
apiQARoutes := router.Group("/qa/:qid")
apiQARoutes.POST("/comments", importQAComment)
}
func qaHandler(f func(*fic.QAQuery, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func(ps httprouter.Params, body []byte) (interface{}, error) {
if qid, err := strconv.ParseInt(string(ps.ByName("qid")), 10, 64); err != nil {
return nil, err
} else if query, err := fic.GetQAQuery(qid); err != nil {
return nil, err
} else {
return f(query, body)
func QAHandler(c *gin.Context) {
qid, err := strconv.ParseInt(string(c.Params.ByName("qid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid QA identifier"})
return
}
qa, err := fic.GetQAQuery(qid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "QA query not found"})
return
}
c.Set("qa-query", qa)
c.Next()
}
func importExerciceQA(_ httprouter.Params, body []byte) (interface{}, error) {
func importExerciceQA(c *gin.Context) {
// Create a new query
var uq fic.QAQuery
if err := json.Unmarshal(body, &uq); err != nil {
return nil, err
err := c.ShouldBindJSON(&uq)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
var exercice *fic.Exercice
var err error
if uq.IdExercice == 0 {
return nil, errors.New("id_exercice not filled")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "id_exercice not filled"})
return
} else if exercice, err = fic.GetExercice(uq.IdExercice); err != nil {
return nil, err
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unable to find requested exercice"})
return
}
if len(uq.State) == 0 {
return nil, errors.New("State not filled")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "State not filled"})
return
}
if len(uq.Subject) == 0 {
return nil, errors.New("Subject not filled")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Subject not filled"})
return
}
if qa, err := exercice.NewQAQuery(uq.Subject, uq.IdTeam, uq.User, uq.State); err != nil {
return nil, err
log.Println("Unable to importExerciceQA:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during query creation."})
return
} else {
qa.Creation = uq.Creation
qa.Solved = uq.Solved
qa.Closed = qa.Closed
_, err = qa.Update()
return qa, err
if err != nil {
log.Println("Unable to update in importExerciceQA:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during query updating."})
return
}
c.JSON(http.StatusOK, qa)
}
}
func importQAComment(query *fic.QAQuery, body []byte) (interface{}, error) {
func importQAComment(c *gin.Context) {
query := c.MustGet("qa-query").(*fic.QAQuery)
// Create a new query
var uc fic.QAComment
if err := json.Unmarshal(body, &uc); err != nil {
return nil, err
err := c.ShouldBindJSON(&uc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if len(uc.Content) == 0 {
return nil, errors.New("Empty comment")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Empty comment"})
return
}
if qac, err := query.AddComment(uc.Content, uc.IdTeam, uc.User); err != nil {
return nil, err
log.Println("Unable to AddComment in importQAComment:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during comment creation."})
return
} else {
qac.Date = uc.Date
_, err = qac.Update()
return qac, err
if err != nil {
log.Println("Unable to Update comment in importQAComment")
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during comment creation."})
return
}
c.JSON(http.StatusOK, qac)
}
}

View file

@ -1,11 +1,26 @@
package api
import (
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
var router = httprouter.New()
func DeclareRoutes(router *gin.RouterGroup) {
apiRoutes := router.Group("/api")
func Router() *httprouter.Router {
return router
declareCertificateRoutes(apiRoutes)
declareClaimsRoutes(apiRoutes)
declareEventsRoutes(apiRoutes)
declareExercicesRoutes(apiRoutes)
declareFilesRoutes(apiRoutes)
declareGlobalExercicesRoutes(apiRoutes)
declareHealthRoutes(apiRoutes)
declareMonitorRoutes(apiRoutes)
declarePasswordRoutes(apiRoutes)
declarePublicRoutes(apiRoutes)
declareQARoutes(apiRoutes)
declareTeamsRoutes(apiRoutes)
declareThemesRoutes(apiRoutes)
declareSettingsRoutes(apiRoutes)
declareSyncRoutes(apiRoutes)
DeclareVersionRoutes(apiRoutes)
}

View file

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

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 (
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/teams.json", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return fic.ExportTeams(false)
}))
router.GET("/api/teams-members.json", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return fic.ExportTeams(true)
}))
router.GET("/api/teams-binding", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return bindingTeams()
}))
router.GET("/api/teams-nginx", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return nginxGenTeams()
}))
router.POST("/api/disableinactiveteams", apiHandler(disableInactiveTeams))
router.POST("/api/enableallteams", apiHandler(enableAllTeams))
router.GET("/api/teams-members-nginx", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return nginxGenMember()
}))
router.GET("/api/teams-tries.json", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return fic.GetTries(nil, nil)
}))
router.GET("/api/teams/", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return fic.GetTeams()
}))
router.POST("/api/teams/", apiHandler(createTeam))
router.GET("/api/teams/:tid/", apiHandler(teamHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return team, nil
})))
router.PUT("/api/teams/:tid/", apiHandler(teamHandler(updateTeam)))
router.POST("/api/teams/:tid/", apiHandler(teamHandler(addTeamMember)))
router.DELETE("/api/teams/:tid/", apiHandler(teamHandler(deleteTeam)))
router.GET("/api/teams/:tid/score-grid.json", apiHandler(teamHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return team.ScoreGrid()
})))
router.GET("/api/teams/:tid/my.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return fic.MyJSONTeam(team, true)
})))
router.GET("/api/teams/:tid/wait.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return fic.MyJSONTeam(team, false)
})))
router.GET("/api/teams/:tid/stats.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
if team != nil {
return team.GetStats()
} else {
return fic.GetTeamsStats(nil)
func declareTeamsRoutes(router *gin.RouterGroup) {
router.GET("/teams.json", func(c *gin.Context) {
teams, err := fic.ExportTeams(false)
if err != nil {
log.Println("Unable to ExportTeams:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."})
return
}
})))
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)
c.JSON(http.StatusOK, teams)
})
router.GET("/teams-members.json", func(c *gin.Context) {
teams, err := fic.ExportTeams(true)
if err != nil {
log.Println("Unable to ExportTeams:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."})
return
}
})))
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)))
c.JSON(http.StatusOK, teams)
})
router.GET("/teams-binding", bindingTeams)
router.GET("/teams-nginx", nginxGenTeams)
router.POST("/disableinactiveteams", disableInactiveTeams)
router.POST("/enableallteams", enableAllTeams)
router.GET("/teams-members-nginx", nginxGenMember)
router.GET("/teams-tries.json", func(c *gin.Context) {
tries, err := fic.GetTries(nil, nil)
if err != nil {
log.Println("Unable to GetTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."})
return
}
c.JSON(http.StatusOK, tries)
})
router.GET("/teams", func(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams listing."})
return
}
c.JSON(http.StatusOK, teams)
})
router.POST("/teams", createTeam)
apiTeamsRoutes := router.Group("/teams/:tid")
apiTeamsRoutes.Use(TeamHandler)
apiTeamsRoutes.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("team").(*fic.Team))
})
apiTeamsRoutes.PUT("/", updateTeam)
apiTeamsRoutes.POST("/", addTeamMember)
apiTeamsRoutes.DELETE("/", deleteTeam)
apiTeamsRoutes.GET("/score-grid.json", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
sg, err := team.ScoreGrid()
if err != nil {
log.Printf("Unable to get ScoreGrid(tid=%d): %s", team.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during score grid calculation."})
return
}
c.JSON(http.StatusOK, sg)
})
apiTeamsPublicRoutes := router.Group("/teams/:tid")
apiTeamsPublicRoutes.Use(TeamPublicHandler)
apiTeamsPublicRoutes.GET("/my.json", func(c *gin.Context) {
tfile, err := fic.MyJSONTeam(c.MustGet("team").(*fic.Team), true)
if err != nil {
log.Println("Unable to get MyJSONTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."})
return
}
c.JSON(http.StatusOK, tfile)
})
apiTeamsPublicRoutes.GET("/wait.json", func(c *gin.Context) {
tfile, err := fic.MyJSONTeam(c.MustGet("team").(*fic.Team), false)
if err != nil {
log.Println("Unable to get MyJSONTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."})
return
}
c.JSON(http.StatusOK, tfile)
})
apiTeamsPublicRoutes.GET("/stats.json", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
if team != nil {
stats, err := team.GetStats()
if err != nil {
log.Println("Unable to get GetStats:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during stats calculation."})
return
}
c.JSON(http.StatusOK, stats)
} else {
stats, err := fic.GetTeamsStats(nil)
if err != nil {
log.Println("Unable to get GetTeamsStats:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during global stats calculation."})
return
}
c.JSON(http.StatusOK, stats)
}
})
apiTeamsRoutes.GET("/history.json", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
history, err := team.GetHistory()
if err != nil {
log.Println("Unable to get GetHistory:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history calculation."})
return
}
c.JSON(http.StatusOK, history)
})
apiTeamsRoutes.DELETE("/history.json", delHistory)
apiTeamsPublicRoutes.GET("/tries", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
tries, err := fic.GetTries(team, nil)
if err != nil {
log.Println("Unable to GetTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during tries calculation."})
return
}
c.JSON(http.StatusOK, tries)
})
apiTeamsRoutes.GET("/members", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
members, err := team.GetMembers()
if err != nil {
log.Println("Unable to GetMembers:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during members retrieval."})
return
}
c.JSON(http.StatusOK, members)
})
apiTeamsRoutes.POST("/members", addTeamMember)
apiTeamsRoutes.PUT("/members", setTeamMember)
declareTeamsPasswordRoutes(apiTeamsRoutes)
declareTeamClaimsRoutes(apiTeamsRoutes)
declareTeamCertificateRoutes(apiTeamsRoutes)
}
func nginxGenTeams() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
func TeamHandler(c *gin.Context) {
tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"})
return
}
team, err := fic.GetTeam(tid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"})
return
}
c.Set("team", team)
c.Next()
}
func TeamPublicHandler(c *gin.Context) {
tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"})
return
}
if tid != 0 {
team, err := fic.GetTeam(tid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"})
return
}
c.Set("team", team)
} else {
c.Set("team", nil)
}
c.Next()
}
func nginxGenTeams(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, team := range teams {
ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", strings.ToLower(team.Name), team.Id)
}
return ret, nil
}
c.String(http.StatusOK, ret)
}
func nginxGenMember() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
func nginxGenMember(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err == nil {
@ -119,22 +246,27 @@ func nginxGenMember() (string, error) {
ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id)
}
} else {
return "", err
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
return ret, nil
}
c.String(http.StatusOK, ret)
}
func bindingTeams() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
func bindingTeams(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err != nil {
return "", err
c.AbortWithError(http.StatusInternalServerError, err)
return
} else {
var mbs []string
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, ";"))
}
}
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
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if ut.Color == 0 {
ut.Color = fic.HSL{rand.Float64(), 1, 0.5}.ToRGB()
}
return fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId)
team, err := fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId)
if err != nil {
log.Println("Unable to CreateTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team creation."})
return
}
c.JSON(http.StatusOK, team)
}
func updateTeam(team *fic.Team, body []byte) (interface{}, error) {
func updateTeam(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var ut fic.Team
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ut.Id = team.Id
@ -172,28 +317,37 @@ func updateTeam(team *fic.Team, body []byte) (interface{}, error) {
ut.Password = nil
}
if _, err := ut.Update(); err != nil {
return nil, err
_, err = ut.Update()
if err != nil {
log.Println("Unable to updateTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team updating."})
return
}
return ut, nil
c.JSON(http.StatusOK, team)
}
func disableInactiveTeams(_ httprouter.Params, _ []byte) (interface{}, error) {
if teams, err := fic.GetTeams(); err != nil {
return nil, err
} else {
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
}
for _, team := range teams {
var serials []uint64
serials, err = pki.GetTeamSerials(TeamsDir, team.Id)
if err != nil {
return nil, err
c.AbortWithError(http.StatusInternalServerError, err)
return
}
var assocs []string
assocs, err = pki.GetTeamAssociations(TeamsDir, team.Id)
if err != nil {
return nil, err
c.AbortWithError(http.StatusInternalServerError, err)
return
}
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(_ httprouter.Params, _ []byte) (interface{}, error) {
if teams, err := fic.GetTeams(); err != nil {
return nil, err
} else {
func enableAllTeams(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
for _, team := range teams {
if !team.Active {
team.Active = true
@ -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)
if err != nil {
return nil, err
log.Printf("Unable to GetTeamAssociations(tid=%s): %s", team.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve team association."})
return
}
for _, assoc := range assocs {
err = pki.DeleteTeamAssociation(TeamsDir, assoc)
}
if err != nil {
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
}
c.JSON(http.StatusOK, true)
}
func addTeamMember(team *fic.Team, body []byte) (interface{}, error) {
func addTeamMember(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var members []fic.Member
if err := json.Unmarshal(body, &members); err != nil {
return nil, err
err := c.ShouldBindJSON(&members)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
for _, member := range members {
team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company))
_, err := team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company))
if err != nil {
log.Println("Unable to AddMember:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during member creation."})
return
}
}
return team.GetMembers()
mmbrs, err := team.GetMembers()
if err != nil {
log.Println("Unable to retrieve members list:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve members list."})
return
}
c.JSON(http.StatusOK, mmbrs)
}
func setTeamMember(team *fic.Team, body []byte) (interface{}, error) {
var members []fic.Member
if err := json.Unmarshal(body, &members); err != nil {
return nil, err
}
func setTeamMember(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
team.ClearMembers()
for _, member := range members {
team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company))
}
return team.GetMembers()
addTeamMember(c)
}
type uploadedHistory struct {
@ -294,11 +469,21 @@ func updateHistory(team *fic.Team, body []byte) (interface{}, error) {
return team.UpdateHistoryCoeff(uh.Kind, uh.Time, givenId, uh.Coefficient)
}
func delHistory(team *fic.Team, body []byte) (interface{}, error) {
func delHistory(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var uh uploadedHistory
if err := json.Unmarshal(body, &uh); err != nil {
return nil, err
err := c.ShouldBindJSON(&uh)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
return team.DelHistoryItem(uh.Kind, uh.Time, uh.Primary, uh.Secondary)
_, err = team.DelHistoryItem(uh.Kind, uh.Time, uh.Primary, uh.Secondary)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete this history line: %s", err.Error())})
return
}
c.JSON(http.StatusOK, true)
}

View file

@ -1,171 +1,75 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"path"
"strconv"
"strings"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/themes", apiHandler(listThemes))
router.POST("/api/themes", apiHandler(createTheme))
router.GET("/api/themes.json", apiHandler(exportThemes))
router.GET("/api/session-forensic.yaml", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) {
func declareThemesRoutes(router *gin.RouterGroup) {
router.GET("/themes", listThemes)
router.POST("/themes", createTheme)
router.GET("/themes.json", exportThemes)
router.GET("/session-forensic.yaml", func(c *gin.Context) {
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
return nil, err
} else if c, err := settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
return nil, err
log.Printf("Unable to ReadSettings: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during settings reading."})
return
} else if ch, err := settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
log.Printf("Unable to ReadChallengeInfo: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during challenge info reading."})
return
} else if sf, err := fic.GenZQDSSessionFile(ch, s); err != nil {
log.Printf("Unable to GenZQDSSessionFile: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during session file generation."})
return
} else {
return fic.GenZQDSSessionFile(c, s)
c.JSON(http.StatusOK, sf)
}
}))
router.GET("/api/files-bindings", apiHandler(bindingFiles))
})
router.GET("/files-bindings", bindingFiles)
router.GET("/api/themes/:thid", apiHandler(themeHandler(showTheme)))
router.PUT("/api/themes/:thid", apiHandler(themeHandler(updateTheme)))
router.DELETE("/api/themes/:thid", apiHandler(themeHandler(deleteTheme)))
apiThemesRoutes := router.Group("/themes/:thid")
apiThemesRoutes.Use(ThemeHandler)
apiThemesRoutes.GET("", showTheme)
apiThemesRoutes.PUT("", updateTheme)
apiThemesRoutes.DELETE("", deleteTheme)
router.GET("/api/themes/:thid/exercices", apiHandler(themeHandler(listThemedExercices)))
router.POST("/api/themes/:thid/exercices", apiHandler(themeHandler(createExercice)))
router.GET("/api/themes/:thid/exercices_stats.json", apiHandler(themeHandler(getThemedExercicesStats)))
router.GET("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(showExercice)))
router.PUT("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(updateExercice)))
router.DELETE("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(deleteExercice)))
router.GET("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles)))
router.POST("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile)))
router.GET("/api/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints)))
router.POST("/api/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint)))
router.GET("/api/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(listExerciceFlags)))
router.POST("/api/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(createExerciceFlag)))
declareExercicesRoutes(apiThemesRoutes)
// Remote
router.GET("/api/remote/themes", apiHandler(sync.ApiListRemoteThemes))
router.GET("/api/remote/themes/:thid", apiHandler(sync.ApiGetRemoteTheme))
router.GET("/api/remote/themes/:thid/exercices", apiHandler(sync.ApiListRemoteExercices))
// Synchronize
router.GET("/api/sync/deep", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) {
if sync.DeepSyncProgress == 0 {
return nil, errors.New("Pas de synchronisation en cours")
} else {
return map[string]interface{}{"progress": sync.DeepSyncProgress}, nil
}
}))
router.POST("/api/sync/base", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) {
err := sync.GlobalImporter.Sync()
return true, err
}))
router.POST("/api/sync/speed", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) {
st := sync.SpeedySyncDeep(sync.GlobalImporter)
sync.EditDeepReport(st, false)
return st, nil
}))
router.POST("/api/sync/deep", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) {
return sync.SyncDeep(sync.GlobalImporter), nil
}))
router.POST("/api/sync/deep/:thid", apiHandler(themeHandler(
func(theme *fic.Theme, _ []byte) (interface{}, error) {
st := sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250)
sync.EditDeepReport(map[string][]string{theme.Name: st}, false)
sync.DeepSyncProgress = 255
return st, nil
})))
router.POST("/api/sync/auto/*p", apiHandler(
func(ps httprouter.Params, _ []byte) (interface{}, error) {
p := strings.TrimPrefix(ps.ByName("p"), "/")
themes, err := fic.GetThemes()
if err != nil {
return nil, err
}
if p == "" {
if !IsProductionEnv {
for _, theme := range themes {
theme.DeleteDeep()
}
}
st := sync.SyncDeep(sync.GlobalImporter)
return st, nil
}
for _, theme := range themes {
if theme.Path == p {
if !IsProductionEnv {
exercices, err := theme.GetExercices()
if err == nil {
for _, exercice := range exercices {
exercice.DeleteDeep()
}
}
}
st := sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250)
sync.EditDeepReport(map[string][]string{theme.Name: st}, false)
sync.DeepSyncProgress = 255
settings.ForceRegeneration()
return st, nil
}
}
return nil, fmt.Errorf("Theme not found %q", p)
}))
router.POST("/api/sync/themes", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) {
return sync.SyncThemes(sync.GlobalImporter), nil
}))
router.POST("/api/sync/themes/:thid/exercices", apiHandler(themeHandler(
func(theme *fic.Theme, _ []byte) (interface{}, error) {
return sync.SyncExercices(sync.GlobalImporter, theme), nil
})))
router.POST("/api/sync/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(
func(exercice *fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceFiles(sync.GlobalImporter, exercice), nil
})))
router.POST("/api/sync/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(
func(exercice *fic.Exercice, _ []byte) (interface{}, error) {
_, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice))
return errs, nil
})))
router.POST("/api/sync/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(
func(exercice *fic.Exercice, _ []byte) (interface{}, error) {
_, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice)
_, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice))
return append(errs, herrs...), nil
})))
router.POST("/api/sync/themes/:thid/fixurlid", apiHandler(themeHandler(
func(theme *fic.Theme, _ []byte) (interface{}, error) {
if theme.FixURLId() {
return theme.Update()
}
return 0, nil
})))
router.POST("/api/sync/fixurlids", apiHandler(fixAllURLIds))
router.GET("/remote/themes", sync.ApiListRemoteThemes)
router.GET("/remote/themes/:thid", sync.ApiGetRemoteTheme)
router.GET("/remote/themes/:thid/exercices", sync.ApiListRemoteExercices)
}
func fixAllURLIds(_ httprouter.Params, _ []byte) (interface{}, error) {
func ThemeHandler(c *gin.Context) {
thid, err := strconv.ParseInt(string(c.Params.ByName("thid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid theme identifier"})
return
}
theme, err := fic.GetTheme(thid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
return
}
c.Set("theme", theme)
c.Next()
}
func fixAllURLIds(c *gin.Context) {
nbFix := 0
if themes, err := fic.GetThemes(); err == nil {
for _, theme := range themes {
@ -185,19 +89,22 @@ func fixAllURLIds(_ httprouter.Params, _ []byte) (interface{}, error) {
}
}
return nbFix, nil
c.JSON(http.StatusOK, nbFix)
}
func bindingFiles(_ httprouter.Params, body []byte) (interface{}, error) {
if files, err := fic.GetFiles(); err != nil {
return "", err
} else {
func bindingFiles(c *gin.Context) {
files, err := fic.GetFiles()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, file := range files {
ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
}
return ret, nil
}
c.String(http.StatusOK, ret)
}
func getExercice(args []string) (*fic.Exercice, error) {
@ -212,60 +119,92 @@ func getExercice(args []string) (*fic.Exercice, error) {
}
}
func listThemes(_ httprouter.Params, _ []byte) (interface{}, error) {
return fic.GetThemes()
func listThemes(c *gin.Context) {
themes, err := fic.GetThemes()
if err != nil {
log.Println("Unable to listThemes:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to list themes."})
return
}
c.JSON(http.StatusOK, themes)
}
func exportThemes(_ httprouter.Params, _ []byte) (interface{}, error) {
return fic.ExportThemes()
func exportThemes(c *gin.Context) {
themes, err := fic.ExportThemes()
if err != nil {
log.Println("Unable to exportthemes:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to export themes."})
return
}
c.JSON(http.StatusOK, themes)
}
func showTheme(theme *fic.Theme, _ []byte) (interface{}, error) {
return theme, nil
func showTheme(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("theme").(*fic.Theme))
}
func listThemedExercices(theme *fic.Theme, _ []byte) (interface{}, error) {
return theme.GetExercices()
}
func showThemedExercice(theme *fic.Theme, exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice, nil
}
func createTheme(_ httprouter.Params, body []byte) (interface{}, error) {
func createTheme(c *gin.Context) {
var ut fic.Theme
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if len(ut.Name) == 0 {
return nil, errors.New("Theme's name not filled")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"})
return
}
return fic.CreateTheme(&ut)
th, err := fic.CreateTheme(&ut)
if err != nil {
log.Println("Unable to createTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during theme creation."})
return
}
c.JSON(http.StatusOK, th)
}
func updateTheme(theme *fic.Theme, body []byte) (interface{}, error) {
func updateTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
var ut fic.Theme
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ut.Id = theme.Id
if len(ut.Name) == 0 {
return nil, errors.New("Theme's name not filled")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"})
return
}
if _, err := ut.Update(); err != nil {
return nil, err
} else {
return ut, nil
log.Println("Unable to updateTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme update."})
return
}
c.JSON(http.StatusOK, ut)
}
func deleteTheme(theme *fic.Theme, _ []byte) (interface{}, error) {
return theme.Delete()
func deleteTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
_, err := theme.Delete()
if err != nil {
log.Println("Unable to deleteTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme deletion."})
return
}
c.JSON(http.StatusOK, true)
}
func getThemedExercicesStats(theme *fic.Theme, body []byte) (interface{}, error) {

View file

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

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

View file

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

View file

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

View file

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

View file

@ -6,11 +6,12 @@ import (
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
_ "golang.org/x/crypto/blake2b"
"srs.epita.fr/fic-server/libfic"
@ -140,21 +141,24 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int
}
// ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints.
func ApiGetRemoteExerciceHints(ps httprouter.Params, _ []byte) (interface{}, error) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid"))
func ApiGetRemoteExerciceHints(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, ps.ByName("exid")), nil)
exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil)
if exercice != nil {
hints, errs := CheckExerciceHints(GlobalImporter, exercice)
if hints != nil {
return hints, nil
} else {
return hints, fmt.Errorf("%q", errs)
c.JSON(http.StatusOK, hints)
return
}
} else {
return exercice, fmt.Errorf("%q", errs)
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
} else {
return nil, fmt.Errorf("%q", errs)
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
}

View file

@ -3,13 +3,14 @@ package sync
import (
"fmt"
"math/rand"
"net/http"
"path"
"sort"
"strconv"
"strings"
"unicode"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
"srs.epita.fr/fic-server/libfic"
)
@ -554,21 +555,25 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice) (kmap map[int64]fic.F
}
// ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags.
func ApiGetRemoteExerciceFlags(ps httprouter.Params, _ []byte) (interface{}, error) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid"))
func ApiGetRemoteExerciceFlags(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, ps.ByName("exid")), nil)
exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil)
if exercice != nil {
flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{})
if flags != nil {
return flags, nil
} else {
return flags, fmt.Errorf("%q", errs)
c.JSON(http.StatusOK, flags)
return
}
} else {
return exercice, fmt.Errorf("%q", errs)
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
} else {
return nil, fmt.Errorf("%q", errs)
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}

View file

@ -3,12 +3,13 @@ package sync
import (
"fmt"
"log"
"net/http"
"path"
"strconv"
"strings"
"github.com/BurntSushi/toml"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
"github.com/russross/blackfriday/v2"
"srs.epita.fr/fic-server/libfic"
@ -308,26 +309,36 @@ func SyncExercices(i Importer, theme *fic.Theme) (errs []string) {
}
// ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list.
func ApiListRemoteExercices(ps httprouter.Params, _ []byte) (interface{}, error) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid"))
func ApiListRemoteExercices(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
return GetExercices(GlobalImporter, theme)
exercices, err := GetExercices(GlobalImporter, theme)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, exercices)
} else {
return nil, fmt.Errorf("%q", errs)
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
}
// ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
func ApiGetRemoteExercice(ps httprouter.Params, _ []byte) (interface{}, error) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid"))
func ApiGetRemoteExercice(c *gin.Context) {
theme, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, ps.ByName("exid")), nil)
exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil)
if exercice != nil {
return exercice, nil
c.JSON(http.StatusOK, exercice)
return
} else {
return exercice, fmt.Errorf("%q", errs)
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
} else {
return nil, fmt.Errorf("%q", errs)
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
}

View file

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

1
go.mod
View file

@ -5,6 +5,7 @@ go 1.16
require (
github.com/BurntSushi/toml v1.1.0
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gin-gonic/gin v1.7.7 // indirect
github.com/go-git/go-git/v5 v5.4.2
github.com/go-sql-driver/mysql v1.6.0
github.com/julienschmidt/httprouter v1.3.0

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/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
@ -78,6 +82,13 @@ github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -114,6 +125,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -134,6 +146,8 @@ github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
@ -147,9 +161,17 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -164,6 +186,7 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8 h1:ipNUBPHSUmHhhcLhvqC2vGZsJPzVuJap8rJx3uGAEco=
@ -172,6 +195,10 @@ github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df h1:C+J/LwTqP8g
github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 h1:b2nJXyPCa9HY7giGM+kYcnQ71m14JnGdQabMPmyt++8=
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -345,6 +372,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -509,6 +537,8 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -74,6 +74,12 @@ func GetFile(id int64) (f *EFile, err error) {
return
}
func (e *Exercice) GetFile(id int64) (f *EFile, err error) {
f = &EFile{}
err = DBQueryRow("SELECT id_file, origin, path, name, cksum, size FROM exercice_files WHERE id_file = ? AND id_exercice = ?", id, e.Id).Scan(&f.Id, &f.origin, &f.Path, &f.Name, &f.Checksum, &f.Size)
return
}
// GetFileByPath retrieves the file that should be found at the given location.
func GetFileByPath(path string) (*EFile, error) {
path = strings.TrimPrefix(path, FilesDir)

View file

@ -78,6 +78,13 @@ func GetFlagKey(id int) (k *FlagKey, err error) {
return
}
// GetFlagKey returns a flag.
func (e *Exercice) GetFlagKey(id int) (k *FlagKey, err error) {
k = &FlagKey{}
err = DBQueryRow("SELECT id_flag, id_exercice, ordre, label, type, placeholder, help, unit, ignorecase, notrim, multiline, validator_regexp, sort_re_grps, cksum, choices_cost FROM exercice_flags WHERE id_flag = ? AND id_exercice = ?", id, e.Id).Scan(&k.Id, &k.IdExercice, &k.Order, &k.Label, &k.Type, &k.Placeholder, &k.Help, &k.Unit, &k.IgnoreCase, &k.NoTrim, &k.Multiline, &k.ValidatorRegexp, &k.SortReGroups, &k.Checksum, &k.ChoicesCost)
return
}
// GetFlagKeyByLabel returns a flag matching the given label.
func (e *Exercice) GetFlagKeyByLabel(label string) (k *FlagKey, err error) {
k = &FlagKey{}

View file

@ -46,6 +46,17 @@ func GetHint(id int64) (*EHint, error) {
return h, nil
}
// GetHint retrieves the hint with the given id.
func (e *Exercice) GetHint(id int64) (*EHint, error) {
h := &EHint{}
if err := DBQueryRow("SELECT id_hint, id_exercice, title, content, cost FROM exercice_hints WHERE id_hint = ? AND id_exercice = ?", id, e.Id).Scan(&h.Id, &h.IdExercice, &h.Title, &h.Content, &h.Cost); err != nil {
return nil, err
}
treatHintContent(h)
return h, nil
}
// GetHintByTitle retrieves the hint with the given id.
func (e *Exercice) GetHintByTitle(id int64) (*EHint, error) {
h := &EHint{}

View file

@ -31,7 +31,7 @@ type MCQ_entry struct {
// GetMCQ returns a list of flags comming with the challenge.
func GetMCQ(id int) (m *MCQ, err error) {
m = &MCQ{}
err = DBQueryRow("SELECT id_mcq, id_exercice, order, title FROM exercice_mcq WHERE id_mcq = ?", id).Scan(&m.Id, &m.IdExercice, &m.Order, &m.Title)
err = DBQueryRow("SELECT id_mcq, id_exercice, ordre, title FROM exercice_mcq WHERE id_mcq = ?", id).Scan(&m.Id, &m.IdExercice, &m.Order, &m.Title)
m.fillEntries()
return
}
@ -84,6 +84,14 @@ func (e *Exercice) GetMCQ() ([]*MCQ, error) {
}
}
// GetMCQById returns a MCQs.
func (e *Exercice) GetMCQById(id int) (m *MCQ, err error) {
m = &MCQ{}
err = DBQueryRow("SELECT id_mcq, id_exercice, ordre, title FROM exercice_mcq WHERE id_mcq = ? AND id_exercice = ?", id, e.Id).Scan(&m.Id, &m.IdExercice, &m.Order, &m.Title)
m.fillEntries()
return
}
// GetMCQbyChoice returns the MCQ corresponding to a choice ID.
func GetMCQbyChoice(cid int) (m *MCQ, c *MCQ_entry, err error) {
m = &MCQ{}