Write docs!

This commit is contained in:
nemunaire 2018-03-09 19:07:08 +01:00
parent c460bb7bf5
commit bcc598ebd5
37 changed files with 478 additions and 188 deletions

View File

@ -21,7 +21,9 @@ func init() {
router.GET("/api/ca/", apiHandler(infoCA)) router.GET("/api/ca/", apiHandler(infoCA))
router.GET("/api/ca.pem", apiHandler(getCAPEM)) router.GET("/api/ca.pem", apiHandler(getCAPEM))
router.POST("/api/ca/new", apiHandler( router.POST("/api/ca/new", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) { return true, pki.GenerateCA(time.Date(2018, 01, 21, 0, 0, 0, 0, time.UTC), time.Date(2018, 01, 24, 23, 59, 59, 0, time.UTC)) })) func(_ httprouter.Params, _ []byte) (interface{}, error) {
return true, pki.GenerateCA(time.Date(2018, 01, 21, 0, 0, 0, 0, time.UTC), time.Date(2018, 01, 24, 23, 59, 59, 0, time.UTC))
}))
router.GET("/api/teams/:tid/certificates", apiHandler(teamHandler( router.GET("/api/teams/:tid/certificates", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) { return fic.GetTeamCertificates(team) }))) func(team fic.Team, _ []byte) (interface{}, error) { return fic.GetTeamCertificates(team) })))
@ -142,7 +144,6 @@ func getCertificates(_ httprouter.Params, _ []byte) (interface{}, error) {
} }
} }
type CertUploaded struct { type CertUploaded struct {
Team *int64 `json:"id_team"` Team *int64 `json:"id_team"`
} }

View File

@ -119,7 +119,6 @@ func clearClaims(_ httprouter.Params, _ []byte) (interface{}, error) {
return fic.ClearClaims() return fic.ClearClaims()
} }
func addClaimDescription(claim fic.Claim, body []byte) (interface{}, error) { func addClaimDescription(claim fic.Claim, body []byte) (interface{}, error) {
var ud fic.ClaimDescription var ud fic.ClaimDescription
if err := json.Unmarshal(body, &ud); err != nil { if err := json.Unmarshal(body, &ud); err != nil {
@ -152,7 +151,6 @@ func deleteClaim(claim fic.Claim, _ []byte) (interface{}, error) {
return claim.Delete() return claim.Delete()
} }
func getAssignees(_ httprouter.Params, _ []byte) (interface{}, error) { func getAssignees(_ httprouter.Params, _ []byte) (interface{}, error) {
return fic.GetAssignees() return fic.GetAssignees()
} }

View File

@ -1,13 +1,13 @@
package api package api
import ( import (
"encoding/json"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"strings" "strings"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
@ -40,16 +40,23 @@ func init() {
router.GET("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(showExerciceQuiz))) router.GET("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(showExerciceQuiz)))
router.DELETE("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(deleteExerciceQuiz))) router.DELETE("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(deleteExerciceQuiz)))
// Synchronize // Synchronize
router.POST("/api/sync/exercices/:eid/files", apiHandler(exerciceHandler( router.POST("/api/sync/exercices/:eid/files", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceFiles(sync.GlobalImporter, exercice), nil }))) func(exercice fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceFiles(sync.GlobalImporter, exercice), nil
})))
router.POST("/api/sync/exercices/:eid/hints", apiHandler(exerciceHandler( router.POST("/api/sync/exercices/:eid/hints", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceHints(sync.GlobalImporter, exercice), nil }))) func(exercice fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceHints(sync.GlobalImporter, exercice), nil
})))
router.POST("/api/sync/exercices/:eid/keys", apiHandler(exerciceHandler( router.POST("/api/sync/exercices/:eid/keys", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceKeys(sync.GlobalImporter, exercice), nil }))) func(exercice fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceKeys(sync.GlobalImporter, exercice), nil
})))
router.POST("/api/sync/exercices/:eid/quiz", apiHandler(exerciceHandler( router.POST("/api/sync/exercices/:eid/quiz", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceMCQ(sync.GlobalImporter, exercice), nil }))) func(exercice fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceMCQ(sync.GlobalImporter, exercice), nil
})))
router.POST("/api/sync/exercices/:eid/fixurlid", apiHandler(exerciceHandler( router.POST("/api/sync/exercices/:eid/fixurlid", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) { func(exercice fic.Exercice, _ []byte) (interface{}, error) {
@ -131,13 +138,12 @@ func createExercice(theme fic.Theme, body []byte) (interface{}, error) {
return theme.AddExercice(ue.Title, ue.URLId, ue.Path, ue.Statement, ue.Overview, depend, ue.Gain, ue.VideoURI) return theme.AddExercice(ue.Title, ue.URLId, ue.Path, ue.Statement, ue.Overview, depend, ue.Gain, ue.VideoURI)
} }
type uploadedHint struct { type uploadedHint struct {
Title string Title string
Path string Path string
Content string Content string
Cost int64 Cost int64
URI string URI string
} }
func createExerciceHint(exercice fic.Exercice, body []byte) (interface{}, error) { func createExerciceHint(exercice fic.Exercice, body []byte) (interface{}, error) {
@ -151,7 +157,7 @@ func createExerciceHint(exercice fic.Exercice, body []byte) (interface{}, error)
} else if len(uh.URI) != 0 { } else if len(uh.URI) != 0 {
return sync.ImportFile(sync.GlobalImporter, uh.URI, return sync.ImportFile(sync.GlobalImporter, uh.URI,
func(filePath string, origin string) (interface{}, error) { func(filePath string, origin string) (interface{}, error) {
return exercice.AddHint(uh.Title, "$FILES" + strings.TrimPrefix(filePath, fic.FilesDir), uh.Cost) return exercice.AddHint(uh.Title, "$FILES"+strings.TrimPrefix(filePath, fic.FilesDir), uh.Cost)
}) })
} else { } else {
return nil, errors.New("Hint's content not filled") return nil, errors.New("Hint's content not filled")
@ -185,7 +191,6 @@ func deleteExerciceHint(hint fic.EHint, _ []byte) (interface{}, error) {
return hint.Delete() return hint.Delete()
} }
type uploadedKey struct { type uploadedKey struct {
Label string Label string
Key string Key string
@ -234,7 +239,6 @@ func deleteExerciceKey(key fic.Key, _ fic.Exercice, _ []byte) (interface{}, erro
return key.Delete() return key.Delete()
} }
func showExerciceQuiz(quiz fic.MCQ, _ fic.Exercice, body []byte) (interface{}, error) { func showExerciceQuiz(quiz fic.MCQ, _ fic.Exercice, body []byte) (interface{}, error) {
return quiz, nil return quiz, nil
} }

View File

@ -14,7 +14,6 @@ import (
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
type DispatchFunction func(httprouter.Params, []byte) (interface{}, error) type DispatchFunction func(httprouter.Params, []byte) (interface{}, error)
func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) { func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) {
@ -82,8 +81,8 @@ func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, htt
} }
} }
func teamPublicHandler(f func(*fic.Team,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func teamPublicHandler(f func(*fic.Team, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []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 { if tid, err := strconv.ParseInt(string(ps.ByName("tid")), 10, 64); err != nil {
return nil, err return nil, err
} else if tid == 0 { } else if tid == 0 {
@ -96,8 +95,8 @@ func teamPublicHandler(f func(*fic.Team,[]byte) (interface{}, error)) func (http
} }
} }
func teamHandler(f func(fic.Team,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func teamHandler(f func(fic.Team, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []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 { if tid, err := strconv.ParseInt(string(ps.ByName("tid")), 10, 64); err != nil {
return nil, err return nil, err
} else if team, err := fic.GetTeam(tid); err != nil { } else if team, err := fic.GetTeam(tid); err != nil {
@ -108,8 +107,8 @@ func teamHandler(f func(fic.Team,[]byte) (interface{}, error)) func (httprouter.
} }
} }
func themeHandler(f func(fic.Theme,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func themeHandler(f func(fic.Theme, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
if thid, err := strconv.Atoi(string(ps.ByName("thid"))); err != nil { if thid, err := strconv.Atoi(string(ps.ByName("thid"))); err != nil {
return nil, err return nil, err
} else if theme, err := fic.GetTheme(thid); err != nil { } else if theme, err := fic.GetTheme(thid); err != nil {
@ -120,8 +119,8 @@ func themeHandler(f func(fic.Theme,[]byte) (interface{}, error)) func (httproute
} }
} }
func exerciceHandler(f func(fic.Exercice,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func exerciceHandler(f func(fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
if eid, err := strconv.Atoi(string(ps.ByName("eid"))); err != nil { if eid, err := strconv.Atoi(string(ps.ByName("eid"))); err != nil {
return nil, err return nil, err
} else if exercice, err := fic.GetExercice(int64(eid)); err != nil { } else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
@ -132,27 +131,27 @@ func exerciceHandler(f func(fic.Exercice,[]byte) (interface{}, error)) func (htt
} }
} }
func themedExerciceHandler(f func(fic.Theme,fic.Exercice,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { 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) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
var theme fic.Theme var theme fic.Theme
var exercice fic.Exercice var exercice fic.Exercice
themeHandler(func (th fic.Theme, _[]byte) (interface{}, error) { themeHandler(func(th fic.Theme, _ []byte) (interface{}, error) {
theme = th theme = th
return nil,nil return nil, nil
})(ps, body) })(ps, body)
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) { exerciceHandler(func(ex fic.Exercice, _ []byte) (interface{}, error) {
exercice = ex exercice = ex
return nil,nil return nil, nil
})(ps, body) })(ps, body)
return f(theme, exercice, body) return f(theme, exercice, body)
} }
} }
func hintHandler(f func(fic.EHint,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func hintHandler(f func(fic.EHint, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
if hid, err := strconv.Atoi(string(ps.ByName("hid"))); err != nil { if hid, err := strconv.Atoi(string(ps.ByName("hid"))); err != nil {
return nil, err return nil, err
} else if hint, err := fic.GetHint(int64(hid)); err != nil { } else if hint, err := fic.GetHint(int64(hid)); err != nil {
@ -163,12 +162,12 @@ func hintHandler(f func(fic.EHint,[]byte) (interface{}, error)) func (httprouter
} }
} }
func keyHandler(f func(fic.Key,fic.Exercice,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func keyHandler(f func(fic.Key, fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
var exercice fic.Exercice var exercice fic.Exercice
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) { exerciceHandler(func(ex fic.Exercice, _ []byte) (interface{}, error) {
exercice = ex exercice = ex
return nil,nil return nil, nil
})(ps, body) })(ps, body)
if kid, err := strconv.Atoi(string(ps.ByName("kid"))); err != nil { if kid, err := strconv.Atoi(string(ps.ByName("kid"))); err != nil {
@ -177,7 +176,7 @@ func keyHandler(f func(fic.Key,fic.Exercice,[]byte) (interface{}, error)) func (
return nil, err return nil, err
} else { } else {
for _, key := range keys { for _, key := range keys {
if (key.Id == int64(kid)) { if key.Id == int64(kid) {
return f(key, exercice, body) return f(key, exercice, body)
} }
} }
@ -186,12 +185,12 @@ func keyHandler(f func(fic.Key,fic.Exercice,[]byte) (interface{}, error)) func (
} }
} }
func quizHandler(f func(fic.MCQ,fic.Exercice,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { 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) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
var exercice fic.Exercice var exercice fic.Exercice
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) { exerciceHandler(func(ex fic.Exercice, _ []byte) (interface{}, error) {
exercice = ex exercice = ex
return nil,nil return nil, nil
})(ps, body) })(ps, body)
if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err != nil { if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err != nil {
@ -200,7 +199,7 @@ func quizHandler(f func(fic.MCQ,fic.Exercice,[]byte) (interface{}, error)) func
return nil, err return nil, err
} else { } else {
for _, mcq := range mcqs { for _, mcq := range mcqs {
if (mcq.Id == int64(qid)) { if mcq.Id == int64(qid) {
return f(mcq, exercice, body) return f(mcq, exercice, body)
} }
} }
@ -209,12 +208,12 @@ func quizHandler(f func(fic.MCQ,fic.Exercice,[]byte) (interface{}, error)) func
} }
} }
func exerciceFileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func exerciceFileHandler(f func(fic.EFile, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
var exercice fic.Exercice var exercice fic.Exercice
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) { exerciceHandler(func(ex fic.Exercice, _ []byte) (interface{}, error) {
exercice = ex exercice = ex
return nil,nil return nil, nil
})(ps, body) })(ps, body)
if fid, err := strconv.Atoi(string(ps.ByName("fid"))); err != nil { if fid, err := strconv.Atoi(string(ps.ByName("fid"))); err != nil {
@ -223,7 +222,7 @@ func exerciceFileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (ht
return nil, err return nil, err
} else { } else {
for _, file := range files { for _, file := range files {
if (file.Id == int64(fid)) { if file.Id == int64(fid) {
return f(file, body) return f(file, body)
} }
} }
@ -232,8 +231,8 @@ func exerciceFileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (ht
} }
} }
func eventHandler(f func(fic.Event,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func eventHandler(f func(fic.Event, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
if evid, err := strconv.Atoi(string(ps.ByName("evid"))); err != nil { if evid, err := strconv.Atoi(string(ps.ByName("evid"))); err != nil {
return nil, err return nil, err
} else if event, err := fic.GetEvent(evid); err != nil { } else if event, err := fic.GetEvent(evid); err != nil {
@ -244,8 +243,8 @@ func eventHandler(f func(fic.Event,[]byte) (interface{}, error)) func (httproute
} }
} }
func claimHandler(f func(fic.Claim,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func claimHandler(f func(fic.Claim, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
if cid, err := strconv.Atoi(string(ps.ByName("cid"))); err != nil { if cid, err := strconv.Atoi(string(ps.ByName("cid"))); err != nil {
return nil, err return nil, err
} else if claim, err := fic.GetClaim(cid); err != nil { } else if claim, err := fic.GetClaim(cid); err != nil {
@ -256,8 +255,8 @@ func claimHandler(f func(fic.Claim,[]byte) (interface{}, error)) func (httproute
} }
} }
func claimAssigneeHandler(f func(fic.ClaimAssignee,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func claimAssigneeHandler(f func(fic.ClaimAssignee, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
if aid, err := strconv.Atoi(string(ps.ByName("aid"))); err != nil { if aid, err := strconv.Atoi(string(ps.ByName("aid"))); err != nil {
return nil, err return nil, err
} else if assignee, err := fic.GetAssignee(int64(aid)); err != nil { } else if assignee, err := fic.GetAssignee(int64(aid)); err != nil {
@ -268,8 +267,8 @@ func claimAssigneeHandler(f func(fic.ClaimAssignee,[]byte) (interface{}, error))
} }
} }
func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func fileHandler(f func(fic.EFile, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
if fileid, err := strconv.Atoi(string(ps.ByName("fileid"))); err != nil { if fileid, err := strconv.Atoi(string(ps.ByName("fileid"))); err != nil {
return nil, err return nil, err
} else if file, err := fic.GetFile(fileid); err != nil { } else if file, err := fic.GetFile(fileid); err != nil {
@ -280,8 +279,8 @@ func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter
} }
} }
func certificateHandler(f func(fic.Certificate,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) { func certificateHandler(f func(fic.Certificate, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) {
if certid, err := strconv.ParseUint(string(ps.ByName("certid")), 10, 64); err != nil { if certid, err := strconv.ParseUint(string(ps.ByName("certid")), 10, 64); err != nil {
return nil, err return nil, err
} else if cert, err := fic.GetCertificate(certid); err != nil { } else if cert, err := fic.GetCertificate(certid); err != nil {

View File

@ -18,8 +18,8 @@ func init() {
} }
type FICPublicScene struct { type FICPublicScene struct {
Type string `json:"type"` Type string `json:"type"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
} }
func readPublic(path string) ([]FICPublicScene, error) { func readPublic(path string) ([]FICPublicScene, error) {

View File

@ -37,18 +37,18 @@ func getSettings(_ httprouter.Params, body []byte) (interface{}, error) {
return settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) return settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile))
} else { } else {
return settings.FICSettings{ return settings.FICSettings{
Title: "Challenge FIC", Title: "Challenge FIC",
Authors: "Laboratoire SRS, ÉPITA", Authors: "Laboratoire SRS, ÉPITA",
Start: time.Unix(0,0), Start: time.Unix(0, 0),
End: time.Unix(0,0), End: time.Unix(0, 0),
Generation: time.Unix(0,0), Generation: time.Unix(0, 0),
FirstBlood: fic.FirstBlood, FirstBlood: fic.FirstBlood,
SubmissionCostBase: fic.SubmissionCostBase, SubmissionCostBase: fic.SubmissionCostBase,
AllowRegistration: false, AllowRegistration: false,
DenyNameChange: false, DenyNameChange: false,
EnableResolutionRoute: false, EnableResolutionRoute: false,
PartialValidation: true, PartialValidation: true,
EnableExerciceDepend: true, EnableExerciceDepend: true,
}, nil }, nil
} }
} }

View File

@ -3,7 +3,6 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
@ -14,37 +13,46 @@ import (
func init() { func init() {
router.GET("/api/teams.json", apiHandler( router.GET("/api/teams.json", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) { func(httprouter.Params, []byte) (interface{}, error) {
return fic.ExportTeams() })) return fic.ExportTeams()
}))
router.GET("/api/teams-binding", apiHandler( router.GET("/api/teams-binding", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) { func(httprouter.Params, []byte) (interface{}, error) {
return bindingTeams() })) return bindingTeams()
}))
router.GET("/api/teams-nginx-members", apiHandler( router.GET("/api/teams-nginx-members", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) { func(httprouter.Params, []byte) (interface{}, error) {
return nginxGenMember() })) return nginxGenMember()
}))
router.GET("/api/teams-tries.json", apiHandler( router.GET("/api/teams-tries.json", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) { func(httprouter.Params, []byte) (interface{}, error) {
return fic.GetTries(nil, nil) })) return fic.GetTries(nil, nil)
}))
router.GET("/api/teams/", apiHandler( router.GET("/api/teams/", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) { func(httprouter.Params, []byte) (interface{}, error) {
return fic.GetTeams() })) return fic.GetTeams()
}))
router.POST("/api/teams/", apiHandler(createTeam)) router.POST("/api/teams/", apiHandler(createTeam))
router.GET("/api/teams/:tid/", apiHandler(teamHandler( router.GET("/api/teams/:tid/", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) { func(team fic.Team, _ []byte) (interface{}, error) {
return team, nil }))) return team, nil
})))
router.PUT("/api/teams/:tid/", apiHandler(teamHandler(updateTeam))) router.PUT("/api/teams/:tid/", apiHandler(teamHandler(updateTeam)))
router.POST("/api/teams/:tid/", apiHandler(teamHandler(addTeamMember))) router.POST("/api/teams/:tid/", apiHandler(teamHandler(addTeamMember)))
router.DELETE("/api/teams/:tid/", apiHandler(teamHandler( router.DELETE("/api/teams/:tid/", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) { func(team fic.Team, _ []byte) (interface{}, error) {
return team.Delete() }))) return team.Delete()
})))
router.GET("/api/teams/:tid/my.json", apiHandler(teamPublicHandler( router.GET("/api/teams/:tid/my.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) { func(team *fic.Team, _ []byte) (interface{}, error) {
return fic.MyJSONTeam(team, true) }))) return fic.MyJSONTeam(team, true)
})))
router.GET("/api/teams/:tid/wait.json", apiHandler(teamPublicHandler( router.GET("/api/teams/:tid/wait.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) { func(team *fic.Team, _ []byte) (interface{}, error) {
return fic.MyJSONTeam(team, false) }))) return fic.MyJSONTeam(team, false)
})))
router.GET("/api/teams/:tid/stats.json", apiHandler(teamPublicHandler( router.GET("/api/teams/:tid/stats.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) { func(team *fic.Team, _ []byte) (interface{}, error) {
if team != nil { if team != nil {
@ -64,14 +72,14 @@ func init() {
router.DELETE("/api/teams/:tid/history.json", apiHandler(teamPublicHandler(delHistory))) router.DELETE("/api/teams/:tid/history.json", apiHandler(teamPublicHandler(delHistory)))
router.GET("/api/teams/:tid/tries", apiHandler(teamPublicHandler( router.GET("/api/teams/:tid/tries", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) { func(team *fic.Team, _ []byte) (interface{}, error) {
return fic.GetTries(team, nil) }))) return fic.GetTries(team, nil)
})))
router.GET("/api/teams/:tid/members", apiHandler(teamHandler( router.GET("/api/teams/:tid/members", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) { func(team fic.Team, _ []byte) (interface{}, error) {
return team.GetMembers() }))) return team.GetMembers()
})))
router.POST("/api/teams/:tid/members", apiHandler(teamHandler(addTeamMember))) router.POST("/api/teams/:tid/members", apiHandler(teamHandler(addTeamMember)))
router.PUT("/api/teams/:tid/members", apiHandler(teamHandler(setTeamMember))) router.PUT("/api/teams/:tid/members", apiHandler(teamHandler(setTeamMember)))
router.GET("/api/members/:mid/team", apiHandler(dispMemberTeam))
} }
func nginxGenMember() (string, error) { func nginxGenMember() (string, error) {
@ -96,17 +104,17 @@ func nginxGenMember() (string, error) {
func bindingTeams() (string, error) { func bindingTeams() (string, error) {
if teams, err := fic.GetTeams(); err != nil { if teams, err := fic.GetTeams(); err != nil {
return "", err return "", err
} else { } else {
ret := "" ret := ""
for _, team := range teams { for _, team := range teams {
if members, err := team.GetMembers(); err != nil { if members, err := team.GetMembers(); err != nil {
return "", err return "", err
} else { } else {
var mbs []string var mbs []string
for _, member := range members { for _, member := range members {
mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname)) mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname))
} }
ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";")) ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";"))
} }
} }
return ret, nil return ret, nil
@ -176,15 +184,6 @@ func setTeamMember(team fic.Team, body []byte) (interface{}, error) {
return team.GetMembers() return team.GetMembers()
} }
func dispMemberTeam(ps httprouter.Params, body []byte) (interface{}, error) {
if mid, err := strconv.Atoi(string(ps.ByName("mid"))); err != nil {
return fic.Team{}, err
} else {
return fic.GetMember(mid)
}
}
type uploadedHistory struct { type uploadedHistory struct {
Kind string Kind string
Time time.Time Time time.Time

View File

@ -29,7 +29,6 @@ func init() {
router.PUT("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(updateExercice))) router.PUT("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(updateExercice)))
router.DELETE("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(deleteExercice))) router.DELETE("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(deleteExercice)))
router.GET("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles))) router.GET("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles)))
router.POST("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile))) router.POST("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile)))
@ -46,19 +45,33 @@ func init() {
// Synchronize // Synchronize
router.POST("/api/sync/deep", apiHandler( router.POST("/api/sync/deep", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) { return sync.SyncDeep(sync.GlobalImporter), nil })) func(_ httprouter.Params, _ []byte) (interface{}, error) {
return sync.SyncDeep(sync.GlobalImporter), nil
}))
router.POST("/api/sync/themes", apiHandler( router.POST("/api/sync/themes", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) { return sync.SyncThemes(sync.GlobalImporter), nil })) func(_ httprouter.Params, _ []byte) (interface{}, error) {
return sync.SyncThemes(sync.GlobalImporter), nil
}))
router.POST("/api/sync/themes/:thid/exercices", apiHandler(themeHandler( router.POST("/api/sync/themes/:thid/exercices", apiHandler(themeHandler(
func(theme fic.Theme, _ []byte) (interface{}, error) { return sync.SyncExercices(sync.GlobalImporter, theme), nil }))) 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( 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 }))) 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( router.POST("/api/sync/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceHints(sync.GlobalImporter, exercice), nil }))) func(exercice fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceHints(sync.GlobalImporter, exercice), nil
})))
router.POST("/api/sync/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler( router.POST("/api/sync/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceKeys(sync.GlobalImporter, exercice), nil }))) func(exercice fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceKeys(sync.GlobalImporter, exercice), nil
})))
router.POST("/api/sync/themes/:thid/exercices/:eid/quiz", apiHandler(exerciceHandler( router.POST("/api/sync/themes/:thid/exercices/:eid/quiz", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceMCQ(sync.GlobalImporter, exercice), nil }))) func(exercice fic.Exercice, _ []byte) (interface{}, error) {
return sync.SyncExerciceMCQ(sync.GlobalImporter, exercice), nil
})))
router.POST("/api/sync/themes/:thid/fixurlid", apiHandler(themeHandler( router.POST("/api/sync/themes/:thid/fixurlid", apiHandler(themeHandler(
func(theme fic.Theme, _ []byte) (interface{}, error) { func(theme fic.Theme, _ []byte) (interface{}, error) {
@ -96,9 +109,9 @@ func fixAllURLIds(_ httprouter.Params, _ []byte) (interface{}, error) {
func bindingFiles(_ httprouter.Params, body []byte) (interface{}, error) { func bindingFiles(_ httprouter.Params, body []byte) (interface{}, error) {
if files, err := fic.GetFiles(); err != nil { if files, err := fic.GetFiles(); err != nil {
return "", err return "", err
} else { } else {
ret := "" ret := ""
for _, file := range files { for _, file := range files {
ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path) ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
} }
return ret, nil return ret, nil
@ -137,7 +150,6 @@ func showThemedExercice(theme fic.Theme, exercice fic.Exercice, body []byte) (in
return exercice, nil return exercice, nil
} }
type uploadedTheme struct { type uploadedTheme struct {
Name string Name string
URLId string URLId string

2
admin/sync/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package sync provides helpers to import the challenge from various locations.
package sync

View File

@ -6,17 +6,20 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
// ExerciceHintParams holds EHint definition infomation.
type ExerciceHintParams struct { type ExerciceHintParams struct {
Filename string Filename string
Cost int64 Cost int64
Title string Title string
} }
// ExerciceParams contains values parsed from defines.txt.
type ExerciceParams struct { type ExerciceParams struct {
Gain int64 Gain int64
Hints []ExerciceHintParams `toml:"hint"` Hints []ExerciceHintParams `toml:"hint"`
} }
// parseExerciceParams reads challenge definitions from defines.txt and extract usefull data to set up the challenge.
func parseExerciceParams(i Importer, exPath string) (p ExerciceParams, err error) { func parseExerciceParams(i Importer, exPath string) (p ExerciceParams, err error) {
var defs string var defs string
defs, err = getFileContent(i, path.Join(exPath, "defines.txt")) defs, err = getFileContent(i, path.Join(exPath, "defines.txt"))

View File

@ -10,6 +10,8 @@ import (
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
// SyncExerciceFiles reads the content of files/ directory and import it as EFile for the given challenge.
// It takes care of DIGESTS.txt and ensure imported files match.
func SyncExerciceFiles(i Importer, exercice fic.Exercice) (errs []string) { func SyncExerciceFiles(i Importer, exercice fic.Exercice) (errs []string) {
// If no files directory, don't display error // If no files directory, don't display error
if ! i.exists(path.Join(exercice.Path, "files")) { if ! i.exists(path.Join(exercice.Path, "files")) {

View File

@ -15,6 +15,7 @@ import (
_ "golang.org/x/crypto/blake2b" _ "golang.org/x/crypto/blake2b"
) )
// SyncExerciceHints reads the content of hints/ directories and import it as EHint for the given challenge.
func SyncExerciceHints(i Importer, exercice fic.Exercice) (errs []string) { func SyncExerciceHints(i Importer, exercice fic.Exercice) (errs []string) {
params, err := parseExerciceParams(i, exercice.Path) params, err := parseExerciceParams(i, exercice.Path)
if err != nil { if err != nil {

View File

@ -9,6 +9,8 @@ import (
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
// isFullGraphic detects if some rune are not graphic one.
// This function is usefull to display warning when importing key ending with \r.
func isFullGraphic(s string) bool { func isFullGraphic(s string) bool {
for _, c := range s { for _, c := range s {
if !unicode.IsGraphic(c) { if !unicode.IsGraphic(c) {
@ -18,6 +20,7 @@ func isFullGraphic(s string) bool {
return true return true
} }
// SyncExerciceKeys reads the content of flags.txt and import them as Key for the given challenge.
func SyncExerciceKeys(i Importer, exercice fic.Exercice) []string { func SyncExerciceKeys(i Importer, exercice fic.Exercice) []string {
var errs []string var errs []string
@ -60,6 +63,7 @@ func SyncExerciceKeys(i Importer, exercice fic.Exercice) []string {
return errs return errs
} }
// SyncExerciceMCQ reads the content of flags-ucq.txt and flags-mcq.txt, and import them as MCQs for the given challenge.
func SyncExerciceMCQ(i Importer, exercice fic.Exercice) (errs []string) { func SyncExerciceMCQ(i Importer, exercice fic.Exercice) (errs []string) {
if _, err := exercice.WipeMCQs(); err != nil { if _, err := exercice.WipeMCQs(); err != nil {
errs = append(errs, err.Error()) errs = append(errs, err.Error())

View File

@ -16,10 +16,15 @@ import (
"golang.org/x/crypto/blake2b" "golang.org/x/crypto/blake2b"
) )
// Importer are abstract methods required to import challenges.
type Importer interface { type Importer interface {
// Kind returns information about the Importer, for human interrest.
Kind() string Kind() string
// exists checks if the given location exists from the Importer point of view.
exists(filename string) bool exists(filename string) bool
// toURL gets the full path/URL to the given file, the Importer will look internaly (used for debuging purpose).
toURL(filename string) string toURL(filename string) string
// importFile imports the file at the given URI
importFile(URI string, next func(string, string) (interface{}, error)) (interface{}, error) importFile(URI string, next func(string, string) (interface{}, error)) (interface{}, error)
getFile(filename string, writer *bufio.Writer) error getFile(filename string, writer *bufio.Writer) error
listDir(filename string) ([]string, error) listDir(filename string) ([]string, error)
@ -62,6 +67,7 @@ func getFile(i Importer, URI string, writer *bufio.Writer) error {
return errors.New(fmt.Sprintf("%q: no such file or directory", URI)) return errors.New(fmt.Sprintf("%q: no such file or directory", URI))
} }
// getFileContent
func getFileContent(i Importer, URI string) (string, error) { func getFileContent(i Importer, URI string) (string, error) {
cnt := bytes.Buffer{} cnt := bytes.Buffer{}
@ -72,11 +78,16 @@ func getFileContent(i Importer, URI string) (string, error) {
} }
} }
// getDestinationFilePath generates the destination path, from the URI.
// This function permits to obfusce to player the original URI.
// Theoricaly, changing the import method doesn't change destination URI.
func getDestinationFilePath(URI string) string { func getDestinationFilePath(URI string) string {
hash := blake2b.Sum512([]byte(URI)) hash := blake2b.Sum512([]byte(URI))
return path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:])), path.Base(URI)) return path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:])), path.Base(URI))
} }
// ImportFile imports the file at the given URI, using helpers of the given Importer.
// After import, next is called with relative path where the file has been saved and the original URI.
func ImportFile(i Importer, URI string, next func(string, string) (interface{}, error)) (interface{}, error) { func ImportFile(i Importer, URI string, next func(string, string) (interface{}, error)) (interface{}, error) {
dest := getDestinationFilePath(URI) dest := getDestinationFilePath(URI)

View File

@ -37,7 +37,7 @@ func treatSubmission(pathname string, team fic.Team, exercice_id string) {
log.Println(id, "[ERR]", err) log.Println(id, "[ERR]", err)
} else if theme, err := exercice.GetTheme(); err != nil { } else if theme, err := exercice.GetTheme(); err != nil {
log.Println(id, "[ERR]", err) log.Println(id, "[ERR]", err)
} else if s, tm, _ := team.HasSolved(exercice); s { } else if s, tm := team.HasSolved(exercice); s {
log.Printf("%s [WRN] Team %d ALREADY solved exercice %d (%s : %s)\n", id, team.Id, exercice.Id, theme.Name, exercice.Title) log.Printf("%s [WRN] Team %d ALREADY solved exercice %d (%s : %s)\n", id, team.Id, exercice.Id, theme.Name, exercice.Title)
} else { } else {
if solved, err := exercice.CheckResponse(responses.Keys, responses.MCQs, team); err != nil { if solved, err := exercice.CheckResponse(responses.Keys, responses.MCQs, team); err != nil {

View File

@ -6,6 +6,16 @@ import (
"time" "time"
) )
// Certificate represents a client certificate, which can be associated to a team.
//
// This is one method usable to handle authentication.
// To use it in nginx, you'll need to add following lines in your configuration:
//
// ssl_client_certificate PKI/shared/ca.pem;
// ssl_trusted_certificate PKI/shared/ca.pem;
// ssl_verify_client optional;
//
// Non-recognized clients will have access to a registration form.
type Certificate struct { type Certificate struct {
Id uint64 `json:"id,string"` Id uint64 `json:"id,string"`
Creation time.Time `json:"creation"` Creation time.Time `json:"creation"`
@ -14,6 +24,7 @@ type Certificate struct {
Revoked *time.Time `json:"revoked"` Revoked *time.Time `json:"revoked"`
} }
// GetCertificates returns the list of all generated certificates.
func GetCertificates() (certificates []Certificate, err error) { func GetCertificates() (certificates []Certificate, err error) {
var rows *sql.Rows var rows *sql.Rows
if rows, err = DBQuery("SELECT id_cert, creation, password, id_team, revoked FROM certificates ORDER BY creation"); err == nil { if rows, err = DBQuery("SELECT id_cert, creation, password, id_team, revoked FROM certificates ORDER BY creation"); err == nil {
@ -32,6 +43,8 @@ func GetCertificates() (certificates []Certificate, err error) {
return return
} }
// GetTeamCertificates returns all certificates generated for a given Team.
func GetTeamCertificates(team Team) (certificates []Certificate, err error) { func GetTeamCertificates(team Team) (certificates []Certificate, err error) {
var rows *sql.Rows var rows *sql.Rows
if rows, err = DBQuery("SELECT id_cert, creation, password, id_team, revoked FROM certificates WHERE id_team = ? ORDER BY creation", team.Id); err == nil { if rows, err = DBQuery("SELECT id_cert, creation, password, id_team, revoked FROM certificates WHERE id_team = ? ORDER BY creation", team.Id); err == nil {
@ -50,12 +63,14 @@ func GetTeamCertificates(team Team) (certificates []Certificate, err error) {
return return
} }
// GetCertificate retrieves a certificate from its serial number.
func GetCertificate(serial uint64) (c Certificate, err error) { func GetCertificate(serial uint64) (c Certificate, err error) {
err = DBQueryRow("SELECT id_cert, creation, password, id_team, revoked FROM certificates WHERE id_cert = ?", serial).Scan(&c.Id, &c.Creation, &c.Password, &c.IdTeam, &c.Revoked) err = DBQueryRow("SELECT id_cert, creation, password, id_team, revoked FROM certificates WHERE id_cert = ?", serial).Scan(&c.Id, &c.Creation, &c.Password, &c.IdTeam, &c.Revoked)
return return
} }
func ExistingCertSerial(serial [8]byte) (bool) { // ExistingCertSerial tells you if the given bytes correspond to a know certificate.
func ExistingCertSerial(serial [8]byte) bool {
var m big.Int var m big.Int
m.SetBytes(serial[:]) m.SetBytes(serial[:])
@ -63,6 +78,10 @@ func ExistingCertSerial(serial [8]byte) (bool) {
return c.Id > 0 return c.Id > 0
} }
// RegisterCertificate registers a certificate in the database.
//
// "serial" is the certificate serial number
// "password" is the one used to crypt privatekey and .p12
func RegisterCertificate(serial uint64, password string) (Certificate, error) { func RegisterCertificate(serial uint64, password string) (Certificate, error) {
now := time.Now() now := time.Now()
if _, err := DBExec("INSERT INTO certificates (id_cert, creation, password) VALUES (?, ?, ?)", serial, now, password); err != nil { if _, err := DBExec("INSERT INTO certificates (id_cert, creation, password) VALUES (?, ?, ?)", serial, now, password); err != nil {
@ -72,6 +91,7 @@ func RegisterCertificate(serial uint64, password string) (Certificate, error) {
} }
} }
// Update applies modifications back to the database.
func (c Certificate) Update() (int64, error) { func (c Certificate) Update() (int64, error) {
if res, err := DBExec("UPDATE certificates SET creation = ?, password = ?, id_team = ?, revoked = ? WHERE id_cert = ?", c.Creation, c.Password, c.IdTeam, c.Revoked, c.Id); err != nil { if res, err := DBExec("UPDATE certificates SET creation = ?, password = ?, id_team = ?, revoked = ? WHERE id_cert = ?", c.Creation, c.Password, c.IdTeam, c.Revoked, c.Id); err != nil {
return 0, err return 0, err
@ -82,12 +102,14 @@ func (c Certificate) Update() (int64, error) {
} }
} }
// Revoke the certificate in database.
func (c *Certificate) Revoke() (int64, error) { func (c *Certificate) Revoke() (int64, error) {
now := time.Now() now := time.Now()
c.Revoked = &now c.Revoked = &now
return c.Update() return c.Update()
} }
// Delete the certificate entry in the database.
func (c Certificate) Delete() (int64, error) { func (c Certificate) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM certificates WHERE id_cert = ?", c.Id); err != nil { if res, err := DBExec("DELETE FROM certificates WHERE id_cert = ?", c.Id); err != nil {
return 0, err return 0, err
@ -98,6 +120,8 @@ func (c Certificate) Delete() (int64, error) {
} }
} }
// ClearCertificates removes all certificates from database.
func ClearCertificates() (int64, error) { func ClearCertificates() (int64, error) {
if res, err := DBExec("DELETE FROM certificates"); err != nil { if res, err := DBExec("DELETE FROM certificates"); err != nil {
return 0, err return 0, err

View File

@ -8,8 +8,10 @@ import (
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
) )
// db stores the connection to the database
var db *sql.DB var db *sql.DB
// DSNGenerator returns DSN filed with values from environment
func DSNGenerator() string { func DSNGenerator() string {
db_user := "fic" db_user := "fic"
db_password := "fic" db_password := "fic"
@ -35,6 +37,7 @@ func DSNGenerator() string {
return db_user + ":" + db_password + "@" + db_host + "/" + db_db return db_user + ":" + db_password + "@" + db_host + "/" + db_db
} }
// DBInit establishes the connection to the database
func DBInit(dsn string) (err error) { func DBInit(dsn string) (err error) {
if db, err = sql.Open("mysql", dsn + "?parseTime=true&foreign_key_checks=1"); err != nil { if db, err = sql.Open("mysql", dsn + "?parseTime=true&foreign_key_checks=1"); err != nil {
return return
@ -51,6 +54,7 @@ func DBInit(dsn string) (err error) {
return return
} }
// DBCreate creates all necessary tables used by the package
func DBCreate() error { func DBCreate() error {
if _, err := db.Exec(` if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS events( CREATE TABLE IF NOT EXISTS events(
@ -284,6 +288,7 @@ CREATE TABLE IF NOT EXISTS claim_descriptions(
return nil return nil
} }
// DBClose closes the connection to the database
func DBClose() error { func DBClose() error {
return db.Close() return db.Close()
} }

3
libfic/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package fic provides access to common structures shared between multiple
// services required to run the fic-server project.
package fic

View File

@ -4,6 +4,7 @@ import (
"time" "time"
) )
// Event represents a challenge event.
type Event struct { type Event struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
@ -11,6 +12,7 @@ type Event struct {
Time time.Time `json:"time"` Time time.Time `json:"time"`
} }
// GetLastEvents returns the list of the last 10 events, sorted by date, last first
func GetLastEvents() ([]Event, error) { func GetLastEvents() ([]Event, error) {
if rows, err := DBQuery("SELECT id_event, txt, kind, time FROM events ORDER BY time DESC LIMIT 10"); err != nil { if rows, err := DBQuery("SELECT id_event, txt, kind, time FROM events ORDER BY time DESC LIMIT 10"); err != nil {
return nil, err return nil, err
@ -33,6 +35,7 @@ func GetLastEvents() ([]Event, error) {
} }
} }
// GetEvents returns the list of all events, sorted by date, last first
func GetEvents() ([]Event, error) { func GetEvents() ([]Event, error) {
if rows, err := DBQuery("SELECT id_event, txt, kind, time FROM events ORDER BY time DESC"); err != nil { if rows, err := DBQuery("SELECT id_event, txt, kind, time FROM events ORDER BY time DESC"); err != nil {
return nil, err return nil, err
@ -55,6 +58,7 @@ func GetEvents() ([]Event, error) {
} }
} }
// GetEvent retrieves the event with the given id
func GetEvent(id int) (Event, error) { func GetEvent(id int) (Event, error) {
var e Event var e Event
if err := DBQueryRow("SELECT id_event, txt, kind, time FROM events WHERE id_event=?", id).Scan(&e.Id, &e.Text, &e.Kind, &e.Time); err != nil { if err := DBQueryRow("SELECT id_event, txt, kind, time FROM events WHERE id_event=?", id).Scan(&e.Id, &e.Text, &e.Kind, &e.Time); err != nil {
@ -64,6 +68,7 @@ func GetEvent(id int) (Event, error) {
return e, nil return e, nil
} }
// NewEvent creates a new event in the database and returns the corresponding structure
func NewEvent(txt string, kind string) (Event, error) { func NewEvent(txt string, kind string) (Event, error) {
if res, err := DBExec("INSERT INTO events (txt, kind, time) VALUES (?, ?, ?)", txt, kind, time.Now()); err != nil { if res, err := DBExec("INSERT INTO events (txt, kind, time) VALUES (?, ?, ?)", txt, kind, time.Now()); err != nil {
return Event{}, err return Event{}, err
@ -74,6 +79,7 @@ func NewEvent(txt string, kind string) (Event, error) {
} }
} }
// Update applies modifications back to the database
func (e Event) Update() (int64, error) { func (e Event) Update() (int64, error) {
if res, err := DBExec("UPDATE events SET txt = ?, kind = ?, time = ? WHERE id_event = ?", e.Text, e.Kind, e.Time, e.Id); err != nil { if res, err := DBExec("UPDATE events SET txt = ?, kind = ?, time = ? WHERE id_event = ?", e.Text, e.Kind, e.Time, e.Id); err != nil {
return 0, err return 0, err
@ -84,6 +90,7 @@ func (e Event) Update() (int64, error) {
} }
} }
// Delete the event from the database
func (e Event) Delete() (int64, error) { func (e Event) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM events WHERE id_event = ?", e.Id); err != nil { if res, err := DBExec("DELETE FROM events WHERE id_event = ?", e.Id); err != nil {
return 0, err return 0, err
@ -94,6 +101,7 @@ func (e Event) Delete() (int64, error) {
} }
} }
// ClearEvents removes all events from database
func ClearEvents() (int64, error) { func ClearEvents() (int64, error) {
if res, err := DBExec("DELETE FROM events"); err != nil { if res, err := DBExec("DELETE FROM events"); err != nil {
return 0, err return 0, err

View File

@ -5,22 +5,40 @@ import (
"time" "time"
) )
// PartialValidation allows a Team to validate an Exercice at a given point if
// it has ever found all classical flags, even if the last validation is not
// complete or contains mistakes on previously validated flags.
var PartialValidation bool var PartialValidation bool
// PartialMCQValidation allows a Team to validate each MCQ independently.
// Otherwise, all MCQ has to be correct for being validated.
var PartialMCQValidation bool var PartialMCQValidation bool
// Exercice represents a challenge inside a Theme.
type Exercice struct { type Exercice struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Title string `json:"title"` Title string `json:"title"`
// URLid is used to reference the challenge from the URL path
URLId string `json:"urlid"` URLId string `json:"urlid"`
// Path is the relative import location where find challenge data
Path string `json:"path"` Path string `json:"path"`
// Statement is the challenge description shown to players
Statement string `json:"statement"` Statement string `json:"statement"`
// Overview is the challenge description shown to public
Overview string `json:"overview"` Overview string `json:"overview"`
Depend *int64 `json:"depend"` Depend *int64 `json:"depend"`
// Gain is the basis amount of points player will earn if it solve the challenge
// If this value fluctuate during the challenge, players' scores will follow changes.
// Use Coefficient value to create temporary bonus/malus.
Gain int64 `json:"gain"` Gain int64 `json:"gain"`
// Coefficient is a temporary bonus/malus applied on Gain when the challenge is solved.
// This value is saved along with solved informations: changing it doesn't affect Team that already solved the challenge.
Coefficient float64 `json:"coefficient"` Coefficient float64 `json:"coefficient"`
// VideoURI is the link to the resolution video
VideoURI string `json:"videoURI"` VideoURI string `json:"videoURI"`
} }
// GetExercice retrieves the challenge with the given id.
func GetExercice(id int64) (Exercice, error) { func GetExercice(id int64) (Exercice, error) {
var e Exercice var e Exercice
if err := DBQueryRow("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_exercice = ?", id).Scan(&e.Id, &e.Title, &e.URLId, &e.Path, &e.Statement, &e.Overview, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil { if err := DBQueryRow("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_exercice = ?", id).Scan(&e.Id, &e.Title, &e.URLId, &e.Path, &e.Statement, &e.Overview, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil {
@ -30,6 +48,7 @@ func GetExercice(id int64) (Exercice, error) {
return e, nil return e, nil
} }
// GetExercice retrieves the challenge with the given id.
func (t Theme) GetExercice(id int) (Exercice, error) { func (t Theme) GetExercice(id int) (Exercice, error) {
var e Exercice var e Exercice
if err := DBQueryRow("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_theme = ? AND id_exercice = ?", t.Id, id).Scan(&e.Id, &e.Title, &e.URLId, &e.Path, &e.Statement, &e.Overview, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil { if err := DBQueryRow("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_theme = ? AND id_exercice = ?", t.Id, id).Scan(&e.Id, &e.Title, &e.URLId, &e.Path, &e.Statement, &e.Overview, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil {
@ -39,6 +58,7 @@ func (t Theme) GetExercice(id int) (Exercice, error) {
return e, nil return e, nil
} }
// GetExerciceByTitle retrieves the challenge with the given title.
func (t Theme) GetExerciceByTitle(title string) (Exercice, error) { func (t Theme) GetExerciceByTitle(title string) (Exercice, error) {
var e Exercice var e Exercice
if err := DBQueryRow("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_theme = ? AND title = ?", t.Id, title).Scan(&e.Id, &e.Title, &t.URLId, &e.Path, &e.Statement, &e.Overview, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil { if err := DBQueryRow("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_theme = ? AND title = ?", t.Id, title).Scan(&e.Id, &e.Title, &t.URLId, &e.Path, &e.Statement, &e.Overview, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil {
@ -48,6 +68,7 @@ func (t Theme) GetExerciceByTitle(title string) (Exercice, error) {
return e, nil return e, nil
} }
// GetExercices returns the list of all challenges present in the database.
func GetExercices() ([]Exercice, error) { func GetExercices() ([]Exercice, error) {
if rows, err := DBQuery("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices"); err != nil { if rows, err := DBQuery("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices"); err != nil {
return nil, err return nil, err
@ -70,6 +91,7 @@ func GetExercices() ([]Exercice, error) {
} }
} }
// GetExercices returns the list of all challenges in the Theme.
func (t Theme) GetExercices() ([]Exercice, error) { func (t Theme) GetExercices() ([]Exercice, error) {
if rows, err := DBQuery("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_theme = ?", t.Id); err != nil { if rows, err := DBQuery("SELECT id_exercice, title, url_id, path, statement, overview, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_theme = ?", t.Id); err != nil {
return nil, err return nil, err
@ -92,6 +114,7 @@ func (t Theme) GetExercices() ([]Exercice, error) {
} }
} }
// AddExercice creates and fills a new struct Exercice and registers it into the database.
func (t Theme) AddExercice(title string, urlId string, path string, statement string, overview string, depend *Exercice, gain int64, videoURI string) (Exercice, error) { func (t Theme) AddExercice(title string, urlId string, path string, statement string, overview string, depend *Exercice, gain int64, videoURI string) (Exercice, error) {
var dpd interface{} var dpd interface{}
if depend == nil { if depend == nil {
@ -112,6 +135,7 @@ func (t Theme) AddExercice(title string, urlId string, path string, statement st
} }
} }
// Update applies modifications back to the database.
func (e Exercice) Update() (int64, error) { func (e Exercice) Update() (int64, error) {
if res, err := DBExec("UPDATE exercices SET title = ?, url_id = ?, path = ?, statement = ?, overview = ?, depend = ?, gain = ?, coefficient_cur = ?, video_uri = ? WHERE id_exercice = ?", e.Title, e.URLId, e.Path, e.Statement, e.Overview, e.Depend, e.Gain, e.Coefficient, e.VideoURI, e.Id); err != nil { if res, err := DBExec("UPDATE exercices SET title = ?, url_id = ?, path = ?, statement = ?, overview = ?, depend = ?, gain = ?, coefficient_cur = ?, video_uri = ? WHERE id_exercice = ?", e.Title, e.URLId, e.Path, e.Statement, e.Overview, e.Depend, e.Gain, e.Coefficient, e.VideoURI, e.Id); err != nil {
return 0, err return 0, err
@ -122,6 +146,8 @@ func (e Exercice) Update() (int64, error) {
} }
} }
// FixURLId generates a valid URLid from the challenge Title.
// It returns true if something has been generated.
func (e *Exercice) FixURLId() bool { func (e *Exercice) FixURLId() bool {
if e.URLId == "" { if e.URLId == "" {
e.URLId = ToURLid(e.Title) e.URLId = ToURLid(e.Title)
@ -130,6 +156,7 @@ func (e *Exercice) FixURLId() bool {
return false return false
} }
// GetThemeId returns the theme's id for the Exercice.
func (e Exercice) GetThemeId() (int, error) { func (e Exercice) GetThemeId() (int, error) {
var tid int var tid int
if err := DBQueryRow("SELECT id_theme FROM exercices WHERE id_exercice=?", e.Id).Scan(&tid); err != nil { if err := DBQueryRow("SELECT id_theme FROM exercices WHERE id_exercice=?", e.Id).Scan(&tid); err != nil {
@ -138,6 +165,7 @@ func (e Exercice) GetThemeId() (int, error) {
return tid, nil return tid, nil
} }
// GetTheme returns the parent Theme where the Exercice lives.
func (e Exercice) GetTheme() (Theme, error) { func (e Exercice) GetTheme() (Theme, error) {
if tid, err := e.GetThemeId(); err != nil { if tid, err := e.GetThemeId(); err != nil {
return Theme{}, err return Theme{}, err
@ -146,6 +174,7 @@ func (e Exercice) GetTheme() (Theme, error) {
} }
} }
// Delete the challenge from the database.
func (e Exercice) Delete() (int64, error) { func (e Exercice) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM exercices WHERE id_exercice = ?", e.Id); err != nil { if res, err := DBExec("DELETE FROM exercices WHERE id_exercice = ?", e.Id); err != nil {
return 0, err return 0, err
@ -156,6 +185,7 @@ func (e Exercice) Delete() (int64, error) {
} }
} }
// GetLevel returns the number of dependancy challenges.
func (e Exercice) GetLevel() (int, error) { func (e Exercice) GetLevel() (int, error) {
dep := e.Depend dep := e.Depend
nb := 1 nb := 1
@ -170,6 +200,7 @@ func (e Exercice) GetLevel() (int, error) {
return nb, nil return nb, nil
} }
// NewTry registers a solving attempt for the given Team.
func (e Exercice) NewTry(t Team) error { func (e Exercice) NewTry(t Team) error {
if _, err := DBExec("INSERT INTO exercice_tries (id_exercice, id_team, time) VALUES (?, ?, ?)", e.Id, t.Id, time.Now()); err != nil { if _, err := DBExec("INSERT INTO exercice_tries (id_exercice, id_team, time) VALUES (?, ?, ?)", e.Id, t.Id, time.Now()); err != nil {
return err return err
@ -178,6 +209,8 @@ func (e Exercice) NewTry(t Team) error {
} }
} }
// UpdateTry applies modifications to the latest try registered for the given Team.
// Updated values are time and the given nbdiff.
func (e Exercice) UpdateTry(t Team, nbdiff int) error { func (e Exercice) UpdateTry(t Team, nbdiff int) error {
if _, err := DBExec("UPDATE exercice_tries SET nbdiff = ?, time = ? WHERE id_exercice = ? AND id_team = ? ORDER BY time DESC LIMIT 1", nbdiff, time.Now(), e.Id, t.Id); err != nil { if _, err := DBExec("UPDATE exercice_tries SET nbdiff = ?, time = ? WHERE id_exercice = ? AND id_team = ? ORDER BY time DESC LIMIT 1", nbdiff, time.Now(), e.Id, t.Id); err != nil {
return err return err
@ -186,6 +219,7 @@ func (e Exercice) UpdateTry(t Team, nbdiff int) error {
} }
} }
// Solved registers that the given Team solves the challenge.
func (e Exercice) Solved(t Team) error { func (e Exercice) Solved(t Team) error {
if _, err := DBExec("INSERT INTO exercice_solved (id_exercice, id_team, time, coefficient) VALUES (?, ?, ?, ?)", e.Id, t.Id, time.Now(), e.Coefficient); err != nil { if _, err := DBExec("INSERT INTO exercice_solved (id_exercice, id_team, time, coefficient) VALUES (?, ?, ?, ?)", e.Id, t.Id, time.Now(), e.Coefficient); err != nil {
return err return err
@ -194,6 +228,7 @@ func (e Exercice) Solved(t Team) error {
} }
} }
// SolvedCount returns the number of Team that already have solved the challenge.
func (e Exercice) SolvedCount() int64 { func (e Exercice) SolvedCount() int64 {
var nb int64 var nb int64
if err := DBQueryRow("SELECT COUNT(id_exercice) FROM exercice_solved WHERE id_exercice = ?", e.Id).Scan(&nb); err != nil { if err := DBQueryRow("SELECT COUNT(id_exercice) FROM exercice_solved WHERE id_exercice = ?", e.Id).Scan(&nb); err != nil {
@ -203,6 +238,7 @@ func (e Exercice) SolvedCount() int64 {
} }
} }
// TriedTeamCount returns the number of Team that attempted to solve the exercice.
func (e Exercice) TriedTeamCount() int64 { func (e Exercice) TriedTeamCount() int64 {
var nb int64 var nb int64
if err := DBQueryRow("SELECT COUNT(DISTINCT id_team) FROM exercice_tries WHERE id_exercice = ?", e.Id).Scan(&nb); err != nil { if err := DBQueryRow("SELECT COUNT(DISTINCT id_team) FROM exercice_tries WHERE id_exercice = ?", e.Id).Scan(&nb); err != nil {
@ -212,6 +248,7 @@ func (e Exercice) TriedTeamCount() int64 {
} }
} }
// TriedCount returns the number of cumulative attempts, all Team combined, for the exercice.
func (e Exercice) TriedCount() int64 { func (e Exercice) TriedCount() int64 {
var nb int64 var nb int64
if err := DBQueryRow("SELECT COUNT(id_team) FROM exercice_tries WHERE id_exercice = ?", e.Id).Scan(&nb); err != nil { if err := DBQueryRow("SELECT COUNT(id_team) FROM exercice_tries WHERE id_exercice = ?", e.Id).Scan(&nb); err != nil {
@ -221,6 +258,8 @@ func (e Exercice) TriedCount() int64 {
} }
} }
// CheckResponse, given both flags and MCQ responses, figures out if thoses are correct (or if they are previously solved).
// In the meanwhile, CheckResponse registers good answers given (but it does not mark the challenge as solved at the end).
func (e Exercice) CheckResponse(respkeys map[string]string, respmcq map[int64]bool, t Team) (bool, error) { func (e Exercice) CheckResponse(respkeys map[string]string, respmcq map[int64]bool, t Team) (bool, error) {
if err := e.NewTry(t); err != nil { if err := e.NewTry(t); err != nil {
return false, err return false, err
@ -273,3 +312,14 @@ func (e Exercice) CheckResponse(respkeys map[string]string, respmcq map[int64]bo
return valid, nil return valid, nil
} }
} }
// IsSolved returns the number of time this challenge has been solved and the time of the first solve occurence.
func (e Exercice) IsSolved() (int, time.Time) {
var nb *int
var tm *time.Time
if DBQueryRow("SELECT COUNT(id_exercice), MIN(time) FROM exercice_solved WHERE id_exercice = ?", e.Id).Scan(&nb, &tm); nb == nil || tm == nil {
return 0, time.Time{}
} else {
return *nb, *tm
}
}

View File

@ -14,20 +14,33 @@ import (
_ "golang.org/x/crypto/blake2b" _ "golang.org/x/crypto/blake2b"
) )
// FilesDir stores the location where files to be served are stored.
var FilesDir string = "./FILES/" var FilesDir string = "./FILES/"
// OptionalDigest permits to avoid importation failure if no digest are given.
var OptionalDigest bool = false var OptionalDigest bool = false
// StrongDigest forces the use of BLAKE2b hash in place of SHA1 (or mixed SHA1/BLAKE2b).
var StrongDigest bool = false var StrongDigest bool = false
// EFile represents a challenge file.
type EFile struct { type EFile struct {
Id int64 `json:"id"` Id int64 `json:"id"`
// origin holds the import relative path of the file
origin string origin string
// Path is the location where the file is stored, relatively to FilesDir
Path string `json:"path"` Path string `json:"path"`
// IdExercice is the identifier of the underlying challenge
IdExercice int64 `json:"idExercice"` IdExercice int64 `json:"idExercice"`
// Name is the title displayed to players
Name string `json:"name"` Name string `json:"name"`
// Checksum stores the cached hash of the file
Checksum []byte `json:"checksum"` Checksum []byte `json:"checksum"`
// Size contains the cached size of the file
Size int64 `json:"size"` Size int64 `json:"size"`
} }
// GetFiles returns a list of all files living in the database.
func GetFiles() ([]EFile, error) { func GetFiles() ([]EFile, error) {
if rows, err := DBQuery("SELECT id_file, id_exercice, origin, path, name, cksum, size FROM exercice_files"); err != nil { if rows, err := DBQuery("SELECT id_file, id_exercice, origin, path, name, cksum, size FROM exercice_files"); err != nil {
return nil, err return nil, err
@ -50,11 +63,13 @@ func GetFiles() ([]EFile, error) {
} }
} }
// GetFile retrieves the file with the given id.
func GetFile(id int) (f EFile, err error) { func GetFile(id int) (f EFile, err error) {
err = DBQueryRow("SELECT id_file, origin, path, name, cksum, size FROM exercice_files WHERE id_file = ?", id).Scan(&f.Id, &f.origin, &f.Path, &f.Name, &f.Checksum, &f.Size) err = DBQueryRow("SELECT id_file, origin, path, name, cksum, size FROM exercice_files WHERE id_file = ?", id).Scan(&f.Id, &f.origin, &f.Path, &f.Name, &f.Checksum, &f.Size)
return f, err return f, err
} }
// GetFileByPath retrieves the file that should be found at the given location.
func GetFileByPath(path string) (EFile, error) { func GetFileByPath(path string) (EFile, error) {
path = strings.TrimPrefix(path, FilesDir) path = strings.TrimPrefix(path, FilesDir)
@ -66,6 +81,7 @@ func GetFileByPath(path string) (EFile, error) {
return f, nil return f, nil
} }
// GetFiles returns a list of files coming with the challenge.
func (e Exercice) GetFiles() ([]EFile, error) { func (e Exercice) GetFiles() ([]EFile, error) {
if rows, err := DBQuery("SELECT id_file, origin, path, name, cksum, size FROM exercice_files WHERE id_exercice = ?", e.Id); err != nil { if rows, err := DBQuery("SELECT id_file, origin, path, name, cksum, size FROM exercice_files WHERE id_exercice = ?", e.Id); err != nil {
return nil, err return nil, err
@ -89,6 +105,7 @@ func (e Exercice) GetFiles() ([]EFile, error) {
} }
} }
// GetFileByPath retrieves the file that should be found at the given location, limited to the challenge files.
func (e Exercice) GetFileByPath(path string) (EFile, error) { func (e Exercice) GetFileByPath(path string) (EFile, error) {
path = strings.TrimPrefix(path, FilesDir) path = strings.TrimPrefix(path, FilesDir)
@ -100,6 +117,8 @@ func (e Exercice) GetFileByPath(path string) (EFile, error) {
return f, nil return f, nil
} }
// checkFileHash checks if the file at the given filePath has the given digest.
// It also returns the file's size.
func checkFileHash(filePath string, digest []byte) ([]byte, int64, error) { func checkFileHash(filePath string, digest []byte) ([]byte, int64, error) {
if digest == nil { if digest == nil {
return []byte{}, 0, errors.New("No digest given.") return []byte{}, 0, errors.New("No digest given.")
@ -145,6 +164,7 @@ func checkFileHash(filePath string, digest []byte) ([]byte, int64, error) {
} }
} }
// ImportFile registers (ou updates if it already exists in database) the file in database.
func (e Exercice) ImportFile(filePath string, origin string, digest []byte) (interface{}, error) { func (e Exercice) ImportFile(filePath string, origin string, digest []byte) (interface{}, error) {
if result512, size, err := checkFileHash(filePath, digest); !OptionalDigest && err != nil { if result512, size, err := checkFileHash(filePath, digest); !OptionalDigest && err != nil {
return EFile{}, err return EFile{}, err
@ -169,6 +189,7 @@ func (e Exercice) ImportFile(filePath string, origin string, digest []byte) (int
} }
} }
// AddFile creates and fills a new struct File and registers it into the database.
func (e Exercice) AddFile(path string, origin string, name string, checksum []byte, size int64) (EFile, error) { func (e Exercice) AddFile(path string, origin string, name string, checksum []byte, size int64) (EFile, error) {
if res, err := DBExec("INSERT INTO exercice_files (id_exercice, origin, path, name, cksum, size) VALUES (?, ?, ?, ?, ?, ?)", e.Id, origin, path, name, checksum, size); err != nil { if res, err := DBExec("INSERT INTO exercice_files (id_exercice, origin, path, name, cksum, size) VALUES (?, ?, ?, ?, ?, ?)", e.Id, origin, path, name, checksum, size); err != nil {
return EFile{}, err return EFile{}, err
@ -179,6 +200,7 @@ func (e Exercice) AddFile(path string, origin string, name string, checksum []by
} }
} }
// Update applies modifications back to the database.
func (f EFile) Update() (int64, error) { func (f EFile) Update() (int64, error) {
if res, err := DBExec("UPDATE exercice_files SET id_exercice = ?, origin = ?, path = ?, name = ?, cksum = ?, size = ? WHERE id_file = ?", f.IdExercice, f.origin, f.Path, f.Name, f.Checksum, f.Size, f.Id); err != nil { if res, err := DBExec("UPDATE exercice_files SET id_exercice = ?, origin = ?, path = ?, name = ?, cksum = ?, size = ? WHERE id_file = ?", f.IdExercice, f.origin, f.Path, f.Name, f.Checksum, f.Size, f.Id); err != nil {
return 0, err return 0, err
@ -189,6 +211,7 @@ func (f EFile) Update() (int64, error) {
} }
} }
// Delete the file from the database.
func (f EFile) Delete() (int64, error) { func (f EFile) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_files WHERE id_file = ?", f.Id); err != nil { if res, err := DBExec("DELETE FROM exercice_files WHERE id_file = ?", f.Id); err != nil {
return 0, err return 0, err
@ -199,6 +222,7 @@ func (f EFile) Delete() (int64, error) {
} }
} }
// WipeFiles deletes (only in the database, not on disk) files coming with the challenge.
func (e Exercice) WipeFiles() (int64, error) { func (e Exercice) WipeFiles() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_files WHERE id_exercice = ?", e.Id); err != nil { if res, err := DBExec("DELETE FROM exercice_files WHERE id_exercice = ?", e.Id); err != nil {
return 0, err return 0, err
@ -209,6 +233,7 @@ func (e Exercice) WipeFiles() (int64, error) {
} }
} }
// ClearFiles removes all certificates from database (but not files on disks).
func ClearFiles() (int64, error) { func ClearFiles() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_files"); err != nil { if res, err := DBExec("DELETE FROM exercice_files"); err != nil {
return 0, err return 0, err
@ -219,6 +244,7 @@ func ClearFiles() (int64, error) {
} }
} }
// GetOrigin access the private field origin of the file.
func (f EFile) GetOrigin() string { func (f EFile) GetOrigin() string {
return f.origin return f.origin
} }

View File

@ -5,15 +5,23 @@ import (
"strings" "strings"
) )
// EHint represents a challenge hint.
type EHint struct { type EHint struct {
Id int64 `json:"id"` Id int64 `json:"id"`
// IdExercice is the identifier of the underlying challenge
IdExercice int64 `json:"idExercice"` IdExercice int64 `json:"idExercice"`
// Title is the hint name displayed to players
Title string `json:"title"` Title string `json:"title"`
// Content is the actual content of small text hints (mutually exclusive with File field)
// When File is filled, Content contains the hexadecimal file's hash.
Content string `json:"content"` Content string `json:"content"`
// File is path, relative to FilesDir where the file hint is stored (mutually exclusive with Content field)
File string `json:"file"` File string `json:"file"`
// Cost is the amount of points the player will loose if it unlocks the hint
Cost int64 `json:"cost"` Cost int64 `json:"cost"`
} }
// treatHintContent reads Content to detect if this is a hint file in order to convert to such hint.
func treatHintContent(h *EHint) { func treatHintContent(h *EHint) {
if strings.HasPrefix(h.Content, "$FILES") { if strings.HasPrefix(h.Content, "$FILES") {
fpath := strings.TrimPrefix(h.Content, "$FILES") fpath := strings.TrimPrefix(h.Content, "$FILES")
@ -23,6 +31,7 @@ func treatHintContent(h *EHint) {
} }
} }
// GetHint retrieves the hint with the given id.
func GetHint(id int64) (EHint, error) { func GetHint(id int64) (EHint, error) {
var h EHint var h EHint
if err := DBQueryRow("SELECT id_hint, id_exercice, title, content, cost FROM exercice_hints WHERE id_hint = ?", id).Scan(&h.Id, &h.IdExercice, &h.Title, &h.Content, &h.Cost); err != nil { if err := DBQueryRow("SELECT id_hint, id_exercice, title, content, cost FROM exercice_hints WHERE id_hint = ?", id).Scan(&h.Id, &h.IdExercice, &h.Title, &h.Content, &h.Cost); err != nil {
@ -33,6 +42,7 @@ func GetHint(id int64) (EHint, error) {
return h, nil return h, nil
} }
// GetHints returns a list of hints comming with the challenge.
func (e Exercice) GetHints() ([]EHint, error) { func (e Exercice) GetHints() ([]EHint, error) {
if rows, err := DBQuery("SELECT id_hint, title, content, cost FROM exercice_hints WHERE id_exercice = ?", e.Id); err != nil { if rows, err := DBQuery("SELECT id_hint, title, content, cost FROM exercice_hints WHERE id_exercice = ?", e.Id); err != nil {
return nil, err return nil, err
@ -57,6 +67,7 @@ func (e Exercice) GetHints() ([]EHint, error) {
} }
} }
// AddHint creates and fills a new struct EHint and registers it into the database.
func (e Exercice) AddHint(title string, content string, cost int64) (EHint, error) { func (e Exercice) AddHint(title string, content string, cost int64) (EHint, error) {
if res, err := DBExec("INSERT INTO exercice_hints (id_exercice, title, content, cost) VALUES (?, ?, ?, ?)", e.Id, title, content, cost); err != nil { if res, err := DBExec("INSERT INTO exercice_hints (id_exercice, title, content, cost) VALUES (?, ?, ?, ?)", e.Id, title, content, cost); err != nil {
return EHint{}, err return EHint{}, err
@ -67,6 +78,7 @@ func (e Exercice) AddHint(title string, content string, cost int64) (EHint, erro
} }
} }
// Update applies modifications back to the database.
func (h EHint) Update() (int64, error) { func (h EHint) Update() (int64, error) {
if res, err := DBExec("UPDATE exercice_hints SET id_exercice = ?, title = ?, content = ?, cost = ? WHERE id_hint = ?", h.IdExercice, h.Title, h.Content, h.Cost, h.Id); err != nil { if res, err := DBExec("UPDATE exercice_hints SET id_exercice = ?, title = ?, content = ?, cost = ? WHERE id_hint = ?", h.IdExercice, h.Title, h.Content, h.Cost, h.Id); err != nil {
return 0, err return 0, err
@ -77,6 +89,7 @@ func (h EHint) Update() (int64, error) {
} }
} }
// Delete the hint from the database.
func (h EHint) Delete() (int64, error) { func (h EHint) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_hints WHERE id_hint = ?", h.Id); err != nil { if res, err := DBExec("DELETE FROM exercice_hints WHERE id_hint = ?", h.Id); err != nil {
return 0, err return 0, err
@ -87,6 +100,7 @@ func (h EHint) Delete() (int64, error) {
} }
} }
// WipeHints deletes (only in the database, not on disk) hints coming with the challenge.
func (e Exercice) WipeHints() (int64, error) { func (e Exercice) WipeHints() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_hints WHERE id_exercice = ?", e.Id); err != nil { if res, err := DBExec("DELETE FROM exercice_hints WHERE id_exercice = ?", e.Id); err != nil {
return 0, err return 0, err
@ -97,6 +111,7 @@ func (e Exercice) WipeHints() (int64, error) {
} }
} }
// GetExercice returns the parent Exercice where this hint can be found.
func (h EHint) GetExercice() (Exercice, error) { func (h EHint) GetExercice() (Exercice, error) {
var eid int64 var eid int64
if err := DBQueryRow("SELECT id_exercice FROM exercice_hints WHERE id_hint = ?", h.Id).Scan(&eid); err != nil { if err := DBQueryRow("SELECT id_exercice FROM exercice_hints WHERE id_hint = ?", h.Id).Scan(&eid); err != nil {

View File

@ -6,13 +6,19 @@ import (
"golang.org/x/crypto/blake2b" "golang.org/x/crypto/blake2b"
) )
// Key represents a flag's challenge, stored as hash.
type Key struct { type Key struct {
Id int64 `json:"id"` Id int64 `json:"id"`
// IdExercice is the identifier of the underlying challenge
IdExercice int64 `json:"idExercice"` IdExercice int64 `json:"idExercice"`
// Label is the title of the flag as displayed to players
Label string `json:"label"` Label string `json:"label"`
// Checksum is the expected hashed flag
Checksum []byte `json:"value"` Checksum []byte `json:"value"`
} }
// GetKeys returns a list of flags comming with the challenge.
func (e Exercice) GetKeys() ([]Key, error) { func (e Exercice) GetKeys() ([]Key, error) {
if rows, err := DBQuery("SELECT id_key, id_exercice, type, cksum FROM exercice_keys WHERE id_exercice = ?", e.Id); err != nil { if rows, err := DBQuery("SELECT id_key, id_exercice, type, cksum FROM exercice_keys WHERE id_exercice = ?", e.Id); err != nil {
return nil, err return nil, err
@ -37,16 +43,19 @@ func (e Exercice) GetKeys() ([]Key, error) {
} }
} }
// getHashedKey calculates the expected checksum for the given raw_value.
func getHashedKey(raw_value string) [blake2b.Size]byte { func getHashedKey(raw_value string) [blake2b.Size]byte {
hash := blake2b.Sum512([]byte(raw_value)) hash := blake2b.Sum512([]byte(raw_value))
return hash return hash
} }
// AddRawKey creates and fills a new struct Key, from a non-hashed flag, and registers it into the database.
func (e Exercice) AddRawKey(name string, raw_value string) (Key, error) { func (e Exercice) AddRawKey(name string, raw_value string) (Key, error) {
hash := getHashedKey(raw_value) hash := getHashedKey(raw_value)
return e.AddKey(name, hash[:]) return e.AddKey(name, hash[:])
} }
// AddKey creates and fills a new struct Key, from a hashed flag, and registers it into the database.
func (e Exercice) AddKey(name string, checksum []byte) (Key, error) { func (e Exercice) AddKey(name string, checksum []byte) (Key, error) {
if res, err := DBExec("INSERT INTO exercice_keys (id_exercice, type, cksum) VALUES (?, ?, ?)", e.Id, name, checksum); err != nil { if res, err := DBExec("INSERT INTO exercice_keys (id_exercice, type, cksum) VALUES (?, ?, ?)", e.Id, name, checksum); err != nil {
return Key{}, err return Key{}, err
@ -57,6 +66,7 @@ func (e Exercice) AddKey(name string, checksum []byte) (Key, error) {
} }
} }
// Update applies modifications back to the database.
func (k Key) Update() (int64, error) { func (k Key) Update() (int64, error) {
if res, err := DBExec("UPDATE exercice_keys SET id_exercice = ?, type = ?, cksum = ? WHERE id_key = ?", k.IdExercice, k.Label, k.Checksum, k.Id); err != nil { if res, err := DBExec("UPDATE exercice_keys SET id_exercice = ?, type = ?, cksum = ? WHERE id_key = ?", k.IdExercice, k.Label, k.Checksum, k.Id); err != nil {
return 0, err return 0, err
@ -67,6 +77,7 @@ func (k Key) Update() (int64, error) {
} }
} }
// Delete the flag from the database.
func (k Key) Delete() (int64, error) { func (k Key) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_keys WHERE id_key = ?", k.Id); err != nil { if res, err := DBExec("DELETE FROM exercice_keys WHERE id_key = ?", k.Id); err != nil {
return 0, err return 0, err
@ -77,6 +88,7 @@ func (k Key) Delete() (int64, error) {
} }
} }
// WipeKeys deletes keys coming with the challenge.
func (e Exercice) WipeKeys() (int64, error) { func (e Exercice) WipeKeys() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_keys WHERE id_exercice = ?", e.Id); err != nil { if res, err := DBExec("DELETE FROM exercice_keys WHERE id_exercice = ?", e.Id); err != nil {
return 0, err return 0, err
@ -87,6 +99,7 @@ func (e Exercice) WipeKeys() (int64, error) {
} }
} }
// Check if the given val is the expected one for this flag.
func (k Key) Check(val string) bool { func (k Key) Check(val string) bool {
hash := getHashedKey(val) hash := getHashedKey(val)
if len(k.Checksum) != len(hash) { if len(k.Checksum) != len(hash) {
@ -102,6 +115,7 @@ func (k Key) Check(val string) bool {
return true return true
} }
// FoundBy registers in the database that the given Team solved the flag.
func (k Key) FoundBy(t Team) { func (k Key) FoundBy(t Team) {
DBExec("INSERT INTO key_found (id_key, id_team, time) VALUES (?, ?, ?)", k.Id, t.Id, time.Now()) DBExec("INSERT INTO key_found (id_key, id_team, time) VALUES (?, ?, ?)", k.Id, t.Id, time.Now())
} }

View File

@ -4,19 +4,27 @@ import (
"time" "time"
) )
// MCQ represents a flag's challenge, in the form of checkbox.
type MCQ struct { type MCQ struct {
Id int64 `json:"id"` Id int64 `json:"id"`
// IdExercice is the identifier of the underlying challenge
IdExercice int64 `json:"idExercice"` IdExercice int64 `json:"idExercice"`
// Title is the label of the question
Title string `json:"title"` Title string `json:"title"`
// Entries stores the set of proposed answers
Entries []MCQ_entry `json:"entries"` Entries []MCQ_entry `json:"entries"`
} }
// MCQ_entry represents a proposed response for a given MCQ.
type MCQ_entry struct { type MCQ_entry struct {
Id int64 `json:"id"` Id int64 `json:"id"`
// Label is the text displayed to players as proposed answer
Label string `json:"label"` Label string `json:"label"`
// Response stores if expected checked state.
Response bool `json:"response"` Response bool `json:"response"`
} }
// GetMCQ returns the MCQs coming with the challenge.
func (e Exercice) GetMCQ() ([]MCQ, error) { func (e Exercice) GetMCQ() ([]MCQ, error) {
if rows, err := DBQuery("SELECT id_mcq, id_exercice, title FROM exercice_mcq WHERE id_exercice = ?", e.Id); err != nil { if rows, err := DBQuery("SELECT id_mcq, id_exercice, title FROM exercice_mcq WHERE id_exercice = ?", e.Id); err != nil {
return nil, err return nil, err
@ -58,6 +66,7 @@ func (e Exercice) GetMCQ() ([]MCQ, error) {
} }
} }
// AddMCQ creates and fills a new struct MCQ and registers it into the database.
func (e Exercice) AddMCQ(title string) (MCQ, error) { func (e Exercice) AddMCQ(title string) (MCQ, error) {
if res, err := DBExec("INSERT INTO exercice_mcq (id_exercice, title) VALUES (?, ?)", e.Id, title); err != nil { if res, err := DBExec("INSERT INTO exercice_mcq (id_exercice, title) VALUES (?, ?)", e.Id, title); err != nil {
return MCQ{}, err return MCQ{}, err
@ -68,6 +77,7 @@ func (e Exercice) AddMCQ(title string) (MCQ, error) {
} }
} }
// Update applies modifications back to the database.
func (m MCQ) Update() (int64, error) { func (m MCQ) Update() (int64, error) {
if res, err := DBExec("UPDATE exercice_mcq SET id_exercice = ?, title = ? WHERE id_mcq = ?", m.IdExercice, m.Title, m.Id); err != nil { if res, err := DBExec("UPDATE exercice_mcq SET id_exercice = ?, title = ? WHERE id_mcq = ?", m.IdExercice, m.Title, m.Id); err != nil {
return 0, err return 0, err
@ -78,6 +88,7 @@ func (m MCQ) Update() (int64, error) {
} }
} }
// Delete the MCQ from the database.
func (m MCQ) Delete() (int64, error) { func (m MCQ) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_mcq WHERE id_mcq = ?", m.Id); err != nil { if res, err := DBExec("DELETE FROM exercice_mcq WHERE id_mcq = ?", m.Id); err != nil {
return 0, err return 0, err
@ -88,6 +99,7 @@ func (m MCQ) Delete() (int64, error) {
} }
} }
// AddEntry creates and fills a new struct MCQ_entry and registers it into the database.
func (m MCQ) AddEntry(label string, response bool) (MCQ_entry, error) { func (m MCQ) AddEntry(label string, response bool) (MCQ_entry, error) {
if res, err := DBExec("INSERT INTO mcq_entries (id_mcq, label, response) VALUES (?, ?, ?)", m.Id, label, response); err != nil { if res, err := DBExec("INSERT INTO mcq_entries (id_mcq, label, response) VALUES (?, ?, ?)", m.Id, label, response); err != nil {
return MCQ_entry{}, err return MCQ_entry{}, err
@ -98,6 +110,7 @@ func (m MCQ) AddEntry(label string, response bool) (MCQ_entry, error) {
} }
} }
// Update applies modifications back to the database.
func (n MCQ_entry) Update() (int64, error) { func (n MCQ_entry) Update() (int64, error) {
if res, err := DBExec("UPDATE mcq_entries SET label = ?, response = ? WHERE id_mcq = ?", n.Label, n.Response, n.Id); err != nil { if res, err := DBExec("UPDATE mcq_entries SET label = ?, response = ? WHERE id_mcq = ?", n.Label, n.Response, n.Id); err != nil {
return 0, err return 0, err
@ -108,6 +121,7 @@ func (n MCQ_entry) Update() (int64, error) {
} }
} }
// Delete the MCQ entry from the database.
func (n MCQ_entry) Delete() (int64, error) { func (n MCQ_entry) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM mcq_entries WHERE id_mcq_entry = ?", n.Id); err != nil { if res, err := DBExec("DELETE FROM mcq_entries WHERE id_mcq_entry = ?", n.Id); err != nil {
return 0, err return 0, err
@ -118,6 +132,7 @@ func (n MCQ_entry) Delete() (int64, error) {
} }
} }
// WipeMCQs deletes MCQs coming with the challenge.
func (e Exercice) WipeMCQs() (int64, error) { func (e Exercice) WipeMCQs() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_mcq, mcq_found, mcq_entries USING exercice_mcq NATURAL JOIN mcq_entries NATURAL JOIN mcq_found WHERE exercice_mcq.id_exercice = ?", e.Id); err != nil { if res, err := DBExec("DELETE FROM exercice_mcq, mcq_found, mcq_entries USING exercice_mcq NATURAL JOIN mcq_entries NATURAL JOIN mcq_found WHERE exercice_mcq.id_exercice = ?", e.Id); err != nil {
return 0, err return 0, err
@ -128,6 +143,7 @@ func (e Exercice) WipeMCQs() (int64, error) {
} }
} }
// Check if the given vals are the expected ones to validate this flag.
func (m MCQ) Check(vals map[int64]bool) int { func (m MCQ) Check(vals map[int64]bool) int {
diff := 0 diff := 0
@ -141,6 +157,7 @@ func (m MCQ) Check(vals map[int64]bool) int {
return diff return diff
} }
// FoundBy registers in the database that the given Team solved the MCQ.
func (m MCQ) FoundBy(t Team) { func (m MCQ) FoundBy(t Team) {
DBExec("INSERT INTO mcq_found (id_mcq, id_team, time) VALUES (?, ?, ?)", m.Id, t.Id, time.Now()) DBExec("INSERT INTO mcq_found (id_mcq, id_team, time) VALUES (?, ?, ?)", m.Id, t.Id, time.Now())
} }

View File

@ -2,6 +2,7 @@ package fic
import () import ()
// Member represents a team member
type Member struct { type Member struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Firstname string `json:"firstname"` Firstname string `json:"firstname"`
@ -10,15 +11,7 @@ type Member struct {
Company string `json:"company"` Company string `json:"company"`
} }
func GetMember(cnt int) (Team, error) { // GetMembers retrieves the members of the Team
var t Team
if err := DBQueryRow("SELECT T.id_team, T.name, T.color FROM team_members M RIGHT OUTER JOIN teams T ON T.id_team = M.id_team LIMIT ?, 1", cnt - 1).Scan(&t.Id, &t.Name, &t.Color); err != nil {
return t, err
}
return t, nil
}
func (t Team) GetMembers() ([]Member, error) { func (t Team) GetMembers() ([]Member, error) {
if rows, err := DBQuery("SELECT id_member, firstname, lastname, nickname, company FROM team_members WHERE id_team = ?", t.Id); err != nil { if rows, err := DBQuery("SELECT id_member, firstname, lastname, nickname, company FROM team_members WHERE id_team = ?", t.Id); err != nil {
return nil, err return nil, err

View File

@ -2,6 +2,7 @@ package fic
import () import ()
// truncateTable performs an insecure wipe on the given tables.
func truncateTable(tables ...string) (error) { func truncateTable(tables ...string) (error) {
if _, err := DBExec("SET FOREIGN_KEY_CHECKS = 0;"); err != nil { if _, err := DBExec("SET FOREIGN_KEY_CHECKS = 0;"); err != nil {
return err return err
@ -17,14 +18,17 @@ func truncateTable(tables ...string) (error) {
return nil return nil
} }
// ResetGame resets all tables containing team attempts and solves.
func ResetGame() (error) { func ResetGame() (error) {
return truncateTable("team_hints", "key_found", "mcq_found", "exercice_solved", "exercice_tries") return truncateTable("team_hints", "key_found", "mcq_found", "exercice_solved", "exercice_tries")
} }
// ResetExercices wipes out all challenges (both attempts and statements).
func ResetExercices() (error) { func ResetExercices() (error) {
return truncateTable("team_hints", "exercice_files", "key_found", "exercice_keys", "exercice_solved", "exercice_tries", "exercice_hints", "mcq_found", "mcq_entries", "exercice_mcq", "exercices", "themes") return truncateTable("team_hints", "exercice_files", "key_found", "exercice_keys", "exercice_solved", "exercice_tries", "exercice_hints", "mcq_found", "mcq_entries", "exercice_mcq", "exercices", "themes")
} }
// ResetTeams wipes out all teams, incluings members and attempts.
func ResetTeams() (error) { func ResetTeams() (error) {
return truncateTable("team_hints", "key_found", "mcq_found", "exercice_solved", "exercice_tries", "team_members", "teams") return truncateTable("team_hints", "key_found", "mcq_found", "exercice_solved", "exercice_tries", "team_members", "teams")
} }

View File

@ -6,7 +6,10 @@ import (
"time" "time"
) )
// FirstBlood is the coefficient added to the challenge coefficient when a Team is the first to solve a challenge.
var FirstBlood = 0.12 var FirstBlood = 0.12
// SubmissionCostBase is the basis amount of point lost per submission
var SubmissionCostBase = 0.5 var SubmissionCostBase = 0.5
func exoptsQuery(whereExo string) string { func exoptsQuery(whereExo string) string {
@ -19,6 +22,7 @@ func rankQuery(whereTeam string) string {
// Points // Points
// EstimateGain calculates the amount of point the Team has (or could have, if not already solved) won.
func (e Exercice) EstimateGain(t Team, solved bool) (float64, error) { func (e Exercice) EstimateGain(t Team, solved bool) (float64, error) {
var pts float64 var pts float64
err := DBQueryRow("SELECT SUM(A.points * A.coeff) AS score FROM (" + exoptsQuery("WHERE S.id_team = ? AND S.id_exercice = ?") + ") A GROUP BY id_team", t.Id, e.Id, t.Id, e.Id).Scan(&pts) err := DBQueryRow("SELECT SUM(A.points * A.coeff) AS score FROM (" + exoptsQuery("WHERE S.id_team = ? AND S.id_exercice = ?") + ") A GROUP BY id_team", t.Id, e.Id, t.Id, e.Id).Scan(&pts)
@ -33,6 +37,7 @@ func (e Exercice) EstimateGain(t Team, solved bool) (float64, error) {
} }
} }
// GetPoints returns the score for the Team.
func (t Team) GetPoints() (float64, error) { func (t Team) GetPoints() (float64, error) {
var tid *int64 var tid *int64
var nb *float64 var nb *float64
@ -45,6 +50,7 @@ func (t Team) GetPoints() (float64, error) {
} }
} }
// GetRank returns a map which associates team ID their rank.
func GetRank() (map[int64]int, error) { func GetRank() (map[int64]int, error) {
if rows, err := DBQuery(rankQuery("")); err != nil { if rows, err := DBQuery(rankQuery("")); err != nil {
return nil, err return nil, err
@ -72,11 +78,11 @@ func GetRank() (map[int64]int, error) {
} }
// Tries // Attempts
func GetTries(t *Team, e *Exercice) ([]time.Time, error) { // GetTries retrieves all attempts made by the matching Team or challenge (both can be nil to not filter).
func GetTries(t *Team, e *Exercice) (times []time.Time, err error) {
var rows *sql.Rows var rows *sql.Rows
var err error
if t == nil { if t == nil {
if e == nil { if e == nil {
@ -93,26 +99,23 @@ func GetTries(t *Team, e *Exercice) ([]time.Time, error) {
} }
if err != nil { if err != nil {
return nil, err return
} else { } else {
defer rows.Close() defer rows.Close()
times := make([]time.Time, 0)
for rows.Next() { for rows.Next() {
var tm time.Time var tm time.Time
if err := rows.Scan(&tm); err != nil { if err = rows.Scan(&tm); err != nil {
return nil, err return
} }
times = append(times, tm) times = append(times, tm)
} }
if err := rows.Err(); err != nil { err = rows.Err()
return nil, err return
}
return times, nil
} }
} }
// GetTryRank generates a special rank based on number of attempts
func GetTryRank() ([]int64, error) { func GetTryRank() ([]int64, error) {
if rows, err := DBQuery("SELECT id_team, COUNT(*) AS score FROM exercice_tries GROUP BY id_team HAVING score > 0 ORDER BY score DESC"); err != nil { if rows, err := DBQuery("SELECT id_team, COUNT(*) AS score FROM exercice_tries GROUP BY id_team HAVING score > 0 ORDER BY score DESC"); err != nil {
return nil, err return nil, err

View File

@ -4,16 +4,17 @@ import (
"time" "time"
) )
// UnlockedChallenges disables dependancy requirement between challenges.
var UnlockedChallenges bool var UnlockedChallenges bool
// Team represents a group of players, come to solve our challenges.
type Team struct { type Team struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Color uint32 `json:"color"` Color uint32 `json:"color"`
} }
// Access functions // GetTeams returns a list of registered Team from the database.
func GetTeams() ([]Team, error) { func GetTeams() ([]Team, error) {
if rows, err := DBQuery("SELECT id_team, name, color FROM teams"); err != nil { if rows, err := DBQuery("SELECT id_team, name, color FROM teams"); err != nil {
return nil, err return nil, err
@ -36,6 +37,7 @@ func GetTeams() ([]Team, error) {
} }
} }
// GetTeam retrieves a Team from its identifier.
func GetTeam(id int64) (Team, error) { func GetTeam(id int64) (Team, error) {
var t Team var t Team
if err := DBQueryRow("SELECT id_team, name, color FROM teams WHERE id_team = ?", id).Scan(&t.Id, &t.Name, &t.Color); err != nil { if err := DBQueryRow("SELECT id_team, name, color FROM teams WHERE id_team = ?", id).Scan(&t.Id, &t.Name, &t.Color); err != nil {
@ -45,6 +47,7 @@ func GetTeam(id int64) (Team, error) {
return t, nil return t, nil
} }
// GetTeamBySerial retrieves a Team from one of its associated certificates.
func GetTeamBySerial(serial int64) (Team, error) { func GetTeamBySerial(serial int64) (Team, error) {
var t Team var t Team
if err := DBQueryRow("SELECT T.id_team, T.name, T.color FROM certificates C INNER JOIN teams T ON T.id_team = C.id_team WHERE id_cert = ?", serial).Scan(&t.Id, &t.Name, &t.Color); err != nil { if err := DBQueryRow("SELECT T.id_team, T.name, T.color FROM certificates C INNER JOIN teams T ON T.id_team = C.id_team WHERE id_cert = ?", serial).Scan(&t.Id, &t.Name, &t.Color); err != nil {
@ -54,9 +57,7 @@ func GetTeamBySerial(serial int64) (Team, error) {
return t, nil return t, nil
} }
// CreateTeam creates and fills a new struct Team and registers it into the database.
// CRUD method
func CreateTeam(name string, color uint32) (Team, error) { func CreateTeam(name string, color uint32) (Team, error) {
if res, err := DBExec("INSERT INTO teams (name, color) VALUES (?, ?)", name, color); err != nil { if res, err := DBExec("INSERT INTO teams (name, color) VALUES (?, ?)", name, color); err != nil {
return Team{}, err return Team{}, err
@ -67,6 +68,7 @@ func CreateTeam(name string, color uint32) (Team, error) {
} }
} }
// Update applies modifications back to the database.
func (t Team) Update() (int64, error) { func (t Team) Update() (int64, error) {
if res, err := DBExec("UPDATE teams SET name = ?, color = ? WHERE id_team = ?", t.Name, t.Color, t.Id); err != nil { if res, err := DBExec("UPDATE teams SET name = ?, color = ? WHERE id_team = ?", t.Name, t.Color, t.Id); err != nil {
return 0, err return 0, err
@ -77,6 +79,7 @@ func (t Team) Update() (int64, error) {
} }
} }
// Delete the challenge from the database
func (t Team) Delete() (int64, error) { func (t Team) Delete() (int64, error) {
if _, err := DBExec("DELETE FROM team_members WHERE id_team = ?", t.Id); err != nil { if _, err := DBExec("DELETE FROM team_members WHERE id_team = ?", t.Id); err != nil {
return 0, err return 0, err
@ -89,20 +92,19 @@ func (t Team) Delete() (int64, error) {
} }
} }
// HasAccess checks if the Team has access to the given challenge.
// Exercice related functions
func (t Team) HasAccess(e Exercice) bool { func (t Team) HasAccess(e Exercice) bool {
if e.Depend == nil || UnlockedChallenges { if e.Depend == nil || UnlockedChallenges {
return true return true
} else { } else {
ed := Exercice{} ed := Exercice{}
ed.Id = *e.Depend ed.Id = *e.Depend
s, _, _ := t.HasSolved(ed) s, _ := t.HasSolved(ed)
return s return s
} }
} }
// NbTry retrieves the number of attempts made by the Team to the given challenge.
func NbTry(t *Team, e Exercice) int { func NbTry(t *Team, e Exercice) int {
var cnt *int var cnt *int
@ -119,17 +121,20 @@ func NbTry(t *Team, e Exercice) int {
} }
} }
// HasHint checks if the Team has revealed the given Hint.
func (t Team) HasHint(h EHint) (bool) { func (t Team) HasHint(h EHint) (bool) {
var tm *time.Time var tm *time.Time
DBQueryRow("SELECT MIN(time) FROM team_hints WHERE id_team = ? AND id_hint = ?", t.Id, h.Id).Scan(&tm) DBQueryRow("SELECT MIN(time) FROM team_hints WHERE id_team = ? AND id_hint = ?", t.Id, h.Id).Scan(&tm)
return tm != nil return tm != nil
} }
// OpenHint registers to the database that the Team has now revealed.
func (t Team) OpenHint(h EHint) (error) { func (t Team) OpenHint(h EHint) (error) {
_, err := DBExec("INSERT INTO team_hints (id_team, id_hint, time) VALUES (?, ?, ?)", t.Id, h.Id, time.Now()) _, err := DBExec("INSERT INTO team_hints (id_team, id_hint, time) VALUES (?, ?, ?)", t.Id, h.Id, time.Now())
return err return err
} }
// CountTries gets the amount of attempts made by the Team and retrieves the time of the latest attempt.
func (t Team) CountTries(e Exercice) (int64, time.Time) { func (t Team) CountTries(e Exercice) (int64, time.Time) {
var nb *int64 var nb *int64
var tm *time.Time var tm *time.Time
@ -142,6 +147,8 @@ func (t Team) CountTries(e Exercice) (int64, time.Time) {
} }
} }
// LastTryDist retrieves the distance to the correct answers, for the given challenge.
// The distance is the number of bad responses given in differents MCQs.
func (t Team) LastTryDist(e Exercice) int64 { func (t Team) LastTryDist(e Exercice) int64 {
var nb *int64 var nb *int64
if DBQueryRow("SELECT nbdiff FROM exercice_tries WHERE id_team = ? AND id_exercice = ? ORDER BY time DESC LIMIT 1", t.Id, e.Id).Scan(&nb); nb == nil { if DBQueryRow("SELECT nbdiff FROM exercice_tries WHERE id_team = ? AND id_exercice = ? ORDER BY time DESC LIMIT 1", t.Id, e.Id).Scan(&nb); nb == nil {
@ -151,36 +158,45 @@ func (t Team) LastTryDist(e Exercice) int64 {
} }
} }
func (t Team) HasSolved(e Exercice) (bool, time.Time, int64) { // HasSolved checks if the Team already has validated the given challenge.
var nb *int64 // Note that the function also returns the effective validation timestamp.
func (t Team) HasSolved(e Exercice) (bool, time.Time) {
var tm *time.Time var tm *time.Time
if DBQueryRow("SELECT MIN(time) FROM exercice_solved WHERE id_team = ? AND id_exercice = ?", t.Id, e.Id).Scan(&tm); tm == nil { if DBQueryRow("SELECT MIN(time) FROM exercice_solved WHERE id_team = ? AND id_exercice = ?", t.Id, e.Id).Scan(&tm); tm == nil {
return false, time.Unix(0, 0), 0 return false, time.Unix(0, 0)
} else if DBQueryRow("SELECT COUNT(id_exercice) FROM exercice_solved WHERE id_exercice = ? AND time < ?", e.Id, tm).Scan(&nb); nb == nil {
return true, *tm, 0
} else { } else {
return true, *tm, *nb + 1 return true, *tm
} }
} }
func IsSolved(e Exercice) (int, time.Time) { // GetSolvedRank returns the number of teams that solved the challenge before the Team.
var nb *int func (t Team) GetSolvedRank(e Exercice) (nb int64, err error) {
var tm *time.Time if rows, errr := DBQuery("SELECT id_team FROM exercice_solved WHERE id_exercice = ? ORDER BY time ASC", e.Id); err != nil {
if DBQueryRow("SELECT COUNT(id_exercice), MIN(time) FROM exercice_solved WHERE id_exercice = ?", e.Id).Scan(&nb, &tm); nb == nil || tm == nil { return nb, errr
return 0, time.Time{}
} else { } else {
return *nb, *tm for rows.Next() {
var tid int64
if err = rows.Scan(&tid); err != nil {
return
}
nb += 1
if t.Id == tid {
break
}
}
return
} }
} }
func (t Team) HasPartiallySolved(k Key) (*time.Time) { // HasPartiallySolved checks if the Team already has unlocked the given key and returns the validation's timestamp.
var tm *time.Time func (t Team) HasPartiallySolved(k Key) (tm *time.Time) {
DBQueryRow("SELECT MIN(time) FROM key_found WHERE id_team = ? AND id_key = ?", t.Id, k.Id).Scan(&tm) DBQueryRow("SELECT MIN(time) FROM key_found WHERE id_team = ? AND id_key = ?", t.Id, k.Id).Scan(&tm)
return tm return
} }
func (t Team) HasPartiallyRespond(m MCQ) (*time.Time) { // HasPartiallyRespond checks if the Team already has unlocked the given MCQ and returns the validation's timestamp.
var tm *time.Time func (t Team) HasPartiallyRespond(m MCQ) (tm *time.Time) {
DBQueryRow("SELECT MIN(time) FROM mcq_found WHERE id_team = ? AND id_mcq = ?", t.Id, m.Id).Scan(&tm) DBQueryRow("SELECT MIN(time) FROM mcq_found WHERE id_team = ? AND id_mcq = ?", t.Id, m.Id).Scan(&tm)
return tm return
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
) )
// exportedTeam is a structure representing a Team, as exposed to players.
type exportedTeam struct { type exportedTeam struct {
Name string `json:"name"` Name string `json:"name"`
Color string `json:"color"` Color string `json:"color"`
@ -11,13 +12,17 @@ type exportedTeam struct {
Points float64 `json:"score"` Points float64 `json:"score"`
} }
func ExportTeams() (interface{}, error) { // Exportedteam creates the structure to respond as teams.json.
if teams, err := GetTeams(); err != nil { func ExportTeams() (ret map[string]exportedTeam, err error) {
return nil, err var teams []Team
} else if rank, err := GetRank(); err != nil { var rank map[int64]int
if teams, err = GetTeams(); err != nil {
return
} else if rank, err = GetRank(); err != nil {
return nil, err return nil, err
} else { } else {
ret := map[string]exportedTeam{} ret = map[string]exportedTeam{}
for _, team := range teams { for _, team := range teams {
points, _ := team.GetPoints() points, _ := team.GetPoints()
ret[fmt.Sprintf("%d", team.Id)] = exportedTeam{ ret[fmt.Sprintf("%d", team.Id)] = exportedTeam{
@ -28,6 +33,6 @@ func ExportTeams() (interface{}, error) {
} }
} }
return ret, nil return
} }
} }

View File

@ -4,6 +4,7 @@ import (
"time" "time"
) )
// GetHistory aggregates all sources of events or actions for a Team
func (t Team) GetHistory() ([]map[string]interface{}, error) { func (t Team) GetHistory() ([]map[string]interface{}, error) {
hist := make([]map[string]interface{}, 0) hist := make([]map[string]interface{}, 0)
@ -45,6 +46,7 @@ func (t Team) GetHistory() ([]map[string]interface{}, error) {
return hist, nil return hist, nil
} }
// DelHistoryItem removes from the database an entry from the history.
func (t Team) DelHistoryItem(kind string, h time.Time, primary *int64, secondary *int64) (interface{}, error) { func (t Team) DelHistoryItem(kind string, h time.Time, primary *int64, secondary *int64) (interface{}, error) {
if kind == "tries" && primary != nil { if kind == "tries" && primary != nil {
if res, err := DBExec("DELETE FROM exercice_tries WHERE id_team = ? AND time = ? AND id_exercice = ?", t.Id, h, *primary); err != nil { if res, err := DBExec("DELETE FROM exercice_tries WHERE id_team = ? AND time = ? AND id_exercice = ?", t.Id, h, *primary); err != nil {

View File

@ -88,7 +88,8 @@ func MyJSONTeam(t *Team, started bool) (interface{}, error) {
exercice.Gain = int(float64(e.Gain) * e.Coefficient) exercice.Gain = int(float64(e.Gain) * e.Coefficient)
} else { } else {
var solved bool var solved bool
solved, exercice.SolvedTime, exercice.SolvedRank = t.HasSolved(e) solved, exercice.SolvedTime = t.HasSolved(e)
exercice.SolvedRank, _ = t.GetSolvedRank(e)
if solved { if solved {
exercice.Tries, _ = t.CountTries(e) exercice.Tries, _ = t.CountTries(e)

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
) )
// statLine is a line of statistics for the file stats.json exposed to players.
type statLine struct { type statLine struct {
Tip string `json:"tip"` Tip string `json:"tip"`
Total int `json:"total"` Total int `json:"total"`
@ -12,11 +13,13 @@ type statLine struct {
Tries int `json:"tries"` Tries int `json:"tries"`
} }
// teamStats represents the structure of stats.json.
type teamStats struct { type teamStats struct {
Levels []statLine `json:"levels"` Levels []statLine `json:"levels"`
Themes map[int64]statLine `json:"themes"` Themes map[int64]statLine `json:"themes"`
} }
// GetLevel
func (s *teamStats) GetLevel(level int) *statLine { func (s *teamStats) GetLevel(level int) *statLine {
level -= 1 level -= 1
@ -33,10 +36,12 @@ func (s *teamStats) GetLevel(level int) *statLine {
return &s.Levels[level] return &s.Levels[level]
} }
// GetStats generates statistics for the Team.
func (t Team) GetStats() (interface{}, error) { func (t Team) GetStats() (interface{}, error) {
return GetTeamsStats(&t) return GetTeamsStats(&t)
} }
// GetTeamsStats returns statistics limited to the given Team.
func GetTeamsStats(t *Team) (interface{}, error) { func GetTeamsStats(t *Team) (interface{}, error) {
stat := teamStats{ stat := teamStats{
[]statLine{}, []statLine{},
@ -66,12 +71,12 @@ func GetTeamsStats(t *Team) (interface{}, error) {
sLvl.Total += 1 sLvl.Total += 1
if t != nil { if t != nil {
if b, _, _ := t.HasSolved(exercice); b { if b, _ := t.HasSolved(exercice); b {
solved += 1 solved += 1
sLvl.Solved += 1 sLvl.Solved += 1
} }
} else { } else {
if n, _ := IsSolved(exercice); n > 0 { if n, _ := exercice.IsSolved(); n > 0 {
solved += 1 solved += 1
sLvl.Solved += 1 sLvl.Solved += 1
} }

View File

@ -2,6 +2,7 @@ package fic
import () import ()
// Theme represents a group of challenges, to display to players
type Theme struct { type Theme struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -10,6 +11,7 @@ type Theme struct {
Intro string `json:"intro,omitempty"` Intro string `json:"intro,omitempty"`
} }
// GetThemes returns a list of registered Themes from the database.
func GetThemes() ([]Theme, error) { func GetThemes() ([]Theme, error) {
if rows, err := DBQuery("SELECT id_theme, name, url_id, authors, intro FROM themes"); err != nil { if rows, err := DBQuery("SELECT id_theme, name, url_id, authors, intro FROM themes"); err != nil {
return nil, err return nil, err
@ -32,6 +34,7 @@ func GetThemes() ([]Theme, error) {
} }
} }
// GetTheme retrieves a Theme from its identifier.
func GetTheme(id int) (Theme, error) { func GetTheme(id int) (Theme, error) {
var t Theme var t Theme
if err := DBQueryRow("SELECT id_theme, name, url_id, authors, intro FROM themes WHERE id_theme=?", id).Scan(&t.Id, &t.Name, &t.URLId, &t.Authors, &t.Intro); err != nil { if err := DBQueryRow("SELECT id_theme, name, url_id, authors, intro FROM themes WHERE id_theme=?", id).Scan(&t.Id, &t.Name, &t.URLId, &t.Authors, &t.Intro); err != nil {
@ -41,6 +44,7 @@ func GetTheme(id int) (Theme, error) {
return t, nil return t, nil
} }
// GetThemeByName retrieves a Theme from its title
func GetThemeByName(name string) (Theme, error) { func GetThemeByName(name string) (Theme, error) {
var t Theme var t Theme
if err := DBQueryRow("SELECT id_theme, name, url_id, authors, intro FROM themes WHERE name=?", name).Scan(&t.Id, &t.Name, &t.URLId, &t.Authors, &t.Intro); err != nil { if err := DBQueryRow("SELECT id_theme, name, url_id, authors, intro FROM themes WHERE name=?", name).Scan(&t.Id, &t.Name, &t.URLId, &t.Authors, &t.Intro); err != nil {
@ -50,6 +54,7 @@ func GetThemeByName(name string) (Theme, error) {
return t, nil return t, nil
} }
// CreateTheme creates and fills a new struct Theme and registers it into the database.
func CreateTheme(name string, url_id string, authors string, intro string) (Theme, error) { func CreateTheme(name string, url_id string, authors string, intro string) (Theme, error) {
if res, err := DBExec("INSERT INTO themes (name, url_id, authors, intro) VALUES (?, ?, ?, ?)", name, url_id, authors, intro); err != nil { if res, err := DBExec("INSERT INTO themes (name, url_id, authors, intro) VALUES (?, ?, ?, ?)", name, url_id, authors, intro); err != nil {
return Theme{}, err return Theme{}, err
@ -60,6 +65,8 @@ func CreateTheme(name string, url_id string, authors string, intro string) (Them
} }
} }
// FixURLId generates a valid URLid from the Theme Title.
// It returns true if something has been generated.
func (t *Theme) FixURLId() bool { func (t *Theme) FixURLId() bool {
if t.URLId == "" { if t.URLId == "" {
t.URLId = ToURLid(t.Name) t.URLId = ToURLid(t.Name)
@ -68,6 +75,7 @@ func (t *Theme) FixURLId() bool {
return false return false
} }
// Update applies modifications back to the database.
func (t Theme) Update() (int64, error) { func (t Theme) Update() (int64, error) {
if res, err := DBExec("UPDATE themes SET name = ?, url_id = ?, authors = ?, intro = ? WHERE id_theme = ?", t.Name, t.URLId, t.Authors, t.Intro, t.Id); err != nil { if res, err := DBExec("UPDATE themes SET name = ?, url_id = ?, authors = ?, intro = ? WHERE id_theme = ?", t.Name, t.URLId, t.Authors, t.Intro, t.Id); err != nil {
return 0, err return 0, err
@ -78,6 +86,7 @@ func (t Theme) Update() (int64, error) {
} }
} }
// Delete the theme from the database.
func (t Theme) Delete() (int64, error) { func (t Theme) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM themes WHERE id_theme = ?", t.Id); err != nil { if res, err := DBExec("DELETE FROM themes WHERE id_theme = ?", t.Id); err != nil {
return 0, err return 0, err

View File

@ -4,7 +4,8 @@ import (
"fmt" "fmt"
) )
type ExportedExercice struct { // exportedExercice is a structure representing a challenge, as exposed to players.
type exportedExercice struct {
Title string `json:"title"` Title string `json:"title"`
URLId string `json:"urlid"` URLId string `json:"urlid"`
Gain int64 `json:"gain"` Gain int64 `json:"gain"`
@ -13,14 +14,16 @@ type ExportedExercice struct {
Tried int64 `json:"tried"` Tried int64 `json:"tried"`
} }
// exportedTheme is a structure representing a Theme, as exposed to players.
type exportedTheme struct { type exportedTheme struct {
Name string `json:"name"` Name string `json:"name"`
URLId string `json:"urlid"` URLId string `json:"urlid"`
Authors string `json:"authors"` Authors string `json:"authors"`
Intro string `json:"intro"` Intro string `json:"intro"`
Exercices map[string]ExportedExercice `json:"exercices"` Exercices map[string]exportedExercice `json:"exercices"`
} }
// Exportedthemes exports themes from the database, to be displayed to players.
func ExportThemes() (interface{}, error) { func ExportThemes() (interface{}, error) {
if themes, err := GetThemes(); err != nil { if themes, err := GetThemes(); err != nil {
return nil, err return nil, err
@ -30,9 +33,9 @@ func ExportThemes() (interface{}, error) {
if exercices, err := theme.GetExercices(); err != nil { if exercices, err := theme.GetExercices(); err != nil {
return nil, err return nil, err
} else { } else {
exos := map[string]ExportedExercice{} exos := map[string]exportedExercice{}
for _, exercice := range exercices { for _, exercice := range exercices {
exos[fmt.Sprintf("%d", exercice.Id)] = ExportedExercice{ exos[fmt.Sprintf("%d", exercice.Id)] = exportedExercice{
exercice.Title, exercice.Title,
exercice.URLId, exercice.URLId,
exercice.Gain, exercice.Gain,

View File

@ -5,6 +5,7 @@ import (
"time" "time"
) )
// Claim represents an issue, a bug or a ToDo item.
type Claim struct { type Claim struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Subject string `json:"subject"` Subject string `json:"subject"`
@ -15,11 +16,13 @@ type Claim struct {
Priority string `json:"priority"` Priority string `json:"priority"`
} }
// GetClaim retrieves the claim with the given identifier.
func GetClaim(id int) (c Claim, err error) { func GetClaim(id int) (c Claim, err error) {
err = DBQueryRow("SELECT id_claim, subject, id_team, id_assignee, creation, state, priority FROM claims WHERE id_claim = ?", id).Scan(&c.Id, &c.Subject, &c.IdTeam, &c.IdAssignee, &c.Creation, &c.State, &c.Priority) err = DBQueryRow("SELECT id_claim, subject, id_team, id_assignee, creation, state, priority FROM claims WHERE id_claim = ?", id).Scan(&c.Id, &c.Subject, &c.IdTeam, &c.IdAssignee, &c.Creation, &c.State, &c.Priority)
return return
} }
// GetClaims returns a list of all Claim registered in the database.
func GetClaims() (res []Claim, err error) { func GetClaims() (res []Claim, err error) {
var rows *sql.Rows var rows *sql.Rows
if rows, err = DBQuery("SELECT id_claim, subject, id_team, id_assignee, creation, state, priority FROM claims"); err != nil { if rows, err = DBQuery("SELECT id_claim, subject, id_team, id_assignee, creation, state, priority FROM claims"); err != nil {
@ -39,6 +42,7 @@ func GetClaims() (res []Claim, err error) {
return return
} }
// GetClaims returns a list of all Claim registered for the Team.
func (t Team) GetClaims() (res []Claim, err error) { func (t Team) GetClaims() (res []Claim, err error) {
var rows *sql.Rows var rows *sql.Rows
if rows, err = DBQuery("SELECT id_claim, subject, IdTeam, id_assignee, creation, state, priority FROM claims WHERE IdTeam = ?", t.Id); err != nil { if rows, err = DBQuery("SELECT id_claim, subject, IdTeam, id_assignee, creation, state, priority FROM claims WHERE IdTeam = ?", t.Id); err != nil {
@ -58,6 +62,7 @@ func (t Team) GetClaims() (res []Claim, err error) {
return return
} }
// NewClaim creates and fills a new struct Claim and registers it into the database.
func NewClaim(subject string, team *Team, assignee *ClaimAssignee, priority string) (Claim, error) { func NewClaim(subject string, team *Team, assignee *ClaimAssignee, priority string) (Claim, error) {
var tid *int64 var tid *int64
if team == nil { if team == nil {
@ -82,6 +87,7 @@ func NewClaim(subject string, team *Team, assignee *ClaimAssignee, priority stri
} }
} }
// GetTeam returns the Team linked to the issue, if any.
func (c Claim) GetTeam() (*Team, error) { func (c Claim) GetTeam() (*Team, error) {
if c.IdTeam == nil { if c.IdTeam == nil {
return nil, nil return nil, nil
@ -92,10 +98,12 @@ func (c Claim) GetTeam() (*Team, error) {
} }
} }
// SetTeam defines the Team that is linked to this issue.
func (c Claim) SetTeam(t Team) { func (c Claim) SetTeam(t Team) {
c.IdTeam = &t.Id c.IdTeam = &t.Id
} }
// Update applies modifications back to the database.
func (c Claim) Update() (int64, error) { func (c Claim) Update() (int64, error) {
if res, err := DBExec("UPDATE claims SET subject = ?, id_team = ?, id_assignee = ?, creation = ?, state = ?, priority = ? WHERE id_claim = ?", c.Subject, c.IdTeam, c.IdAssignee, c.Creation, c.State, c.Priority, c.Id); err != nil { if res, err := DBExec("UPDATE claims SET subject = ?, id_team = ?, id_assignee = ?, creation = ?, state = ?, priority = ? WHERE id_claim = ?", c.Subject, c.IdTeam, c.IdAssignee, c.Creation, c.State, c.Priority, c.Id); err != nil {
return 0, err return 0, err
@ -106,6 +114,7 @@ func (c Claim) Update() (int64, error) {
} }
} }
// Delete the issue from the database.
func (c Claim) Delete() (int64, error) { func (c Claim) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM claims WHERE id_claim = ?", c.Id); err != nil { if res, err := DBExec("DELETE FROM claims WHERE id_claim = ?", c.Id); err != nil {
return 0, err return 0, err
@ -116,6 +125,7 @@ func (c Claim) Delete() (int64, error) {
} }
} }
// ClearClaims removes all issues from database.
func ClearClaims() (int64, error) { func ClearClaims() (int64, error) {
if res, err := DBExec("DELETE FROM claims"); err != nil { if res, err := DBExec("DELETE FROM claims"); err != nil {
return 0, err return 0, err
@ -126,14 +136,18 @@ func ClearClaims() (int64, error) {
} }
} }
// ClaimDescription represents some text describing an issue.
type ClaimDescription struct { type ClaimDescription struct {
Id int64 `json:"id"` Id int64 `json:"id"`
// IdAssignee stores the user who handle the claim (or 0 if nobody handles it).
IdAssignee int64 `json:"id_assignee"` IdAssignee int64 `json:"id_assignee"`
// Content is the raw description.
Content string `json:"content"` Content string `json:"content"`
// Date is the timestamp when the description was written.
Date time.Time `json:"date"` Date time.Time `json:"date"`
} }
// GetDescriptions returns a list of all descriptions stored in the database for the Claim.
func (c Claim) GetDescriptions() (res []ClaimDescription, err error) { func (c Claim) GetDescriptions() (res []ClaimDescription, err error) {
var rows *sql.Rows var rows *sql.Rows
if rows, err = DBQuery("SELECT id_description, id_assignee, content, date FROM claim_descriptions WHERE id_claim = ?", c.Id); err != nil { if rows, err = DBQuery("SELECT id_description, id_assignee, content, date FROM claim_descriptions WHERE id_claim = ?", c.Id); err != nil {
@ -153,6 +167,7 @@ func (c Claim) GetDescriptions() (res []ClaimDescription, err error) {
return return
} }
// AddDescription append in the database a new description; then returns the corresponding structure.
func (c Claim) AddDescription(content string, assignee ClaimAssignee) (ClaimDescription, error) { func (c Claim) AddDescription(content string, assignee ClaimAssignee) (ClaimDescription, error) {
if res, err := DBExec("INSERT INTO claim_descriptions (id_claim, id_assignee, content, date) VALUES (?, ?, ?, ?)", c.Id, assignee.Id, content, time.Now()); err != nil { if res, err := DBExec("INSERT INTO claim_descriptions (id_claim, id_assignee, content, date) VALUES (?, ?, ?, ?)", c.Id, assignee.Id, content, time.Now()); err != nil {
return ClaimDescription{}, err return ClaimDescription{}, err
@ -163,6 +178,7 @@ func (c Claim) AddDescription(content string, assignee ClaimAssignee) (ClaimDesc
} }
} }
// Update applies modifications back to the database
func (d ClaimDescription) Update() (int64, error) { func (d ClaimDescription) Update() (int64, error) {
if res, err := DBExec("UPDATE claim_descriptions SET id_assignee = ?, content = ?, date = ? WHERE id_description = ?", d.IdAssignee, d.Content, d.Date, d.Id); err != nil { if res, err := DBExec("UPDATE claim_descriptions SET id_assignee = ?, content = ?, date = ? WHERE id_description = ?", d.IdAssignee, d.Content, d.Date, d.Id); err != nil {
return 0, err return 0, err
@ -173,6 +189,7 @@ func (d ClaimDescription) Update() (int64, error) {
} }
} }
// Delete the description in the database.
func (d ClaimDescription) Delete() (int64, error) { func (d ClaimDescription) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM claim_descriptions WHERE id_description = ?", d.Id); err != nil { if res, err := DBExec("DELETE FROM claim_descriptions WHERE id_description = ?", d.Id); err != nil {
return 0, err return 0, err
@ -183,17 +200,19 @@ func (d ClaimDescription) Delete() (int64, error) {
} }
} }
// ClaimAssignee represents a user that can handle claims.
type ClaimAssignee struct { type ClaimAssignee struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} }
// GetAssignee retrieves an assignee from its identifier.
func GetAssignee(id int64) (a ClaimAssignee, err error) { func GetAssignee(id int64) (a ClaimAssignee, err error) {
err = DBQueryRow("SELECT id_assignee, name FROM claim_assignees WHERE id_assignee = ?", id).Scan(&a.Id, &a.Name) err = DBQueryRow("SELECT id_assignee, name FROM claim_assignees WHERE id_assignee = ?", id).Scan(&a.Id, &a.Name)
return return
} }
// GetAssignees returns a list of all assignees found in the database.
func GetAssignees() (res []ClaimAssignee, err error) { func GetAssignees() (res []ClaimAssignee, err error) {
var rows *sql.Rows var rows *sql.Rows
if rows, err = DBQuery("SELECT id_assignee, name FROM claim_assignees"); err != nil { if rows, err = DBQuery("SELECT id_assignee, name FROM claim_assignees"); err != nil {
@ -213,6 +232,7 @@ func GetAssignees() (res []ClaimAssignee, err error) {
return return
} }
// NewClaimAssignee creates and fills a new struct ClaimAssignee and registers it into the database.
func NewClaimAssignee(name string) (ClaimAssignee, error) { func NewClaimAssignee(name string) (ClaimAssignee, error) {
if res, err := DBExec("INSERT INTO claim_assignees (name) VALUES (?)", name); err != nil { if res, err := DBExec("INSERT INTO claim_assignees (name) VALUES (?)", name); err != nil {
return ClaimAssignee{}, err return ClaimAssignee{}, err
@ -223,6 +243,7 @@ func NewClaimAssignee(name string) (ClaimAssignee, error) {
} }
} }
// Update applies modifications back to the database
func (a ClaimAssignee) Update() (int64, error) { func (a ClaimAssignee) Update() (int64, error) {
if res, err := DBExec("UPDATE claim_assignees SET name = ? WHERE id_assignee = ?", a.Name, a.Id); err != nil { if res, err := DBExec("UPDATE claim_assignees SET name = ? WHERE id_assignee = ?", a.Name, a.Id); err != nil {
return 0, err return 0, err
@ -233,6 +254,7 @@ func (a ClaimAssignee) Update() (int64, error) {
} }
} }
// Delete the assignee in the database.
func (a ClaimAssignee) Delete() (int64, error) { func (a ClaimAssignee) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM claim_assignees WHERE id_assignee = ?", a.Id); err != nil { if res, err := DBExec("DELETE FROM claim_assignees WHERE id_assignee = ?", a.Id); err != nil {
return 0, err return 0, err
@ -243,6 +265,7 @@ func (a ClaimAssignee) Delete() (int64, error) {
} }
} }
// ClearAssignees removes all assignees from database.
func ClearAssignees() (int64, error) { func ClearAssignees() (int64, error) {
if res, err := DBExec("DELETE FROM claim_assignees"); err != nil { if res, err := DBExec("DELETE FROM claim_assignees"); err != nil {
return 0, err return 0, err
@ -253,6 +276,7 @@ func ClearAssignees() (int64, error) {
} }
} }
// GetAssignee returns the assignee assigned to the claim.
func (c Claim) GetAssignee() (*ClaimAssignee, error) { func (c Claim) GetAssignee() (*ClaimAssignee, error) {
if c.IdAssignee == nil { if c.IdAssignee == nil {
return nil, nil return nil, nil
@ -263,6 +287,7 @@ func (c Claim) GetAssignee() (*ClaimAssignee, error) {
} }
} }
// SetAssignee defines the assignee that'll handle the claim.
func (c Claim) SetAssignee(a ClaimAssignee) { func (c Claim) SetAssignee(a ClaimAssignee) {
c.IdAssignee = &a.Id c.IdAssignee = &a.Id
} }

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
) )
// ToURLid converts the given string to a valid URLid.
func ToURLid(str string) string { func ToURLid(str string) string {
re := regexp.MustCompile("[^a-zA-Z0-9]+") re := regexp.MustCompile("[^a-zA-Z0-9]+")
return strings.TrimSuffix(re.ReplaceAllLiteralString(str, "-"), "-") return strings.TrimSuffix(re.ReplaceAllLiteralString(str, "-"), "-")

View File

@ -1,3 +1,5 @@
// Package settings is shared across multiple services for easy parsing and
// retrieval of the challenge settings.
package settings package settings
import ( import (
@ -10,33 +12,50 @@ import (
"gopkg.in/fsnotify.v1" "gopkg.in/fsnotify.v1"
) )
// SettingsFile is the expected name of the file containing the settings.
const SettingsFile = "settings.json" const SettingsFile = "settings.json"
// SettingsDir is the relative location where the SettingsFile lies.
var SettingsDir string = "./SETTINGS" var SettingsDir string = "./SETTINGS"
// FICSettings represents the settings panel.
type FICSettings struct { type FICSettings struct {
// Title is the displayed name of the challenge.
Title string `json:"title"` Title string `json:"title"`
// Authors is the group name of people making the challenge.
Authors string `json:"authors"` Authors string `json:"authors"`
// Start is the departure time (expected or effective).
Start time.Time `json:"start"` Start time.Time `json:"start"`
// End is the expected end time.
End time.Time `json:"end"` End time.Time `json:"end"`
// Generation is a value used to regenerate static files.
Generation time.Time `json:"generation"` Generation time.Time `json:"generation"`
// FirstBlood is the coefficient applied to each first team who solve a challenge.
FirstBlood float64 `json:"firstBlood"` FirstBlood float64 `json:"firstBlood"`
// SubmissionCostBase is a complex number representing the cost of each attempts.
SubmissionCostBase float64 `json:"submissionCostBase"` SubmissionCostBase float64 `json:"submissionCostBase"`
// AllowRegistration permits unregistered Team to register themselves.
AllowRegistration bool `json:"allowRegistration"` AllowRegistration bool `json:"allowRegistration"`
// DenyNameChange disallow Team to change their name.
DenyNameChange bool `json:"denyNameChange"` DenyNameChange bool `json:"denyNameChange"`
// EnableResolutionRoute activates the route displaying resolution movies.
EnableResolutionRoute bool `json:"enableResolutionRoute"` EnableResolutionRoute bool `json:"enableResolutionRoute"`
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
PartialValidation bool `json:"partialValidation"` PartialValidation bool `json:"partialValidation"`
// EnableExerciceDepend don't show (or permit to solve) to team challenges they are not unlocked through dependancies.
EnableExerciceDepend bool `json:"enableExerciceDepend"` EnableExerciceDepend bool `json:"enableExerciceDepend"`
} }
// ExistsSettings checks if the settings file can by found at the given path.
func ExistsSettings(settingsPath string) bool { func ExistsSettings(settingsPath string) bool {
_, err := os.Stat(settingsPath) _, err := os.Stat(settingsPath)
return !os.IsNotExist(err) return !os.IsNotExist(err)
} }
// ReadSettings parses the file at the given location.
func ReadSettings(path string) (FICSettings, error) { func ReadSettings(path string) (FICSettings, error) {
var s FICSettings var s FICSettings
if fd, err := os.Open(path); err != nil { if fd, err := os.Open(path); err != nil {
@ -53,6 +72,7 @@ func ReadSettings(path string) (FICSettings, error) {
} }
} }
// SaveSettings saves settings at the given location.
func SaveSettings(path string, s FICSettings) error { func SaveSettings(path string, s FICSettings) error {
if fd, err := os.Create(path); err != nil { if fd, err := os.Create(path); err != nil {
return err return err
@ -68,6 +88,7 @@ func SaveSettings(path string, s FICSettings) error {
} }
} }
// ForceRegeneration makes a small change to the settings structure in order to force the regeneration of all static files.
func ForceRegeneration() error { func ForceRegeneration() error {
location := path.Join(SettingsDir, SettingsFile) location := path.Join(SettingsDir, SettingsFile)
if settings, err := ReadSettings(location); err != nil { if settings, err := ReadSettings(location); err != nil {
@ -78,6 +99,10 @@ func ForceRegeneration() error {
} }
} }
// LoadAndWatchSettings is the function you are looking for!
// Giving the location and a callback, this function will first call your reload function
// before returning (if the file can be parsed); then it starts watching modifications made to
// this file. Your callback is then run each time the file is modified.
func LoadAndWatchSettings(settingsPath string, reload func (FICSettings)) { func LoadAndWatchSettings(settingsPath string, reload func (FICSettings)) {
// First load of configuration if it exists // First load of configuration if it exists
if _, err := os.Stat(settingsPath); !os.IsNotExist(err) { if _, err := os.Stat(settingsPath); !os.IsNotExist(err) {