Use gin-gonic instead of httprouter

This commit is contained in:
nemunaire 2022-07-09 19:42:00 +02:00
parent 7c719d9fd5
commit a203cdc36a
22 changed files with 1668 additions and 1392 deletions

94
api.go Normal file
View File

@ -0,0 +1,94 @@
package main
import (
"encoding/base64"
"net/http"
"github.com/gin-gonic/gin"
)
func declareAPIRoutes(router *gin.Engine) {
apiRoutes := router.Group("/api")
apiRoutes.Use(authMiddleware())
declareAPIAuthRoutes(apiRoutes)
declareAPISurveysRoutes(apiRoutes)
declareAPIWorksRoutes(apiRoutes)
apiAuthRoutes := router.Group("/api")
apiAuthRoutes.Use(authMiddleware(loggedUser))
declareAPIAuthAsksRoutes(apiAuthRoutes)
declareAPIAuthQuestionsRoutes(apiAuthRoutes)
declareAPIAuthHelpRoutes(apiAuthRoutes)
declareAPIAuthSurveysRoutes(apiAuthRoutes)
declareAPIAuthUsersRoutes(apiAuthRoutes)
declareAPIAuthWorksRoutes(apiAuthRoutes)
apiAdminRoutes := router.Group("/api")
apiAdminRoutes.Use(authMiddleware(adminRestricted))
declareAPIAdminAsksRoutes(apiAdminRoutes)
declareAPIAuthGradesRoutes(apiAdminRoutes)
declareAPIAdminHelpRoutes(apiAdminRoutes)
declareAPIAdminQuestionsRoutes(apiAdminRoutes)
declareAPIAdminSurveysRoutes(apiAdminRoutes)
declareAPIAdminUsersRoutes(apiAdminRoutes)
declareAPIAdminWorksRoutes(apiAdminRoutes)
}
func loggedUser(u *User, c *gin.Context) bool {
if u != nil {
return true
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Permission Denied"})
return false
}
func adminRestricted(u *User, c *gin.Context) bool {
if u != nil && u.IsAdmin {
return true
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Permission Denied"})
return false
}
func authMiddleware(access ...func(*User, *gin.Context) bool) gin.HandlerFunc {
return func(c *gin.Context) {
var user *User = nil
if cookie, err := c.Request.Cookie("auth"); err == nil {
if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil {
eraseCookie(c)
c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{"errmsg": err.Error()})
return
} else if session, err := getSession(sessionid); err != nil {
eraseCookie(c)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": err.Error()})
return
} else if session.IdUser == nil {
user = nil
} else if std, err := getUser(int(*session.IdUser)); err != nil {
eraseCookie(c)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": err.Error()})
return
} else {
user = std
}
}
// Check access limitation
for _, a := range access {
if !a(user, c) {
return
}
}
// Retrieve corresponding user
c.Set("LoggedUser", user)
// We are now ready to continue
c.Next()
}
}

48
app.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"context"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
type App struct {
router *gin.Engine
srv *http.Server
}
func NewApp() App {
gin.ForceConsoleColor()
router := gin.Default()
declareStaticRoutes(router)
declareAPIRoutes(router)
app := App{
router: router,
}
return app
}
func (app *App) Start(bind string) {
app.srv = &http.Server{
Addr: bind,
Handler: app.router,
}
if err := app.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}
func (app *App) Stop() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
}

44
asks.go
View File

@ -1,32 +1,52 @@
package main
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func init() {
router.POST("/api/surveys/:sid/ask", apiAuthHandler(surveyAuthHandler(func(s Survey, u *User, body []byte) HTTPResponse {
func declareAPIAuthAsksRoutes(router *gin.RouterGroup) {
router.POST("/ask", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
s := c.MustGet("survey").(*Survey)
var ask Ask
if err := json.Unmarshal(body, &ask); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&ask); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
a, err := s.NewAsk(u.Id, ask.Content)
if err != nil {
return APIErrorResponse{err: err}
log.Println("Unable to NewAsk:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register your question. Please try again in a few moment."})
return
}
if s.Direct != nil {
s.WSAdminWriteAll(WSMessage{Action: "new_ask", UserId: &u.Id, QuestionId: &a.Id, Response: ask.Content})
}
return formatApiResponse(a, err)
}), loggedUser))
router.GET("/api/surveys/:sid/ask", apiHandler(surveyHandler(
func(s Survey, _ []byte) HTTPResponse {
return formatApiResponse(s.GetAsks(true))
}), adminRestricted))
c.JSON(http.StatusOK, a)
})
}
func declareAPIAdminAsksRoutes(router *gin.RouterGroup) {
router.GET("/ask", func(c *gin.Context) {
s := c.MustGet("survey").(*Survey)
asks, err := s.GetAsks(true)
if err != nil {
log.Println("Unable to GetAsks:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve asks. Please try again later."})
return
}
c.JSON(http.StatusOK, asks)
})
}
type Ask struct {

64
auth.go
View File

@ -2,12 +2,10 @@ package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
var LocalAuthFunc = checkAuthKrb5
@ -18,12 +16,12 @@ type loginForm struct {
Password string `json:"password"`
}
func init() {
router.GET("/api/auth", apiAuthHandler(validateAuthToken))
router.POST("/api/auth", apiRawHandler(func(w http.ResponseWriter, ps httprouter.Params, body []byte) HTTPResponse {
return formatApiResponse(LocalAuthFunc(w, ps, body))
}))
router.POST("/api/auth/logout", apiRawHandler(logout))
func declareAPIAuthRoutes(router *gin.RouterGroup) {
router.GET("/auth", validateAuthToken)
router.POST("/auth", func(c *gin.Context) {
LocalAuthFunc(c)
})
router.POST("/auth/logout", logout)
}
type authToken struct {
@ -31,20 +29,21 @@ type authToken struct {
CurrentPromo uint `json:"current_promo"`
}
func validateAuthToken(u *User, _ httprouter.Params, _ []byte) HTTPResponse {
if u == nil {
return APIErrorResponse{status: http.StatusUnauthorized, err: fmt.Errorf("Not connected")}
func validateAuthToken(c *gin.Context) {
if u, ok := c.Get("LoggedUser"); !ok || u.(*User) == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Not connected"})
return
} else {
return APIResponse{authToken{u, currentPromo}}
c.JSON(http.StatusOK, authToken{u.(*User), currentPromo})
}
}
func logout(w http.ResponseWriter, ps httprouter.Params, body []byte) HTTPResponse {
eraseCookie(w)
return APIResponse{true}
func logout(c *gin.Context) {
eraseCookie(c)
c.JSON(http.StatusOK, true)
}
func completeAuth(w http.ResponseWriter, username string, email string, firstname string, lastname string, groups string, session *Session) (usr User, err error) {
func completeAuth(c *gin.Context, username string, email string, firstname string, lastname string, groups string, session *Session) (usr *User, err error) {
if !userExists(username) {
if usr, err = NewUser(username, email, firstname, lastname, groups); err != nil {
return
@ -64,9 +63,7 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
}
if session == nil {
var s Session
s, err = usr.NewSession()
session = &s
session, err = usr.NewSession()
} else {
_, err = session.SetUser(usr)
}
@ -75,7 +72,7 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
return
}
http.SetCookie(w, &http.Cookie{
http.SetCookie(c.Writer, &http.Cookie{
Name: "auth",
Value: base64.StdEncoding.EncodeToString(session.Id),
Path: baseURL + "/",
@ -88,11 +85,28 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
return
}
func dummyAuth(w http.ResponseWriter, _ httprouter.Params, body []byte) (interface{}, error) {
func eraseCookie(c *gin.Context) {
http.SetCookie(c.Writer, &http.Cookie{
Name: "auth",
Value: "",
Path: baseURL + "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
}
func dummyAuth(c *gin.Context) {
var lf map[string]string
if err := json.Unmarshal(body, &lf); err != nil {
return nil, err
if err := c.ShouldBindJSON(&lf); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
return completeAuth(w, lf["username"], lf["email"], lf["firstname"], lf["lastname"], "", nil)
if usr, err := completeAuth(c, lf["username"], lf["email"], lf["firstname"], lf["lastname"], "", nil); err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": err.Error()})
return
} else {
c.JSON(http.StatusOK, authToken{usr, currentPromo})
}
}

View File

@ -1,17 +1,15 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/krberror"
"github.com/julienschmidt/httprouter"
)
func parseETypes(s []string, w bool) []int32 {
@ -37,10 +35,11 @@ func parseETypes(s []string, w bool) []int32 {
return eti
}
func checkAuthKrb5(w http.ResponseWriter, _ httprouter.Params, body []byte) (interface{}, error) {
func checkAuthKrb5(c *gin.Context) {
var lf loginForm
if err := json.Unmarshal(body, &lf); err != nil {
return nil, err
if err := c.ShouldBindJSON(&lf); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
found := false
@ -52,7 +51,8 @@ func checkAuthKrb5(w http.ResponseWriter, _ httprouter.Params, body []byte) (int
}
if !userExists(lf.Login) && !found {
return nil, fmt.Errorf("You are not allowed to log you in this way. Please use OpenID Connect.")
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "You are not allowed to log you in this way. Please use OpenID Connect."})
return
}
cnf := config.New()
@ -62,17 +62,21 @@ func checkAuthKrb5(w http.ResponseWriter, _ httprouter.Params, body []byte) (int
cnf.LibDefaults.DefaultTktEnctypeIDs = parseETypes(cnf.LibDefaults.DefaultTktEnctypes, cnf.LibDefaults.AllowWeakCrypto)
cnf.LibDefaults.PermittedEnctypeIDs = parseETypes(cnf.LibDefaults.PermittedEnctypes, cnf.LibDefaults.AllowWeakCrypto)
c := client.NewWithPassword(lf.Login, "CRI.EPITA.FR", lf.Password, cnf)
if err := c.Login(); err != nil {
cl := client.NewWithPassword(lf.Login, "CRI.EPITA.FR", lf.Password, cnf)
if err := cl.Login(); err != nil {
if errk, ok := err.(krberror.Krberror); ok {
if errk.RootCause == krberror.NetworkingError {
return nil, errors.New(`{"status": "Authentication system unavailable, please retry."}`)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Authentication system unavailable, please retry."})
return
} else if errk.RootCause == krberror.KDCError {
return nil, errors.New(`{"status": "Invalid username or password"}`)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Invalid username or password"})
return
}
}
return nil, err
} else {
return completeAuth(w, lf.Login, lf.Login+"@epita.fr", "", "", "", nil)
log.Println("Unable to login through Kerberos: unknown error:", err)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Invalid credentials (unknown error)."})
return
}
completeAuth(c, lf.Login, lf.Login+"@epita.fr", "", "", "", nil)
}

View File

@ -11,7 +11,7 @@ import (
"golang.org/x/oauth2"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
var (
@ -27,12 +27,12 @@ func init() {
flag.StringVar(&oidcClientID, "oidc-clientid", oidcClientID, "ClientID for OIDC")
flag.StringVar(&oidcSecret, "oidc-secret", oidcSecret, "Secret for OIDC")
flag.StringVar(&oidcRedirectURL, "oidc-redirect", oidcRedirectURL, "Base URL for the redirect after connection")
router.GET("/auth/CRI", redirectOIDC_CRI)
router.GET("/auth/complete", OIDC_CRI_complete)
}
func initializeOIDC() {
func initializeOIDC(router *gin.Engine) {
router.GET("/auth/CRI", redirectOIDC_CRI)
router.GET("/auth/complete", OIDC_CRI_complete)
if oidcClientID != "" && oidcSecret != "" {
provider, err := oidc.NewProvider(context.Background(), "https://cri.epita.fr")
if err != nil {
@ -59,47 +59,48 @@ func initializeOIDC() {
}
func redirectOIDC_CRI(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
func redirectOIDC_CRI(c *gin.Context) {
session, err := NewSession()
// Save next parameter
if len(r.URL.Query().Get("next")) > 0 {
nextSessionMap[fmt.Sprintf("%x", session.Id)] = r.URL.Query().Get("next")
if len(c.Request.URL.Query().Get("next")) > 0 {
nextSessionMap[fmt.Sprintf("%x", session.Id)] = c.Request.URL.Query().Get("next")
}
if err != nil {
http.Error(w, fmt.Sprintf("{'errmsg':%q}", err.Error()), http.StatusInternalServerError)
} else {
http.Redirect(w, r, oauth2Config.AuthCodeURL(hex.EncodeToString(session.Id)), http.StatusFound)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.Redirect(http.StatusFound, oauth2Config.AuthCodeURL(hex.EncodeToString(session.Id)))
}
func OIDC_CRI_complete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
idsession, err := hex.DecodeString(r.URL.Query().Get("state"))
func OIDC_CRI_complete(c *gin.Context) {
idsession, err := hex.DecodeString(c.Request.URL.Query().Get("state"))
if err != nil {
http.Error(w, fmt.Sprintf("{'errmsg':%q}", err.Error()), http.StatusBadRequest)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
session, err := getSession(idsession)
if err != nil {
http.Error(w, fmt.Sprintf("{'errmsg':%q}", err.Error()), http.StatusBadRequest)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
oauth2Token, err := oauth2Config.Exchange(context.Background(), r.URL.Query().Get("code"))
oauth2Token, err := oauth2Config.Exchange(context.Background(), c.Request.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to exchange token: " + err.Error()})
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "No id_token field in oauth2 token."})
return
}
idToken, err := oidcVerifier.Verify(context.Background(), rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to verify ID Token: " + err.Error()})
return
}
@ -112,7 +113,7 @@ func OIDC_CRI_complete(w http.ResponseWriter, r *http.Request, ps httprouter.Par
Groups []map[string]interface{} `json:"groups"`
}
if err := idToken.Claims(&claims); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
@ -123,17 +124,17 @@ func OIDC_CRI_complete(w http.ResponseWriter, r *http.Request, ps httprouter.Par
}
}
if _, err := completeAuth(w, claims.Username, claims.Email, claims.Firstname, claims.Lastname, groups, &session); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := completeAuth(c, claims.Username, claims.Email, claims.Firstname, claims.Lastname, groups, session); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
// Retrieve next URL associated with session
if next, ok := nextSessionMap[fmt.Sprintf("%x", session.Id)]; ok {
http.Redirect(w, r, next, http.StatusFound)
c.Redirect(http.StatusFound, next)
delete(nextSessionMap, fmt.Sprintf("%x", session.Id))
} else {
http.Redirect(w, r, "/", http.StatusFound)
c.Redirect(http.StatusFound, "/")
}
}

View File

@ -1,160 +1,173 @@
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/surveys/:sid/questions/:qid/corrections", apiHandler(questionHandler(
func(q Question, _ []byte) HTTPResponse {
if cts, err := q.GetCorrectionTemplates(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{cts}
}
}), adminRestricted))
router.POST("/api/surveys/:sid/questions/:qid/corrections", apiHandler(questionHandler(func(q Question, body []byte) HTTPResponse {
var new CorrectionTemplate
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
func declareAPIAdminCorrectionsRoutes(router *gin.RouterGroup) {
router.GET("/corrections", func(c *gin.Context) {
q := c.MustGet("question").(*Question)
cts, err := q.GetCorrectionTemplates()
if err != nil {
log.Println("Unable to GetCorrectionTemplates:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrive correction's templates"})
return
}
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination))
}), adminRestricted))
c.JSON(http.StatusOK, cts)
})
router.POST("/corrections", func(c *gin.Context) {
q := c.MustGet("question").(*Question)
router.GET("/api/surveys/:sid/questions/:qid/corrections/:cid", apiHandler(correctionHandler(
func(ct CorrectionTemplate, _ []byte) HTTPResponse {
if users, err := ct.GetUserCorrected(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{users}
}
}), adminRestricted))
router.PUT("/api/surveys/:sid/questions/:qid/corrections/:cid", apiHandler(correctionHandler(func(current CorrectionTemplate, body []byte) HTTPResponse {
var new CorrectionTemplate
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ct, err := q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination)
if err != nil {
log.Println("Unable to NewCorrectionTemplate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to insert new correction template."})
return
}
c.JSON(http.StatusOK, ct)
})
correctionsRoutes := router.Group("/corrections/:cid")
correctionsRoutes.Use(correctionHandler)
correctionsRoutes.GET("", func(c *gin.Context) {
ct := c.MustGet("correctiontemplate").(*CorrectionTemplate)
users, err := ct.GetUserCorrected()
if err != nil {
log.Println("Unable to GetUserCorrected:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve users' corrections"})
return
}
c.JSON(http.StatusOK, users)
})
correctionsRoutes.PUT("", func(c *gin.Context) {
current := c.MustGet("correctiontemplate").(*CorrectionTemplate)
var new CorrectionTemplate
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
new.Id = current.Id
if err := new.Update(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{new}
}
}), adminRestricted))
router.DELETE("/api/surveys/:sid/questions/:qid/corrections/:cid", apiHandler(correctionHandler(func(ct CorrectionTemplate, body []byte) HTTPResponse {
return formatApiResponse(ct.Delete())
}), adminRestricted))
router.GET("/api/questions/:qid/corrections", apiHandler(questionHandler(
func(q Question, _ []byte) HTTPResponse {
if cts, err := q.GetCorrectionTemplates(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{cts}
}
}), adminRestricted))
router.POST("/api/questions/:qid/corrections", apiHandler(questionHandler(func(q Question, body []byte) HTTPResponse {
var new CorrectionTemplate
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
log.Println("Unable to Update correctionTemplate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update the correction template"})
return
}
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination))
}), adminRestricted))
c.JSON(http.StatusOK, new)
})
correctionsRoutes.DELETE("", func(c *gin.Context) {
ct := c.MustGet("correctiontemplate").(*CorrectionTemplate)
router.GET("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(
func(ct CorrectionTemplate, _ []byte) HTTPResponse {
if users, err := ct.GetUserCorrected(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{users}
}
}), adminRestricted))
router.PUT("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(func(current CorrectionTemplate, body []byte) HTTPResponse {
var new CorrectionTemplate
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if _, err := ct.Delete(); err != nil {
log.Println("Unable to Delete correctionTemplate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete the correction template."})
return
}
new.Id = current.Id
if err := new.Update(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{new}
}
}), adminRestricted))
router.DELETE("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(func(ct CorrectionTemplate, body []byte) HTTPResponse {
return formatApiResponse(ct.Delete())
}), adminRestricted))
c.JSON(http.StatusOK, nil)
})
}
router.GET("/api/users/:uid/questions/:qid", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return userHandler(func(u User, _ []byte) HTTPResponse {
if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err != nil {
return APIErrorResponse{err: err}
} else if question, err := getQuestion(qid); err != nil {
return APIErrorResponse{err: err}
} else {
return formatApiResponse(question.ComputeScoreQuestion(&u))
}
})(ps, body)
}, adminRestricted))
func declareAPIAdminUserCorrectionsRoutes(router *gin.RouterGroup) {
router.GET("/corrections", func(c *gin.Context) {
user := c.MustGet("user").(*User)
corrections, err := user.GetCorrections()
if err != nil {
log.Printf("Unable to GetCorrections(uid=%d): %s", user.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve corrections."})
return
}
c.JSON(http.StatusOK, corrections)
})
router.POST("/corrections", func(c *gin.Context) {
user := c.MustGet("user").(*User)
router.GET("/api/users/:uid/corrections", apiHandler(userHandler(
func(u User, _ []byte) HTTPResponse {
return formatApiResponse(u.GetCorrections())
}), adminRestricted))
router.POST("/api/users/:uid/corrections", apiHandler(userHandler(func(u User, body []byte) HTTPResponse {
var new UserCorrection
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
return formatApiResponse(u.NewCorrection(new.IdTemplate))
}), adminRestricted))
router.PUT("/api/users/:uid/corrections", apiHandler(userHandler(func(u User, body []byte) HTTPResponse {
correction, err := user.NewCorrection(new.IdTemplate)
if err != nil {
log.Printf("Unable to NewCorrection(uid=%d): %s", user.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to insert the new correction."})
return
}
c.JSON(http.StatusOK, correction)
})
router.PUT("/corrections", func(c *gin.Context) {
user := c.MustGet("user").(*User)
var new map[int64]bool
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
return formatApiResponse(u.EraseCorrections(new))
}), adminRestricted))
router.DELETE("/api/users/:uid/corrections/:cid", apiHandler(userCorrectionHandler(func(u User, uc UserCorrection, body []byte) HTTPResponse {
return formatApiResponse(uc.Delete(u))
}), adminRestricted))
correction, err := user.EraseCorrections(new)
if err != nil {
log.Printf("Unable to EraseCorrections(uid=%d): %s", user.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to erase the correction."})
return
}
c.JSON(http.StatusOK, correction)
})
correctionsRoutes := router.Group("/corrections/:cid")
correctionsRoutes.Use(userCorrectionHandler)
correctionsRoutes.DELETE("", func(c *gin.Context) {
user := c.MustGet("user").(*User)
uc := c.MustGet("correction").(*UserCorrection)
if _, err := uc.Delete(user); err != nil {
log.Printf("Unable to Delete(uid=%d, cid=%d) user correction: %s", user.Id, uc.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to delete this correction."})
return
}
c.JSON(http.StatusOK, nil)
})
}
func correctionHandler(f func(CorrectionTemplate, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
return questionHandler(func(q Question, body []byte) HTTPResponse {
if cid, err := strconv.Atoi(string(ps.ByName("cid"))); err != nil {
return APIErrorResponse{err: err}
func correctionHandler(c *gin.Context) {
q := c.MustGet("question").(*Question)
if cid, err := strconv.Atoi(string(c.Param("cid"))); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid correction id"})
return
} else if correction, err := q.GetCorrectionTemplate(cid); err != nil {
return APIErrorResponse{err: err}
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Correction not found"})
return
} else {
return f(correction, body)
}
})(ps, body)
}
}
c.Set("correctiontemplate", correction)
func userCorrectionHandler(f func(User, UserCorrection, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
return userHandler(func(u User, body []byte) HTTPResponse {
if cid, err := strconv.Atoi(string(ps.ByName("cid"))); err != nil {
return APIErrorResponse{err: err}
} else if correction, err := u.GetCorrection(cid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(u, correction, body)
}
})(ps, body)
c.Next()
}
}
@ -167,7 +180,7 @@ type CorrectionTemplate struct {
ScoreExplaination string `json:"score_explaination,omitempty"`
}
func (q *Question) GetCorrectionTemplates() (ct []CorrectionTemplate, err error) {
func (q *Question) GetCorrectionTemplates() (ct []*CorrectionTemplate, err error) {
if rows, errr := DBQuery("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_question=?", q.Id); errr != nil {
return nil, errr
} else {
@ -178,7 +191,7 @@ func (q *Question) GetCorrectionTemplates() (ct []CorrectionTemplate, err error)
if err = rows.Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination); err != nil {
return
}
ct = append(ct, c)
ct = append(ct, &c)
}
if err = rows.Err(); err != nil {
return
@ -188,23 +201,25 @@ func (q *Question) GetCorrectionTemplates() (ct []CorrectionTemplate, err error)
}
}
func (q *Question) GetCorrectionTemplate(id int) (c CorrectionTemplate, err error) {
func (q *Question) GetCorrectionTemplate(id int) (c *CorrectionTemplate, err error) {
c = new(CorrectionTemplate)
err = DBQueryRow("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_question=? AND id_template=?", q.Id, id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination)
return
}
func GetCorrectionTemplate(id int64) (c CorrectionTemplate, err error) {
func GetCorrectionTemplate(id int64) (c *CorrectionTemplate, err error) {
c = new(CorrectionTemplate)
err = DBQueryRow("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_template=?", id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination)
return
}
func (q *Question) NewCorrectionTemplate(label string, regexp string, score int, score_explaination string) (CorrectionTemplate, error) {
func (q *Question) NewCorrectionTemplate(label string, regexp string, score int, score_explaination string) (*CorrectionTemplate, error) {
if res, err := DBExec("INSERT INTO correction_templates (id_question, label, re, score, score_explanation) VALUES (?, ?, ?, ?, ?)", q.Id, label, regexp, score, score_explaination); err != nil {
return CorrectionTemplate{}, err
return nil, err
} else if cid, err := res.LastInsertId(); err != nil {
return CorrectionTemplate{}, err
return nil, err
} else {
return CorrectionTemplate{cid, q.Id, label, regexp, score, score_explaination}, nil
return &CorrectionTemplate{cid, q.Id, label, regexp, score, score_explaination}, nil
}
}
@ -223,7 +238,7 @@ func (t *CorrectionTemplate) Delete() (int64, error) {
}
}
func (t *CorrectionTemplate) GetUserCorrected() (ucs []UserCorrection, err error) {
func (t *CorrectionTemplate) GetUserCorrected() (ucs []*UserCorrection, err error) {
if rows, errr := DBQuery("SELECT id_correction, id_user, id_template FROM student_corrected WHERE id_template=?", t.Id); errr != nil {
return nil, errr
} else {
@ -234,7 +249,7 @@ func (t *CorrectionTemplate) GetUserCorrected() (ucs []UserCorrection, err error
if err = rows.Scan(&c.Id, &c.IdUser, &c.IdTemplate); err != nil {
return
}
ucs = append(ucs, c)
ucs = append(ucs, &c)
}
if err = rows.Err(); err != nil {
return
@ -260,6 +275,22 @@ type UserCorrection struct {
IdTemplate int64 `json:"id_template"`
}
func userCorrectionHandler(c *gin.Context) {
u := c.MustGet("user").(*User)
if cid, err := strconv.Atoi(string(c.Param("cid"))); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid correction id"})
return
} else if correction, err := u.GetCorrection(cid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Correction not found"})
return
} else {
c.Set("correction", correction)
c.Next()
}
}
func (u *User) GetCorrections() (uc []UserCorrection, err error) {
if rows, errr := DBQuery("SELECT id_correction, id_template FROM student_corrected WHERE id_user=?", u.Id); errr != nil {
return nil, errr
@ -302,7 +333,8 @@ func (u *User) GetCorrectionsTemplate() (tpls []int64, err error) {
}
}
func (u *User) GetCorrection(id int) (c UserCorrection, err error) {
func (u *User) GetCorrection(id int) (c *UserCorrection, err error) {
c = new(UserCorrection)
err = DBQueryRow("SELECT id_correction, id_template FROM student_corrected WHERE id_user=? AND id_correction=?", u.Id, id).Scan(&c.Id, &c.IdTemplate)
return
}
@ -340,7 +372,7 @@ func (u *User) EraseCorrections(ids map[int64]bool) (*UserCorrectionSummary, err
}
}
func (c *UserCorrection) Delete(u User) (*UserCorrectionSummary, error) {
func (c *UserCorrection) Delete(u *User) (*UserCorrectionSummary, error) {
if res, err := DBExec("DELETE FROM student_corrected WHERE id_correction = ?", c.Id); err != nil {
return nil, err
} else if _, err := res.RowsAffected(); err != nil {
@ -374,7 +406,7 @@ func (q *Question) ComputeScoreQuestion(u *User) (*UserCorrectionSummary, error)
} else if corrections, err := u.GetCorrectionsTemplate(); err != nil {
return nil, err
} else {
tpls := map[int64]CorrectionTemplate{}
tpls := map[int64]*CorrectionTemplate{}
for _, tpl := range templates {
tpls[tpl.Id] = tpl
}

View File

@ -4,11 +4,10 @@ import (
"context"
"log"
"net/http"
"strconv"
"sync"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
)
@ -21,15 +20,18 @@ var (
WSAdminMutex = sync.RWMutex{}
)
func init() {
router.GET("/api/surveys/:sid/ws", rawAuthHandler(SurveyWS, loggedUser))
router.GET("/api/surveys/:sid/ws-admin", rawAuthHandler(SurveyWSAdmin, adminRestricted))
func declareAPIAuthDirectRoutes(router *gin.RouterGroup) {
router.GET("/ws", SurveyWS)
}
router.GET("/api/surveys/:sid/ws/stats", apiHandler(surveyHandler(func(s Survey, body []byte) HTTPResponse {
return APIResponse{
WSSurveyStats(s.Id),
}
}), adminRestricted))
func declareAPIAdminDirectRoutes(router *gin.RouterGroup) {
router.GET("/ws-admin", SurveyWSAdmin)
router.GET("/ws/stats", func(c *gin.Context) {
s := c.MustGet("survey").(*Survey)
c.JSON(http.StatusOK, WSSurveyStats(s.Id))
})
}
func WSSurveyStats(sid int64) map[string]interface{} {
@ -104,35 +106,32 @@ func msgCurrentState(survey *Survey) (msg WSMessage) {
return
}
func SurveyWS(w http.ResponseWriter, r *http.Request, ps httprouter.Params, u *User, body []byte) {
if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err != nil {
http.Error(w, "{\"errmsg\": \"Invalid survey identifier\"}", http.StatusBadRequest)
func SurveyWS(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
survey := c.MustGet("survey").(*Survey)
if survey.Direct == nil {
c.AbortWithStatusJSON(http.StatusPaymentRequired, gin.H{"errmsg": "Not a live survey"})
return
} else if survey, err := getSurvey(sid); err != nil {
http.Error(w, "{\"errmsg\": \"Survey not found\"}", http.StatusNotFound)
return
} else if survey.Direct == nil {
http.Error(w, "{\"errmsg\": \"Not a direct survey\"}", http.StatusBadRequest)
return
} else {
ws, err := websocket.Accept(w, r, nil)
}
ws, err := websocket.Accept(c.Writer, c.Request, nil)
if err != nil {
log.Fatal("error get connection", err)
}
log.Println(u.Login, "is now connected to WS", sid)
log.Println(u.Login, "is now connected to WS", survey.Id)
c := make(chan WSMessage, 1)
ch := make(chan WSMessage, 1)
WSClientsMutex.Lock()
defer WSClientsMutex.Unlock()
WSClients[survey.Id] = append(WSClients[survey.Id], WSClient{ws, c, u, survey.Id})
WSClients[survey.Id] = append(WSClients[survey.Id], WSClient{ws, ch, u, survey.Id})
// Send current state
c <- msgCurrentState(&survey)
ch <- msgCurrentState(survey)
go SurveyWS_run(ws, c, survey.Id, u)
}
go SurveyWS_run(ws, ch, survey.Id, u)
}
func WSWriteAll(message WSMessage) {
@ -230,34 +229,32 @@ loopadmin:
log.Println(u.Login, "admin disconnected")
}
func SurveyWSAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params, u *User, body []byte) {
if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err != nil {
http.Error(w, "{\"errmsg\": \"Invalid survey identifier\"}", http.StatusBadRequest)
func SurveyWSAdmin(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
survey := c.MustGet("survey").(*Survey)
if survey.Direct == nil {
c.AbortWithStatusJSON(http.StatusPaymentRequired, gin.H{"errmsg": "Not a live survey"})
return
} else if survey, err := getSurvey(sid); err != nil {
http.Error(w, "{\"errmsg\": \"Survey not found\"}", http.StatusNotFound)
return
} else if survey.Direct == nil {
http.Error(w, "{\"errmsg\": \"Not a direct survey\"}", http.StatusBadRequest)
return
} else {
ws, err := websocket.Accept(w, r, nil)
}
ws, err := websocket.Accept(c.Writer, c.Request, nil)
if err != nil {
log.Fatal("error get connection", err)
}
log.Println(u.Login, "is now connected to WS-admin", sid)
log.Println(u.Login, "is now connected to WS-admin", survey.Id)
c := make(chan WSMessage, 2)
ch := make(chan WSMessage, 2)
WSAdminMutex.Lock()
defer WSAdminMutex.Unlock()
WSAdmin = append(WSAdmin, WSClient{ws, c, u, survey.Id})
WSAdmin = append(WSAdmin, WSClient{ws, ch, u, survey.Id})
// Send current state
c <- msgCurrentState(&survey)
ch <- msgCurrentState(survey)
go SurveyWSAdmin_run(r.Context(), ws, c, survey.Id, u)
go SurveyWSAdmin_run(c.Request.Context(), ws, ch, survey.Id, u)
go func(c chan WSMessage, sid int) {
var v WSMessage
var err error
@ -381,8 +378,7 @@ func SurveyWSAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params,
log.Println("Unknown admin action:", v.Action)
}
}
}(c, sid)
}
}(ch, int(survey.Id))
}
func WSAdminWriteAll(message WSMessage) {

3
go.mod
View File

@ -4,13 +4,12 @@ go 1.16
require (
github.com/coreos/go-oidc/v3 v3.2.0
github.com/gin-gonic/gin v1.7.7
github.com/go-sql-driver/mysql v1.6.0
github.com/golang/protobuf v1.5.2 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.3
github.com/julienschmidt/httprouter v1.3.0
github.com/russross/blackfriday/v2 v2.1.0
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
nhooyr.io/websocket v1.8.7
)

42
go.sum
View File

@ -74,8 +74,6 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-oidc/v3 v3.1.0 h1:6avEvcdvTa1qYsOZ6I5PRkSYHzpTNWgKYmaJfaYbrRw=
github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
github.com/coreos/go-oidc/v3 v3.2.0 h1:2eR2MGR7thBXSQ2YbODlF0fcmgtliLCfr9iX6RW11fc=
github.com/coreos/go-oidc/v3 v3.2.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -94,18 +92,21 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
@ -158,10 +159,10 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@ -198,7 +199,6 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@ -210,14 +210,10 @@ github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFK
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/gokrb5/v8 v8.4.3 h1:iTonLeSJOn7MVUtyMT+arAn5AKAPrkilzhGw8wE/Tq8=
github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
@ -231,8 +227,10 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
@ -255,10 +253,10 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
@ -282,9 +280,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -359,14 +354,12 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220725212005-46097bf591d3 h1:2yWTtPWWRcISTw3/o+s/Y4UOMnQL71DWyToOANFusCg=
golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
@ -388,18 +381,8 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb h1:8tDJ3aechhddbdPAxpycgXHJRMLpk/Ab+aa4OgdN5/g=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 h1:+jnHzr9VPj32ykQVai5DNahi9+NSp7yYuCsl5eAQtL0=
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 h1:VnGaRqoLmqZH/3TMLJwYCEWkR4j1nuIU1U9TvbqsDUw=
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 h1:oVlhw3Oe+1reYsE2Nqu19PDJfLzwdU3QUUrG86rLK68=
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c h1:q3gFqPqH7NVofKo3c3yETAP//pPI+G5mvB7qqj1Y5kY=
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -463,7 +446,6 @@ golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -474,6 +456,7 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -543,7 +526,6 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
@ -716,12 +698,12 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
@ -731,8 +713,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,58 +1,68 @@
package main
import (
"errors"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/users/:uid/surveys/:sid/grades", apiAuthHandler(func(uauth *User, ps httprouter.Params, body []byte) HTTPResponse {
return surveyAuthHandler(func(s Survey, uauth *User, _ []byte) HTTPResponse {
return userHandler(func(u User, _ []byte) HTTPResponse {
if uauth != nil && ((s.Shown && u.Id == uauth.Id) || uauth.IsAdmin) {
if score, err := s.GetUserGrades(&u); err != nil {
return APIErrorResponse{err: err}
} else if score == nil {
return APIResponse{"N/A"}
func declareAPIAuthGradesRoutes(router *gin.RouterGroup) {
router.GET("/grades", func(c *gin.Context) {
uauth := c.MustGet("LoggedUser").(*User)
if survey, ok := c.Get("survey"); !ok {
if uauth == nil || !uauth.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
return
}
grades, err := GetAllGrades()
if err != nil {
log.Println("Unable to GetAllGrades:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve grades."})
return
}
c.JSON(http.StatusOK, grades)
} else {
return APIResponse{score}
s := survey.(*Survey)
if user, ok := c.Get("user"); ok {
u := user.(*User)
if uauth == nil || !((s.Shown && u.Id == uauth.Id) || uauth.IsAdmin) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not accessible"})
return
}
score, err := s.GetUserGrades(u)
if err != nil {
log.Printf("Unable to GetUserGrades(sid=%d; uid=%d): %s", s.Id, u.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrive user grade. Please try again later."})
return
}
if score == nil {
c.JSON(http.StatusOK, "N/A")
} else {
return APIErrorResponse{
status: http.StatusForbidden,
err: errors.New("Not accessible"),
c.JSON(http.StatusOK, score)
}
} else if uauth.IsAdmin {
scores, err := s.GetGrades()
if err != nil {
log.Printf("Unable to GetGrades(sid=%d): %s", s.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrive grades."})
return
}
})(ps, body)
})(uauth, ps, body)
}, loggedUser))
router.GET("/api/surveys/:sid/grades", apiAuthHandler(surveyAuthHandler(func(s Survey, uauth *User, _ []byte) HTTPResponse {
if scores, err := s.GetGrades(); err != nil {
return APIErrorResponse{err: err}
} else if scores == nil {
return APIResponse{"N/A"}
c.JSON(http.StatusOK, scores)
} else {
return APIResponse{scores}
}
}), adminRestricted))
router.GET("/api/grades", apiAuthHandler(func(uauth *User, ps httprouter.Params, body []byte) HTTPResponse {
if uauth != nil && uauth.IsAdmin {
if score, err := GetAllGrades(); err != nil {
return APIErrorResponse{err: err}
} else if score == nil {
return APIResponse{"N/A"}
} else {
return APIResponse{score}
}
} else {
return APIErrorResponse{
status: http.StatusForbidden,
err: errors.New("Not accessible"),
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
return
}
}
}, adminRestricted))
})
}
func GetAllGrades() (scores map[int64]map[int64]*float64, err error) {

View File

@ -1,227 +0,0 @@
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
)
var router = httprouter.New()
func Router() *httprouter.Router {
return router
}
type HTTPResponse interface {
WriteResponse(http.ResponseWriter)
}
type APIResponse struct {
response interface{}
}
func (r APIResponse) WriteResponse(w http.ResponseWriter) {
if str, found := r.response.(string); found {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
io.WriteString(w, str)
} else if bts, found := r.response.([]byte); found {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment")
w.Header().Set("Content-Transfer-Encoding", "binary")
w.WriteHeader(http.StatusOK)
w.Write(bts)
} else if j, err := json.Marshal(r.response); err != nil {
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", err.Error()), http.StatusInternalServerError)
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(j)
}
}
type APIErrorResponse struct {
status int
err error
}
func (r APIErrorResponse) WriteResponse(w http.ResponseWriter) {
if r.status == 0 {
r.status = http.StatusBadRequest
}
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", r.err.Error()), r.status)
}
type DispatchFunction func(httprouter.Params, []byte) HTTPResponse
func eraseCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: "",
Path: baseURL + "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
}
func rawHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, []byte), access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return rawAuthHandler(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params, _ *User, body []byte) {
f(w, r, ps, body)
}, access...)
}
func rawAuthHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, *User, []byte), access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr
}
log.Printf("%s \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent())
// Read Authorization header
var user *User = nil
if cookie, err := r.Cookie("auth"); err == nil {
if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil {
eraseCookie(w)
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusNotAcceptable)
return
} else if session, err := getSession(sessionid); err != nil {
eraseCookie(w)
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusUnauthorized)
return
} else if session.IdUser == nil {
user = nil
} else if std, err := getUser(int(*session.IdUser)); err != nil {
eraseCookie(w)
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusUnauthorized)
return
} else {
user = &std
}
}
// Check access limitation
for _, a := range access {
if err := a(user, r); err != nil {
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"errmsg":%q}`, err.err.Error()), http.StatusForbidden)
return
}
}
// Read the body
if r.ContentLength < 0 || r.ContentLength > 6553600 {
w.Header().Set("Content-Type", "application/json")
http.Error(w, "{errmsg:\"Request too large or request size unknown\"}", http.StatusRequestEntityTooLarge)
return
}
var body []byte
if r.ContentLength > 0 {
tmp := make([]byte, 1024)
for {
n, err := r.Body.Read(tmp)
for j := 0; j < n; j++ {
body = append(body, tmp[j])
}
if err != nil || n <= 0 {
break
}
}
}
f(w, r, ps, user, body)
}
}
func formatResponseHandler(f func(*http.Request, httprouter.Params, []byte) HTTPResponse) func(http.ResponseWriter, *http.Request, httprouter.Params, []byte) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params, body []byte) {
f(r, ps, body).WriteResponse(w)
}
}
func apiRawHandler(f func(http.ResponseWriter, httprouter.Params, []byte) HTTPResponse, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return rawHandler(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params, b []byte) {
formatResponseHandler(func(_ *http.Request, ps httprouter.Params, b []byte) HTTPResponse {
return f(w, ps, b)
})(w, r, ps, b)
}, access...)
}
func apiHandler(f DispatchFunction, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return rawHandler(formatResponseHandler(func(_ *http.Request, ps httprouter.Params, b []byte) HTTPResponse { return f(ps, b) }), access...)
}
func formatApiResponse(i interface{}, err error) HTTPResponse {
if err != nil {
return APIErrorResponse{
status: http.StatusBadRequest,
err: err,
}
} else {
return APIResponse{i}
}
}
func apiAuthHandler(f func(*User, httprouter.Params, []byte) HTTPResponse, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return rawHandler(formatResponseHandler(func(r *http.Request, ps httprouter.Params, b []byte) HTTPResponse {
if cookie, err := r.Cookie("auth"); err != nil {
return f(nil, ps, b)
} else if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil {
return APIErrorResponse{
status: http.StatusBadRequest,
err: err,
}
} else if session, err := getSession(sessionid); err != nil {
return APIErrorResponse{
status: http.StatusBadRequest,
err: err,
}
} else if session.IdUser == nil {
return f(nil, ps, b)
} else if std, err := getUser(int(*session.IdUser)); err != nil {
return APIErrorResponse{
status: http.StatusInternalServerError,
err: err,
}
} else {
return f(&std, ps, b)
}
}), access...)
}
func loggedUser(u *User, r *http.Request) *APIErrorResponse {
if u != nil {
return nil
} else {
ret := &APIErrorResponse{
status: http.StatusForbidden,
err: errors.New("Permission Denied"),
}
return ret
}
}
func adminRestricted(u *User, r *http.Request) *APIErrorResponse {
if u != nil && u.IsAdmin {
return nil
} else {
ret := &APIErrorResponse{
status: http.StatusForbidden,
err: errors.New("Permission Denied"),
}
return ret
}
}

37
help.go
View File

@ -1,18 +1,39 @@
package main
import (
"log"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/help", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return formatApiResponse(getNeedHelps())
}, adminRestricted))
router.POST("/api/help", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return formatApiResponse(u.NewNeedHelp())
}, loggedUser))
func declareAPIAdminHelpRoutes(router *gin.RouterGroup) {
router.GET("/help", func(c *gin.Context) {
nhs, err := getNeedHelps()
if err != nil {
log.Println("Unable to getNeedHelps:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during need helps retrieval. Please retry."})
return
}
c.JSON(http.StatusOK, nhs)
})
}
func declareAPIAuthHelpRoutes(router *gin.RouterGroup) {
router.POST("/help", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
nh, err := u.NewNeedHelp()
if err != nil {
log.Printf("Unable to NewNeedHelp(uid=%d): %s", u.Id, err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, something went wrong. Please retry in a few moment."})
return
}
c.JSON(http.StatusOK, nh)
})
}
type NeedHelp struct {

30
main.go
View File

@ -1,9 +1,7 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"net/url"
@ -83,16 +81,6 @@ func main() {
LocalAuthFunc = dummyAuth
}
if DevProxy != "" {
Router().GET("/.svelte-kit/*_", serveOrReverse(""))
Router().GET("/node_modules/*_", serveOrReverse(""))
Router().GET("/@vite/*_", serveOrReverse(""))
Router().GET("/__vite_ping", serveOrReverse(""))
Router().GET("/src/*_", serveOrReverse(""))
}
initializeOIDC()
// Initialize contents
log.Println("Opening database...")
if err := DBInit(*dsn); err != nil {
@ -105,25 +93,19 @@ func main() {
log.Fatal("Cannot create database: ", err)
}
a := NewApp()
go a.Start(*bind)
initializeOIDC(a.router)
// Prepare graceful shutdown
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
srv := &http.Server{
Addr: *bind,
Handler: StripPrefix(baseURL, Router()),
}
// Serve content
go func() {
log.Fatal(srv.ListenAndServe())
}()
log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
// Wait shutdown signal
<-interrupt
log.Print("The service is shutting down...")
srv.Shutdown(context.Background())
a.Stop()
log.Println("done")
}

View File

@ -1,97 +1,113 @@
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/questions/:qid/proposals", apiAuthHandler(questionAuthHandler(
func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetProposals())
}), loggedUser))
router.POST("/api/questions/:qid/proposals", apiAuthHandler(questionAuthHandler(func(q Question, u *User, body []byte) HTTPResponse {
var new Proposal
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
func declareAPIAuthProposalsRoutes(router *gin.RouterGroup) {
router.GET("/proposals", func(c *gin.Context) {
q := c.MustGet("question").(*Question)
proposals, err := q.GetProposals()
if err != nil {
log.Printf("Unable to GetProposals(qid=%d): %s", q.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during proposals retrieving"})
return
}
return formatApiResponse(q.NewProposal(new.Label))
}), adminRestricted))
router.PUT("/api/questions/:qid/proposals/:pid", apiAuthHandler(proposalAuthHandler(func(current Proposal, u *User, body []byte) HTTPResponse {
var new Proposal
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
new.Id = current.Id
return formatApiResponse(new.Update())
}), adminRestricted))
router.DELETE("/api/questions/:qid/proposals/:pid", apiAuthHandler(proposalAuthHandler(func(p Proposal, u *User, body []byte) HTTPResponse {
return formatApiResponse(p.Delete())
}), adminRestricted))
router.GET("/api/surveys/:sid/questions/:qid/proposals", apiAuthHandler(questionAuthHandler(
func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetProposals())
}), loggedUser))
router.POST("/api/surveys/:sid/questions/:qid/proposals", apiAuthHandler(questionAuthHandler(func(q Question, u *User, body []byte) HTTPResponse {
var new Proposal
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
return formatApiResponse(q.NewProposal(new.Label))
}), adminRestricted))
router.PUT("/api/surveys/:sid/questions/:qid/proposals/:pid", apiAuthHandler(proposalAuthHandler(func(current Proposal, u *User, body []byte) HTTPResponse {
var new Proposal
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
new.Id = current.Id
return formatApiResponse(new.Update())
}), adminRestricted))
router.DELETE("/api/surveys/:sid/questions/:qid/proposals/:pid", apiAuthHandler(proposalAuthHandler(func(p Proposal, u *User, body []byte) HTTPResponse {
return formatApiResponse(p.Delete())
}), adminRestricted))
c.JSON(http.StatusOK, proposals)
})
}
func proposalHandler(f func(Proposal, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
func declareAPIAdminProposalsRoutes(router *gin.RouterGroup) {
router.POST("/proposals", func(c *gin.Context) {
q := c.MustGet("question").(*Question)
var new Proposal
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
proposal, err := q.NewProposal(new.Label)
if err != nil {
log.Printf("Unable to NewProposal(qid=%d): %s", q.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to insert new proposal."})
return
}
c.JSON(http.StatusOK, proposal)
})
proposalsRoutes := router.Group("/proposals/:pid")
proposalsRoutes.Use(proposalHandler)
proposalsRoutes.PUT("", func(c *gin.Context) {
current := c.MustGet("proposal").(*Proposal)
var new Proposal
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
new.Id = current.Id
proposal, err := new.Update()
if err != nil {
log.Println("Unable to Update proposal:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during proposal updating."})
return
}
c.JSON(http.StatusOK, proposal)
})
proposalsRoutes.DELETE("", func(c *gin.Context) {
p := c.MustGet("proposal").(*Proposal)
if _, err := p.Delete(); err != nil {
log.Printf("Unable to Delete(pid=%d) proposal: %s", p.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete proposal."})
return
}
c.JSON(http.StatusOK, nil)
})
}
func proposalHandler(c *gin.Context) {
var question *Question = nil
if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err == nil {
if q, err := getQuestion(qid); err == nil {
question = &q
}
if q, ok := c.Get("question"); ok {
question = q.(*Question)
}
if pid, err := strconv.Atoi(string(ps.ByName("pid"))); err != nil {
return APIErrorResponse{err: err}
var proposal *Proposal
if pid, err := strconv.Atoi(string(c.Param("pid"))); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid proposal ID"})
return
} else if question == nil {
if proposal, err := getProposal(pid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(proposal, body)
if proposal, err = getProposal(pid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Proposal not found"})
return
}
} else {
if proposal, err := question.GetProposal(pid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(proposal, body)
if proposal, err = question.GetProposal(pid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Proposal not found"})
return
}
}
}
}
func proposalAuthHandler(f func(Proposal, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse {
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return proposalHandler(func(p Proposal, body []byte) HTTPResponse {
return f(p, u, body)
})(ps, body)
if proposal == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Proposal not found"})
return
} else {
c.Set("proposal", proposal)
c.Next()
}
}
@ -101,7 +117,7 @@ type Proposal struct {
Label string `json:"label"`
}
func (q *Question) GetProposals() (proposals []Proposal, err error) {
func (q *Question) GetProposals() (proposals []*Proposal, err error) {
if rows, errr := DBQuery("SELECT id_proposal, id_question, label FROM survey_proposals WHERE id_question=?", q.Id); errr != nil {
return nil, errr
} else {
@ -112,7 +128,7 @@ func (q *Question) GetProposals() (proposals []Proposal, err error) {
if err = rows.Scan(&p.Id, &p.IdQuestion, &p.Label); err != nil {
return
}
proposals = append(proposals, p)
proposals = append(proposals, &p)
}
if err = rows.Err(); err != nil {
return
@ -122,12 +138,14 @@ func (q *Question) GetProposals() (proposals []Proposal, err error) {
}
}
func getProposal(id int) (p Proposal, err error) {
func getProposal(id int) (p *Proposal, err error) {
p = new(Proposal)
err = DBQueryRow("SELECT id_proposal, id_question, label FROM survey_proposals WHERE id_proposal=?", id).Scan(&p.Id, &p.IdQuestion, &p.Label)
return
}
func (q *Question) GetProposal(id int) (p Proposal, err error) {
func (q *Question) GetProposal(id int) (p *Proposal, err error) {
p = new(Proposal)
err = DBQueryRow("SELECT id_proposal, id_question, label FROM survey_proposals WHERE id_proposal=? AND id_question=?", id, q.Id).Scan(&p.Id, &p.IdQuestion, &p.Label)
return
}

View File

@ -1,132 +1,200 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
"github.com/russross/blackfriday/v2"
)
func init() {
router.GET("/api/questions", apiHandler(
func(httprouter.Params, []byte) HTTPResponse {
return formatApiResponse(getQuestions())
}, adminRestricted))
router.GET("/api/surveys/:sid/questions", apiAuthHandler(surveyAuthHandler(
func(s Survey, u *User, _ []byte) HTTPResponse {
func declareAPIAuthQuestionsRoutes(router *gin.RouterGroup) {
router.GET("/questions", func(c *gin.Context) {
var s *Survey
if survey, ok := c.Get("survey"); ok {
s = survey.(*Survey)
}
u := c.MustGet("LoggedUser").(*User)
if s == nil {
if !u.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Permission denied"})
return
}
if questions, err := getQuestions(); err != nil {
log.Println("Unable to getQuestions:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve questions. Please try again later."})
return
} else {
c.JSON(http.StatusOK, questions)
}
} else {
if !s.Shown && !u.IsAdmin {
return APIErrorResponse{err: errors.New("Not accessible")}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not accessible"})
return
}
if s.StartAvailability.After(time.Now()) && !u.IsAdmin {
return APIErrorResponse{status: http.StatusPaymentRequired, err: errors.New("Not available yet")}
}
return formatApiResponse(s.GetQuestions())
}), loggedUser))
router.POST("/api/surveys/:sid/questions", apiAuthHandler(surveyAuthHandler(func(s Survey, u *User, body []byte) HTTPResponse {
if !s.Shown && !u.IsAdmin {
return APIErrorResponse{err: errors.New("Not accessible")}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not accessible yet"})
return
}
var new Question
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
return formatApiResponse(s.NewQuestion(new.Title, new.DescriptionRaw, new.Placeholder, new.Kind))
}), adminRestricted))
router.GET("/api/questions/:qid", apiAuthHandler(questionAuthHandler(
func(q Question, u *User, _ []byte) HTTPResponse {
if u.IsAdmin {
return APIResponse{q}
} else if s, err := getSurvey(int(q.IdSurvey)); err != nil {
return APIErrorResponse{err: err}
} else if s.Shown || (s.Direct != nil && *s.Direct == q.Id) {
return APIResponse{q}
if questions, err := s.GetQuestions(); err != nil {
log.Println("Unable to GetQuestions:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve questions. Please try again later."})
return
} else {
return APIErrorResponse{err: fmt.Errorf("Not authorized"), status: http.StatusForbidden}
c.JSON(http.StatusOK, questions)
}
}), loggedUser))
router.GET("/api/surveys/:sid/questions/:qid", apiHandler(questionHandler(
func(s Question, _ []byte) HTTPResponse {
return APIResponse{s}
}), adminRestricted))
router.PUT("/api/questions/:qid", apiHandler(questionHandler(func(current Question, body []byte) HTTPResponse {
}
})
questionsRoutes := router.Group("/questions/:qid")
questionsRoutes.Use(questionHandler)
questionsRoutes.GET("", func(c *gin.Context) {
q := c.MustGet("question").(*Question)
u := c.MustGet("LoggedUser").(*User)
if !u.IsAdmin {
s, err := getSurvey(int(q.IdSurvey))
if err != nil {
log.Println("Unable to getSurvey:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during survey retrieval. Please try again later."})
return
}
if !(s.Shown || (s.Direct != nil && *s.Direct == q.Id)) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
return
}
}
c.JSON(http.StatusOK, q)
})
declareAPIAuthProposalsRoutes(questionsRoutes)
declareAPIAuthQuestionResponsesRoutes(questionsRoutes)
}
func declareAPIAdminQuestionsRoutes(router *gin.RouterGroup) {
router.POST("/questions", func(c *gin.Context) {
var s *Survey
if survey, ok := c.Get("survey"); ok {
s = survey.(*Survey)
} else {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Survey identifier not defined."})
return
}
var new Question
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
q, err := s.NewQuestion(new.Title, new.DescriptionRaw, new.Placeholder, new.Kind)
if err != nil {
log.Println("Unable to NewQuestion:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during question insertion."})
return
}
c.JSON(http.StatusOK, q)
})
questionsRoutes := router.Group("/questions/:qid")
questionsRoutes.Use(questionHandler)
questionsRoutes.PUT("", func(c *gin.Context) {
current := c.MustGet("question").(*Question)
var new Question
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
new.Id = current.Id
return formatApiResponse(new.Update())
}), adminRestricted))
router.PUT("/api/surveys/:sid/questions/:qid", apiHandler(questionHandler(func(current Question, body []byte) HTTPResponse {
var new Question
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if q, err := new.Update(); err != nil {
log.Println("Unable to Update question:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during question update."})
return
} else {
c.JSON(http.StatusOK, q)
}
})
questionsRoutes.DELETE("", func(c *gin.Context) {
q := c.MustGet("question").(*Question)
if _, err := q.Delete(); err != nil {
log.Println("Unable to Delete question:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete the question."})
return
}
new.Id = current.Id
return formatApiResponse(new.Update())
}), adminRestricted))
router.DELETE("/api/questions/:qid", apiHandler(questionHandler(
func(q Question, _ []byte) HTTPResponse {
return formatApiResponse(q.Delete())
}), adminRestricted))
router.DELETE("/api/surveys/:sid/questions/:qid", apiHandler(questionHandler(
func(q Question, _ []byte) HTTPResponse {
return formatApiResponse(q.Delete())
}), adminRestricted))
c.JSON(http.StatusOK, nil)
})
declareAPIAdminCorrectionsRoutes(questionsRoutes)
declareAPIAdminProposalsRoutes(questionsRoutes)
declareAPIAdminResponsesRoutes(questionsRoutes)
}
func questionHandler(f func(Question, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
var survey *Survey = nil
func declareAPIAdminUserQuestionsRoutes(router *gin.RouterGroup) {
questionsRoutes := router.Group("/questions/:qid")
questionsRoutes.Use(questionHandler)
if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err == nil {
if s, err := getSurvey(sid); err == nil {
survey = &s
}
questionsRoutes.GET("", func(c *gin.Context) {
question := c.MustGet("question").(*Question)
user := c.MustGet("user").(*User)
score, err := question.ComputeScoreQuestion(user)
if err != nil {
log.Println("Unable to ComputeScoreQuestion:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to compute score. Please try again later."})
return
}
if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err != nil {
return APIErrorResponse{err: err}
} else if survey == nil {
if question, err := getQuestion(qid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(question, body)
}
} else {
if question, err := survey.GetQuestion(qid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(question, body)
}
}
}
c.JSON(http.StatusOK, score)
})
}
func questionAuthHandler(f func(Question, *User, []byte) HTTPResponse, access ...func(*User, *Question) error) func(*User, httprouter.Params, []byte) HTTPResponse {
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return questionHandler(func(q Question, body []byte) HTTPResponse {
// Check access limitation
for _, a := range access {
if err := a(u, &q); err != nil {
return APIErrorResponse{
status: http.StatusForbidden,
err: err,
func questionHandler(c *gin.Context) {
var survey *Survey
if s, ok := c.Get("survey"); ok {
survey = s.(*Survey)
}
qid, err := strconv.Atoi(string(c.Param("qid")))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid question identifier."})
return
}
var question *Question
if survey == nil {
question, err = getQuestion(qid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Question not found"})
return
}
} else {
question, err = survey.GetQuestion(qid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Question not found"})
return
}
}
return f(q, u, body)
})(ps, body)
}
c.Set("question", question)
c.Next()
}
type Question struct {
@ -139,7 +207,7 @@ type Question struct {
Kind string `json:"kind"`
}
func getQuestions() (questions []Question, err error) {
func getQuestions() (questions []*Question, err error) {
if rows, errr := DBQuery("SELECT id_question, id_survey, title, description, placeholder, kind FROM survey_quests"); errr != nil {
return nil, errr
} else {
@ -151,7 +219,7 @@ func getQuestions() (questions []Question, err error) {
return
}
q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw)))
questions = append(questions, q)
questions = append(questions, &q)
}
if err = rows.Err(); err != nil {
return
@ -161,7 +229,7 @@ func getQuestions() (questions []Question, err error) {
}
}
func (s *Survey) GetQuestions() (questions []Question, err error) {
func (s *Survey) GetQuestions() (questions []*Question, err error) {
if rows, errr := DBQuery("SELECT id_question, id_survey, title, description, placeholder, kind FROM survey_quests WHERE id_survey=?", s.Id); errr != nil {
return nil, errr
} else {
@ -173,7 +241,7 @@ func (s *Survey) GetQuestions() (questions []Question, err error) {
return
}
q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw)))
questions = append(questions, q)
questions = append(questions, &q)
}
if err = rows.Err(); err != nil {
return
@ -183,41 +251,43 @@ func (s *Survey) GetQuestions() (questions []Question, err error) {
}
}
func getQuestion(id int) (q Question, err error) {
func getQuestion(id int) (q *Question, err error) {
q = new(Question)
err = DBQueryRow("SELECT id_question, id_survey, title, description, placeholder, kind FROM survey_quests WHERE id_question=?", id).Scan(&q.Id, &q.IdSurvey, &q.Title, &q.DescriptionRaw, &q.Placeholder, &q.Kind)
q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw)))
return
}
func (s *Survey) GetQuestion(id int) (q Question, err error) {
func (s *Survey) GetQuestion(id int) (q *Question, err error) {
q = new(Question)
err = DBQueryRow("SELECT id_question, id_survey, title, description, placeholder, kind FROM survey_quests WHERE id_question=? AND id_survey=?", id, s.Id).Scan(&q.Id, &q.IdSurvey, &q.Title, &q.DescriptionRaw, &q.Placeholder, &q.Kind)
q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw)))
return
}
func (s *Survey) NewQuestion(title string, description string, placeholder string, kind string) (Question, error) {
func (s *Survey) NewQuestion(title string, description string, placeholder string, kind string) (*Question, error) {
if res, err := DBExec("INSERT INTO survey_quests (id_survey, title, description, placeholder, kind) VALUES (?, ?, ?, ?, ?)", s.Id, title, description, placeholder, kind); err != nil {
return Question{}, err
return nil, err
} else if qid, err := res.LastInsertId(); err != nil {
return Question{}, err
return nil, err
} else {
return Question{qid, s.Id, title, string(blackfriday.Run([]byte(description))), description, placeholder, kind}, nil
return &Question{qid, s.Id, title, string(blackfriday.Run([]byte(description))), description, placeholder, kind}, nil
}
}
func (q Question) GetSurvey() (Survey, error) {
func (q *Question) GetSurvey() (*Survey, error) {
return getSurvey(int(q.IdSurvey))
}
func (q Question) Update() (Question, error) {
func (q *Question) Update() (*Question, error) {
if _, err := DBExec("UPDATE survey_quests SET id_survey = ?, title = ?, description = ?, placeholder = ?, kind = ? WHERE id_question = ?", q.IdSurvey, q.Title, q.DescriptionRaw, q.Placeholder, q.Kind, q.Id); err != nil {
return Question{}, err
return nil, err
} else {
return q, err
}
}
func (q Question) Delete() (int64, error) {
func (q *Question) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM survey_quests WHERE id_question = ?", q.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {

View File

@ -1,33 +1,53 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.POST("/api/surveys/:sid", apiAuthHandler(surveyAuthHandler(func(s Survey, u *User, body []byte) HTTPResponse {
func declareAPIAuthResponsesRoutes(router *gin.RouterGroup) {
router.POST("", func(c *gin.Context) {
s := c.MustGet("survey").(*Survey)
uauth := c.MustGet("LoggedUser").(*User)
var u *User
if user, ok := c.Get("user"); ok {
if !u.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
return
}
u = user.(*User)
} else {
u = uauth
}
var responses []Response
if err := json.Unmarshal(body, &responses); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(responses); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
// Check the survey is open
if !uauth.IsAdmin {
now := time.Now()
if now.Before(s.StartAvailability) {
return APIErrorResponse{err: fmt.Errorf("Le questionnaire n'a pas encore commencé")}
c.AbortWithStatusJSON(http.StatusPaymentRequired, gin.H{"errmsg": "Le questionnaire n'a pas encore commencé"})
return
} else if now.After(s.EndAvailability.Add(5 * time.Minute)) {
return APIErrorResponse{err: fmt.Errorf("Le questionnaire n'est plus ouvert")}
c.AbortWithStatusJSON(http.StatusPaymentRequired, gin.H{"errmsg": "Le questionnaire n'est plus ouvert"})
return
}
}
for _, response := range responses {
if !s.Shown && (s.Direct == nil || *s.Direct != response.IdQuestion) {
return APIErrorResponse{err: fmt.Errorf("Cette question n'est pas disponible")}
if !uauth.IsAdmin && !s.Shown && (s.Direct == nil || *s.Direct != response.IdQuestion) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Cette question n'est pas disponible"})
return
} else if len(response.Answer) > 0 {
// Check if the response has changed
if response.Id != 0 {
@ -39,7 +59,9 @@ func init() {
}
if _, err := s.NewResponse(response.IdQuestion, u.Id, response.Answer); err != nil {
return APIErrorResponse{err: err}
log.Printf("Unable to NewResponse(uid=%d;sid=%d;qid=%d): %s", u.Id, s.Id, response.IdQuestion, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Une erreur s'est produite durant l'enregistrement des réponses. Veuillez réessayer dans quelques instants."})
return
}
if s.Direct != nil {
@ -48,60 +70,54 @@ func init() {
}
}
return APIResponse{true}
}), loggedUser))
router.POST("/api/users/:uid/surveys/:sid", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return surveyAuthHandler(func(s Survey, u *User, _ []byte) HTTPResponse {
return userHandler(func(u User, _ []byte) HTTPResponse {
var responses []Response
if err := json.Unmarshal(body, &responses); err != nil {
return APIErrorResponse{err: err}
c.JSON(http.StatusOK, true)
})
router.GET("/responses", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
s := c.MustGet("survey").(*Survey)
if user, ok := c.Get("user"); ok {
if !u.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
return
}
for _, response := range responses {
if len(response.Answer) > 0 {
// Check if the response has changed
if response.Id != 0 {
if res, err := s.GetResponse(int(response.Id)); err == nil {
if res.IdUser == u.Id && res.Answer == response.Answer {
continue
}
}
u = user.(*User)
}
if _, err := s.NewResponse(response.IdQuestion, u.Id, response.Answer); err != nil {
return APIErrorResponse{err: err}
}
}
responses, err := s.GetMyResponses(u, s.Corrected)
if err != nil {
log.Printf("Unable to GetMyResponses(uid=%d;sid=%d): %s", u.Id, s.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Une erreur s'est produite pendant la récupération des réponses."})
return
}
c.JSON(http.StatusOK, responses)
})
responsesRoutes := router.Group("/responses/:rid")
responsesRoutes.Use(responseHandler)
responsesRoutes.GET("", func(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("response"))
})
responsesRoutes.POST("/report", func(c *gin.Context) {
s := c.MustGet("survey").(*Survey)
r := c.MustGet("response").(*Response)
u := c.MustGet("LoggedUser").(*User)
if user, ok := c.Get("user"); ok {
if !u.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
return
}
u = user.(*User)
}
return APIResponse{true}
})(ps, body)
})(u, ps, body)
}, adminRestricted))
router.GET("/api/surveys/:sid/responses", apiAuthHandler(surveyAuthHandler(
func(s Survey, u *User, _ []byte) HTTPResponse {
return formatApiResponse(s.GetMyResponses(u, s.Corrected))
}), loggedUser))
router.GET("/api/questions/:qid/response", apiAuthHandler(questionAuthHandler(
func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetMyResponse(u, false))
}), loggedUser))
router.GET("/api/users/:uid/surveys/:sid/responses", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return surveyAuthHandler(func(s Survey, u *User, _ []byte) HTTPResponse {
return userHandler(func(u User, _ []byte) HTTPResponse {
return formatApiResponse(s.GetMyResponses(&u, s.Corrected))
})(ps, body)
})(u, ps, body)
}, adminRestricted))
router.GET("/api/surveys/:sid/responses/:rid", apiAuthHandler(responseAuthHandler(
func(r Response, _ *User, _ []byte) HTTPResponse {
return APIResponse{r}
}), adminRestricted))
router.POST("/api/surveys/:sid/responses/:rid/report", apiAuthHandler(surveyResponseAuthHandler(
func(s *Survey, r Response, u *User, _ []byte) HTTPResponse {
if s == nil || !s.Corrected || r.IdUser != u.Id {
return APIErrorResponse{err: fmt.Errorf("Cette action est impossible pour l'instant"), status: http.StatusForbidden}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Cette action est impossible pour l'instant"})
return
}
if r.TimeScored == nil || r.TimeReported == nil || r.TimeReported.Before(*r.TimeScored) {
@ -111,35 +127,56 @@ func init() {
r.TimeReported = nil
}
if _, err := r.Update(); err != nil {
return APIErrorResponse{err: err}
log.Printf("Unable to Update(uid=%d;rid=%d) response: %s", u.Id, r.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Une erreur s'est produite lors de la mise à jour du statut de la réponse. Veuillez réessayer dans quelques instants."})
return
}
return APIResponse{r}
}), loggedUser))
router.GET("/api/surveys/:sid/questions/:qid/responses", apiAuthHandler(questionAuthHandler(
func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetResponses())
}), adminRestricted))
router.PUT("/api/surveys/:sid/questions/:qid/responses/:rid", apiAuthHandler(responseAuthHandler(func(current Response, u *User, body []byte) HTTPResponse {
c.JSON(http.StatusOK, r)
})
}
func declareAPIAuthQuestionResponsesRoutes(router *gin.RouterGroup) {
router.GET("/response", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
q := c.MustGet("question").(*Question)
res, err := q.GetMyResponse(u, false)
if err != nil {
log.Printf("Unable to GetMyResponse(uid=%d;qid=%d;false): %s", u.Id, q.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during response retrieval."})
return
}
c.JSON(http.StatusOK, res)
})
}
func declareAPIAdminResponsesRoutes(router *gin.RouterGroup) {
router.GET("/responses", func(c *gin.Context) {
q := c.MustGet("question").(*Question)
res, err := q.GetResponses()
if err != nil {
log.Printf("Unable to GetResponses(qid=%d): %s", q.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during responses retrieval."})
return
}
c.JSON(http.StatusOK, res)
})
responsesRoutes := router.Group("/responses/:rid")
responsesRoutes.Use(responseHandler)
responsesRoutes.PUT("", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
current := c.MustGet("response").(*Response)
var new Response
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
if new.Score != nil && (current.Score == nil || *new.Score != *current.Score) {
now := time.Now()
new.IdCorrector = &u.Id
new.TimeScored = &now
}
new.Id = current.Id
new.IdUser = current.IdUser
return formatApiResponse(new.Update())
}), adminRestricted))
router.PUT("/api/questions/:qid/responses/:rid", apiAuthHandler(responseAuthHandler(func(current Response, u *User, body []byte) HTTPResponse {
var new Response
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if new.Score != nil && (current.Score == nil || *new.Score != *current.Score) {
@ -147,6 +184,7 @@ func init() {
new.IdCorrector = &u.Id
new.TimeScored = &now
// Remove from cache
if _, ok := _score_cache[current.IdUser]; ok {
if surveyId, err := current.GetSurveyId(); err == nil {
if _, ok = _score_cache[current.IdUser][surveyId]; ok {
@ -158,60 +196,42 @@ func init() {
new.Id = current.Id
new.IdUser = current.IdUser
return formatApiResponse(new.Update())
}), adminRestricted))
response, err := new.Update()
if err != nil {
log.Println("Unable to Update response:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during response updating."})
return
}
c.JSON(http.StatusOK, response)
})
}
func responseHandler(f func(Response, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
return surveyResponseHandler(func(s *Survey, r Response, b []byte) HTTPResponse {
return f(r, b)
})(ps, body)
}
}
func responseHandler(c *gin.Context) {
var survey *Survey
func surveyResponseHandler(f func(*Survey, Response, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
var survey *Survey = nil
if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err == nil {
if s, err := getSurvey(sid); err == nil {
survey = &s
}
if s, ok := c.Get("survey"); ok {
survey = s.(*Survey)
}
if rid, err := strconv.Atoi(string(ps.ByName("rid"))); err != nil {
return APIErrorResponse{err: err}
var response *Response
if rid, err := strconv.Atoi(string(c.Param("rid"))); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad response identifier."})
return
} else if survey == nil {
if response, err := getResponse(rid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(survey, response, body)
if response, err = getResponse(rid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Response not found."})
return
}
} else {
if response, err := survey.GetResponse(rid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(survey, response, body)
} else if response, err = survey.GetResponse(rid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Response not found."})
return
}
}
}
}
func surveyResponseAuthHandler(f func(*Survey, Response, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse {
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return surveyResponseHandler(func(s *Survey, r Response, body []byte) HTTPResponse {
return f(s, r, u, body)
})(ps, body)
}
}
c.Set("response", response)
func responseAuthHandler(f func(Response, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse {
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return responseHandler(func(r Response, body []byte) HTTPResponse {
return f(r, u, body)
})(ps, body)
}
c.Next()
}
type Response struct {
@ -227,7 +247,7 @@ type Response struct {
TimeReported *time.Time `json:"time_reported,omitempty"`
}
func (s *Survey) GetResponses() (responses []Response, err error) {
func (s *Survey) GetResponses() (responses []*Response, err error) {
if rows, errr := DBQuery("SELECT R.id_response, R.id_question, R.id_user, R.answer, R.time_submit, R.score, R.score_explanation, R.id_corrector, R.time_scored, R.time_reported FROM survey_responses R INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE Q.id_survey=?", s.Id); errr != nil {
return nil, errr
} else {
@ -238,7 +258,7 @@ func (s *Survey) GetResponses() (responses []Response, err error) {
if err = rows.Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported); err != nil {
return
}
responses = append(responses, r)
responses = append(responses, &r)
}
if err = rows.Err(); err != nil {
return
@ -248,7 +268,7 @@ func (s *Survey) GetResponses() (responses []Response, err error) {
}
}
func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []Response, err error) {
func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []*Response, err error) {
if rows, errr := DBQuery("SELECT R.id_response, R.id_question, R.id_user, R.answer, R.time_submit, R.score, R.score_explanation, R.id_corrector, R.time_scored, R.time_reported FROM survey_responses R INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE Q.id_survey=? AND R.id_user=? ORDER BY time_submit DESC", s.Id, u.Id); errr != nil {
return nil, errr
} else {
@ -263,7 +283,7 @@ func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []Response,
r.Score = nil
r.ScoreExplaination = nil
}
responses = append(responses, r)
responses = append(responses, &r)
}
if err = rows.Err(); err != nil {
return
@ -273,7 +293,8 @@ func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []Response,
}
}
func (q *Question) GetMyResponse(u *User, showScore bool) (r Response, err error) {
func (q *Question) GetMyResponse(u *User, showScore bool) (r *Response, err error) {
r = new(Response)
err = DBQueryRow("SELECT R.id_response, R.id_question, R.id_user, R.answer, R.time_submit, R.score, R.score_explanation, R.id_corrector, R.time_scored, R.time_reported FROM survey_responses R WHERE R.id_question=? AND R.id_user=? ORDER BY time_submit DESC LIMIT 1", q.Id, u.Id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported)
if !showScore {
r.Score = nil
@ -282,7 +303,7 @@ func (q *Question) GetMyResponse(u *User, showScore bool) (r Response, err error
return
}
func (q *Question) GetResponses() (responses []Response, err error) {
func (q *Question) GetResponses() (responses []*Response, err error) {
if rows, errr := DBQuery("SELECT id_response, id_question, S.id_user, answer, S.time_submit, score, score_explanation, id_corrector, time_scored, time_reported FROM (SELECT id_user, MAX(time_submit) AS time_submit FROM survey_responses WHERE id_question=? GROUP BY id_user) R INNER JOIN survey_responses S ON S.id_user = R.id_user AND S.time_submit = R.time_submit AND S.id_question=?", q.Id, q.Id); errr != nil {
return nil, errr
} else {
@ -293,7 +314,7 @@ func (q *Question) GetResponses() (responses []Response, err error) {
if err = rows.Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported); err != nil {
return
}
responses = append(responses, r)
responses = append(responses, &r)
}
if err = rows.Err(); err != nil {
return
@ -303,23 +324,25 @@ func (q *Question) GetResponses() (responses []Response, err error) {
}
}
func getResponse(id int) (r Response, err error) {
func getResponse(id int) (r *Response, err error) {
r = new(Response)
err = DBQueryRow("SELECT id_response, id_question, id_user, answer, time_submit, score, score_explanation, id_corrector, time_scored, time_reported FROM survey_responses WHERE id_response=?", id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported)
return
}
func (s *Survey) GetResponse(id int) (r Response, err error) {
func (s *Survey) GetResponse(id int) (r *Response, err error) {
r = new(Response)
err = DBQueryRow("SELECT R.id_response, R.id_question, R.id_user, R.answer, R.time_submit, R.score, R.score_explanation, R.id_corrector, R.time_scored, R.time_reported FROM survey_responses R INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE R.id_response=? AND Q.id_survey=?", id, s.Id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported)
return
}
func (s *Survey) NewResponse(id_question int64, id_user int64, response string) (Response, error) {
func (s *Survey) NewResponse(id_question int64, id_user int64, response string) (*Response, error) {
if res, err := DBExec("INSERT INTO survey_responses (id_question, id_user, answer, time_submit) VALUES (?, ?, ?, ?)", id_question, id_user, response, time.Now()); err != nil {
return Response{}, err
return nil, err
} else if rid, err := res.LastInsertId(); err != nil {
return Response{}, err
return nil, err
} else {
return Response{rid, id_question, id_user, response, time.Now(), nil, nil, nil, nil, nil}, nil
return &Response{rid, id_question, id_user, response, time.Now(), nil, nil, nil, nil, nil}, nil
}
}

View File

@ -11,37 +11,38 @@ type Session struct {
Time time.Time `json:"time"`
}
func getSession(id []byte) (s Session, err error) {
func getSession(id []byte) (s *Session, err error) {
s = new(Session)
err = DBQueryRow("SELECT id_session, id_user, time FROM user_sessions WHERE id_session=?", id).Scan(&s.Id, &s.IdUser, &s.Time)
return
}
func NewSession() (Session, error) {
func NewSession() (*Session, error) {
session_id := make([]byte, 255)
if _, err := rand.Read(session_id); err != nil {
return Session{}, err
return nil, err
} else if _, err := DBExec("INSERT INTO user_sessions (id_session, time) VALUES (?, ?)", session_id, time.Now()); err != nil {
return Session{}, err
return nil, err
} else {
return Session{session_id, nil, time.Now()}, nil
return &Session{session_id, nil, time.Now()}, nil
}
}
func (user User) NewSession() (Session, error) {
func (user User) NewSession() (*Session, error) {
session_id := make([]byte, 255)
if _, err := rand.Read(session_id); err != nil {
return Session{}, err
return nil, err
} else if _, err := DBExec("INSERT INTO user_sessions (id_session, id_user, time) VALUES (?, ?, ?)", session_id, user.Id, time.Now()); err != nil {
return Session{}, err
return nil, err
} else {
return Session{session_id, &user.Id, time.Now()}, nil
return &Session{session_id, &user.Id, time.Now()}, nil
}
}
func (s Session) SetUser(user User) (Session, error) {
func (s Session) SetUser(user *User) (*Session, error) {
s.IdUser = &user.Id
_, err := s.Update()
return s, err
return &s, err
}
func (s Session) Update() (int64, error) {

View File

@ -6,62 +6,70 @@ import (
"net/url"
"path"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
var DevProxy string
func serveOrReverse(forced_url string) func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
func serveOrReverse(forced_url string) func(c *gin.Context) {
return func(c *gin.Context) {
if DevProxy != "" {
if u, err := url.Parse(DevProxy); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
} else {
if forced_url != "" {
u.Path = path.Join(u.Path, forced_url)
} else {
u.Path = path.Join(u.Path, r.URL.Path)
u.Path = path.Join(u.Path, c.Request.URL.Path)
}
if r, err := http.NewRequest(r.Method, u.String(), r.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
if r, err := http.NewRequest(c.Request.Method, u.String(), c.Request.Body); err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
} else if resp, err := http.DefaultClient.Do(r); err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
http.Error(c.Writer, err.Error(), http.StatusBadGateway)
} else {
defer resp.Body.Close()
for key := range resp.Header {
w.Header().Add(key, resp.Header.Get(key))
c.Writer.Header().Add(key, resp.Header.Get(key))
}
w.WriteHeader(resp.StatusCode)
c.Writer.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
io.Copy(c.Writer, resp.Body)
}
}
} else {
if forced_url != "" {
r.URL.Path = forced_url
c.Request.URL.Path = forced_url
}
http.FileServer(Assets).ServeHTTP(w, r)
http.FileServer(Assets).ServeHTTP(c.Writer, c.Request)
}
}
}
func init() {
Router().GET("/@fs/*_", serveOrReverse(""))
Router().GET("/", serveOrReverse(""))
Router().GET("/_app/*_", serveOrReverse(""))
Router().GET("/auth", serveOrReverse("/"))
Router().GET("/bug-bounty", serveOrReverse("/"))
Router().GET("/grades", serveOrReverse("/"))
Router().GET("/help", serveOrReverse("/"))
Router().GET("/surveys", serveOrReverse("/"))
Router().GET("/surveys/*_", serveOrReverse("/"))
Router().GET("/users", serveOrReverse("/"))
Router().GET("/users/*_", serveOrReverse("/"))
Router().GET("/works", serveOrReverse("/"))
Router().GET("/works/*_", serveOrReverse("/"))
Router().GET("/css/*_", serveOrReverse(""))
Router().GET("/fonts/*_", serveOrReverse(""))
Router().GET("/img/*_", serveOrReverse(""))
func declareStaticRoutes(router *gin.Engine) {
router.GET("/@fs/*_", serveOrReverse(""))
router.GET("/", serveOrReverse(""))
router.GET("/_app/*_", serveOrReverse(""))
router.GET("/auth/", serveOrReverse("/"))
router.GET("/bug-bounty", serveOrReverse("/"))
router.GET("/grades", serveOrReverse("/"))
router.GET("/help", serveOrReverse("/"))
router.GET("/surveys", serveOrReverse("/"))
router.GET("/surveys/*_", serveOrReverse("/"))
router.GET("/users", serveOrReverse("/"))
router.GET("/users/*_", serveOrReverse("/"))
router.GET("/works", serveOrReverse("/"))
router.GET("/works/*_", serveOrReverse("/"))
router.GET("/css/*_", serveOrReverse(""))
router.GET("/fonts/*_", serveOrReverse(""))
router.GET("/img/*_", serveOrReverse(""))
if DevProxy != "" {
router.GET("/.svelte-kit/*_", serveOrReverse(""))
router.GET("/node_modules/*_", serveOrReverse(""))
router.GET("/@vite/*_", serveOrReverse(""))
router.GET("/__vite_ping", serveOrReverse(""))
router.GET("/src/*_", serveOrReverse(""))
}
}

View File

@ -1,77 +1,142 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
var (
_score_cache = map[int64]map[int64]*float64{}
)
func init() {
router.GET("/api/surveys", apiAuthHandler(
func(u *User, _ httprouter.Params, _ []byte) HTTPResponse {
if u == nil {
return formatApiResponse(getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo)))
} else if u.IsAdmin {
return formatApiResponse(getSurveys("ORDER BY promo DESC, start_availability ASC"))
} else {
surveys, err := getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND promo = %d ORDER BY start_availability ASC", u.Promo))
if err != nil {
return APIErrorResponse{err: err}
}
func declareAPISurveysRoutes(router *gin.RouterGroup) {
router.GET("/surveys", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
var response []Survey
var response []*Survey
var err error
if u == nil {
response, err = getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo))
} else if u.IsAdmin {
response, err = getSurveys("ORDER BY promo DESC, start_availability ASC")
} else {
var surveys []*Survey
surveys, err = getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND promo = %d ORDER BY start_availability ASC", u.Promo))
if err == nil {
for _, s := range surveys {
if s.Group == "" || strings.Contains(u.Groups, ","+s.Group+",") {
s.Group = ""
response = append(response, s)
}
}
return formatApiResponse(response, nil)
}
}))
router.POST("/api/surveys", apiHandler(func(_ httprouter.Params, body []byte) HTTPResponse {
}
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Impossible de lister les questionnaires. Veuillez réessayer dans quelques instants"})
log.Printf("Unable to list surveys: %s", err.Error())
} else {
c.JSON(http.StatusOK, response)
}
})
surveysRoutes := router.Group("/surveys/:sid")
surveysRoutes.Use(surveyHandler)
surveysRoutes.GET("", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
if u == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Veuillez vous connecter pour accéder à cette page."})
return
}
s := c.MustGet("survey").(*Survey)
if (s.Promo == u.Promo && (s.Group == "" || strings.Contains(u.Groups, ","+s.Group+",") && s.Shown)) || u.IsAdmin {
c.JSON(http.StatusOK, s)
} else {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not accessible"})
}
})
}
func declareAPIAuthSurveysRoutes(router *gin.RouterGroup) {
surveysRoutes := router.Group("/surveys/:sid")
surveysRoutes.Use(surveyHandler)
surveysRoutes.GET("/score", func(c *gin.Context) {
var u *User
if user, ok := c.Get("user"); ok {
u = user.(*User)
} else {
u = c.MustGet("LoggedUser").(*User)
}
s := c.MustGet("survey").(*Survey)
if (s.Promo == u.Promo && s.Shown) || (u != nil && u.IsAdmin) {
score, err := s.GetScore(u)
if err != nil {
log.Printf("Unable to GetScore(uid=%d;sid=%d): %s", u.Id, s.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve score."})
return
}
if score == nil {
c.JSON(http.StatusOK, map[string]string{"score": "N/A"})
} else {
c.JSON(http.StatusOK, map[string]float64{"score": *score})
}
} else {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not accessible"})
return
}
})
declareAPIAuthAsksRoutes(surveysRoutes)
declareAPIAuthDirectRoutes(surveysRoutes)
declareAPIAuthGradesRoutes(surveysRoutes)
declareAPIAuthQuestionsRoutes(surveysRoutes)
declareAPIAuthResponsesRoutes(surveysRoutes)
}
func declareAPIAdminSurveysRoutes(router *gin.RouterGroup) {
router.POST("/surveys", func(c *gin.Context) {
var new Survey
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if new.Promo == 0 {
new.Promo = currentPromo
}
return formatApiResponse(NewSurvey(new.Title, new.Promo, new.Group, new.Shown, new.Direct, new.StartAvailability, new.EndAvailability))
}, adminRestricted))
router.GET("/api/surveys/:sid", apiAuthHandler(surveyAuthHandler(
func(s Survey, u *User, _ []byte) HTTPResponse {
if u == nil {
return APIErrorResponse{
status: http.StatusUnauthorized,
err: errors.New("Veuillez vous connecter pour accéder à cette page."),
}
} else if (s.Promo == u.Promo && (s.Group == "" || strings.Contains(u.Groups, ","+s.Group+",") && s.Shown)) || u.IsAdmin {
return APIResponse{s}
if s, err := NewSurvey(new.Title, new.Promo, new.Group, new.Shown, new.Direct, new.StartAvailability, new.EndAvailability); err != nil {
log.Println("Unable to NewSurvey:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during survey creation: %s", err.Error())})
return
} else {
return APIErrorResponse{
status: http.StatusForbidden,
err: errors.New("Not accessible"),
c.JSON(http.StatusOK, s)
}
}
})))
router.PUT("/api/surveys/:sid", apiHandler(surveyHandler(func(current Survey, body []byte) HTTPResponse {
})
surveysRoutes := router.Group("/surveys/:sid")
surveysRoutes.Use(surveyHandler)
surveysRoutes.PUT("", func(c *gin.Context) {
current := c.MustGet("survey").(*Survey)
var new Survey
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
new.Id = current.Id
@ -91,72 +156,41 @@ func init() {
}
}
return formatApiResponse(new.Update())
}), adminRestricted))
router.DELETE("/api/surveys/:sid", apiHandler(surveyHandler(
func(s Survey, _ []byte) HTTPResponse {
return formatApiResponse(s.Delete())
}), adminRestricted))
router.GET("/api/surveys/:sid/score", apiAuthHandler(surveyAuthHandler(
func(s Survey, u *User, _ []byte) HTTPResponse {
if (s.Promo == u.Promo && s.Shown) || (u != nil && u.IsAdmin) {
if score, err := s.GetScore(u); err != nil {
return APIErrorResponse{err: err}
} else if score == nil {
return APIResponse{map[string]string{"score": "N/A"}}
if survey, err := new.Update(); err != nil {
log.Println("Unable to Update survey:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during survey updation: %s", err.Error())})
return
} else {
return APIResponse{map[string]float64{"score": *score}}
c.JSON(http.StatusOK, survey)
}
})
surveysRoutes.DELETE("", func(c *gin.Context) {
survey := c.MustGet("survey").(*Survey)
if _, err := survey.Delete(); err != nil {
log.Println("Unable to Delete survey:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during survey deletion: %s", err.Error())})
return
} else {
return APIErrorResponse{
status: http.StatusForbidden,
err: errors.New("Not accessible"),
c.JSON(http.StatusOK, nil)
}
}
}), loggedUser))
router.GET("/api/users/:uid/surveys/:sid/score", apiAuthHandler(func(uauth *User, ps httprouter.Params, body []byte) HTTPResponse {
return surveyAuthHandler(func(s Survey, uauth *User, _ []byte) HTTPResponse {
return userHandler(func(u User, _ []byte) HTTPResponse {
if uauth != nil && ((s.Promo == u.Promo && s.Shown && u.Id == uauth.Id) || uauth.IsAdmin) {
if score, err := s.GetScore(&u); err != nil {
return APIErrorResponse{err: err}
} else if score == nil {
return APIResponse{map[string]string{"score": "N/A"}}
} else {
return APIResponse{map[string]float64{"score": *score}}
}
} else {
return APIErrorResponse{
status: http.StatusForbidden,
err: errors.New("Not accessible"),
}
}
})(ps, body)
})(uauth, ps, body)
}, loggedUser))
})
declareAPIAdminAsksRoutes(surveysRoutes)
declareAPIAdminDirectRoutes(surveysRoutes)
declareAPIAdminQuestionsRoutes(surveysRoutes)
}
func surveyHandler(f func(Survey, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err != nil {
return APIErrorResponse{err: err}
func surveyHandler(c *gin.Context) {
if sid, err := strconv.Atoi(string(c.Param("sid"))); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad survey identifier."})
return
} else if survey, err := getSurvey(sid); err != nil {
return APIErrorResponse{err: err}
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Survey not found."})
return
} else {
return f(survey, body)
}
}
}
func surveyAuthHandler(f func(Survey, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse {
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err != nil {
return APIErrorResponse{err: err}
} else if survey, err := getSurvey(sid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(survey, u, body)
}
c.Set("survey", survey)
c.Next()
}
}
@ -172,7 +206,7 @@ type Survey struct {
EndAvailability time.Time `json:"end_availability"`
}
func getSurveys(cnd string, param ...interface{}) (surveys []Survey, err error) {
func getSurveys(cnd string, param ...interface{}) (surveys []*Survey, err error) {
if rows, errr := DBQuery("SELECT id_survey, title, promo, grp, shown, direct, corrected, start_availability, end_availability FROM surveys "+cnd, param...); errr != nil {
return nil, errr
} else {
@ -183,7 +217,7 @@ func getSurveys(cnd string, param ...interface{}) (surveys []Survey, err error)
if err = rows.Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Direct, &s.Corrected, &s.StartAvailability, &s.EndAvailability); err != nil {
return
}
surveys = append(surveys, s)
surveys = append(surveys, &s)
}
if err = rows.Err(); err != nil {
return
@ -193,7 +227,8 @@ func getSurveys(cnd string, param ...interface{}) (surveys []Survey, err error)
}
}
func getSurvey(id int) (s Survey, err error) {
func getSurvey(id int) (s *Survey, err error) {
s = new(Survey)
err = DBQueryRow("SELECT id_survey, title, promo, grp, shown, direct, corrected, start_availability, end_availability FROM surveys WHERE id_survey=?", id).Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Direct, &s.Corrected, &s.StartAvailability, &s.EndAvailability)
return
}

151
users.go
View File

@ -1,49 +1,106 @@
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
var currentPromo uint = 0
func init() {
router.GET("/api/promos", apiHandler(
func(httprouter.Params, []byte) HTTPResponse {
return formatApiResponse(getPromos())
}, adminRestricted))
router.GET("/api/users", apiHandler(
func(httprouter.Params, []byte) HTTPResponse {
return formatApiResponse(getUsers())
}, adminRestricted))
router.GET("/api/users/:uid", apiHandler(userHandler(
func(u User, _ []byte) HTTPResponse {
return APIResponse{u}
}), loggedUser))
router.PUT("/api/users/:uid", apiHandler(userHandler(updateUser), adminRestricted))
router.DELETE("/api/users/:uid", apiHandler(userHandler(
func(u User, _ []byte) HTTPResponse {
return formatApiResponse(u.Delete())
}), adminRestricted))
func declareAPIAuthUsersRoutes(router *gin.RouterGroup) {
usersRoutes := router.Group("/users/:uid")
usersRoutes.Use(userHandler)
usersRoutes.Use(sameUserMiddleware)
usersRoutes.GET("", func(c *gin.Context) {
u := c.MustGet("user").(*User)
c.JSON(http.StatusOK, u)
})
declareAPIAuthSurveysRoutes(usersRoutes)
}
func userHandler(f func(User, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
if uid, err := strconv.Atoi(string(ps.ByName("uid"))); err != nil {
if user, err := getUserByLogin(ps.ByName("uid")); err != nil {
return APIErrorResponse{err: err}
func declareAPIAdminUsersRoutes(router *gin.RouterGroup) {
router.GET("/promos", func(c *gin.Context) {
promos, err := getPromos()
if err != nil {
log.Println("Unable to getPromos:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve promotions. Please try again later."})
return
}
c.JSON(http.StatusOK, promos)
})
router.GET("/users", func(c *gin.Context) {
users, err := getUsers()
if err != nil {
log.Println("Unable to getUsers:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve users. Please try again later."})
return
}
c.JSON(http.StatusOK, users)
})
usersRoutes := router.Group("/users/:uid")
usersRoutes.Use(userHandler)
usersRoutes.PUT("", updateUser)
usersRoutes.DELETE("", func(c *gin.Context) {
u := c.MustGet("user").(*User)
if _, err := u.Delete(); err != nil {
log.Println("Unable to Delete user:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to delete the user. Please try again later."})
return
}
c.JSON(http.StatusOK, nil)
})
declareAPIAdminUserCorrectionsRoutes(usersRoutes)
declareAPIAdminUserQuestionsRoutes(usersRoutes)
}
func userHandler(c *gin.Context) {
var user *User
uid, err := strconv.Atoi(string(c.Param("uid")))
if err != nil {
user, err = getUserByLogin(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad user identifier."})
return
}
} else {
return f(user, body)
}
} else if user, err := getUser(uid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(user, body)
user, err = getUser(uid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "User not found."})
return
}
}
c.Set("user", user)
c.Next()
}
func sameUserMiddleware(c *gin.Context) {
user := c.MustGet("user").(*User)
loggeduser := c.MustGet("LoggedUser").(*User)
if user.Id != loggeduser.Id && !loggeduser.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Permission denied."})
return
}
c.Next()
}
type User struct {
@ -100,12 +157,14 @@ func getPromos() (promos []uint, err error) {
}
}
func getUser(id int) (u User, err error) {
func getUser(id int) (u *User, err error) {
u = new(User)
err = DBQueryRow("SELECT id_user, login, email, firstname, lastname, time, promo, groups, is_admin FROM users WHERE id_user=?", id).Scan(&u.Id, &u.Login, &u.Email, &u.Firstname, &u.Lastname, &u.Time, &u.Promo, &u.Groups, &u.IsAdmin)
return
}
func getUserByLogin(login string) (u User, err error) {
func getUserByLogin(login string) (u *User, err error) {
u = new(User)
err = DBQueryRow("SELECT id_user, login, email, firstname, lastname, time, promo, groups, is_admin FROM users WHERE login=?", login).Scan(&u.Id, &u.Login, &u.Email, &u.Firstname, &u.Lastname, &u.Time, &u.Promo, &u.Groups, &u.IsAdmin)
return
}
@ -116,14 +175,14 @@ func userExists(login string) bool {
return err == nil && z == 1
}
func NewUser(login string, email string, firstname string, lastname string, groups string) (User, error) {
func NewUser(login string, email string, firstname string, lastname string, groups string) (*User, error) {
t := time.Now()
if res, err := DBExec("INSERT INTO users (login, email, firstname, lastname, time, promo, groups) VALUES (?, ?, ?, ?, ?, ?, ?)", login, email, firstname, lastname, t, currentPromo, groups); err != nil {
return User{}, err
return nil, err
} else if sid, err := res.LastInsertId(); err != nil {
return User{}, err
return nil, err
} else {
return User{sid, login, email, firstname, lastname, t, currentPromo, groups, false}, nil
return &User{sid, login, email, firstname, lastname, t, currentPromo, groups, false}, nil
}
}
@ -166,10 +225,13 @@ func ClearUsers() (int64, error) {
}
}
func updateUser(current User, body []byte) HTTPResponse {
func updateUser(c *gin.Context) {
current := c.MustGet("user").(*User)
var new User
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
current.Login = new.Login
@ -179,5 +241,12 @@ func updateUser(current User, body []byte) HTTPResponse {
current.Time = new.Time
current.Promo = new.Promo
current.Groups = new.Groups
return formatApiResponse(current.Update())
if u, err := current.Update(); err != nil {
log.Println("Unable to Update user:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to update the given user. Please try again later."})
return
} else {
c.JSON(http.StatusOK, u)
}
}

270
works.go
View File

@ -2,158 +2,233 @@ package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gin-gonic/gin"
)
func init() {
router.GET("/api/works", apiAuthHandler(
func(u *User, _ httprouter.Params, _ []byte) HTTPResponse {
if u == nil {
return formatApiResponse(getWorks(fmt.Sprintf("WHERE shown = TRUE AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo)))
} else if u.IsAdmin {
return formatApiResponse(getWorks("ORDER BY promo DESC, start_availability ASC"))
} else {
works, err := getWorks(fmt.Sprintf("WHERE shown = TRUE AND promo = %d ORDER BY start_availability ASC", u.Promo))
if err != nil {
return APIErrorResponse{err: err}
func declareAPIWorksRoutes(router *gin.RouterGroup) {
router.GET("/works", func(c *gin.Context) {
var u *User
if user, ok := c.Get("LoggedUser"); ok {
u = user.(*User)
}
var response []Work
var works []*Work
var err error
if u == nil {
works, err = getWorks(fmt.Sprintf("WHERE shown = TRUE AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo))
} else if u.IsAdmin {
works, err = getWorks("ORDER BY promo DESC, start_availability ASC")
} else {
works, err = getWorks(fmt.Sprintf("WHERE shown = TRUE AND promo = %d ORDER BY start_availability ASC", u.Promo))
}
if err != nil {
log.Println("Unable to getWorks:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Impossible de récupérer la liste des travaux. Veuillez réessayer dans quelques instants."})
return
}
var response []*Work
if u == nil || u.IsAdmin {
response = works
} else {
for _, w := range works {
if w.Group == "" || strings.Contains(u.Groups, ","+w.Group+",") {
w.Group = ""
response = append(response, w)
}
}
return formatApiResponse(response, nil)
}
}))
router.GET("/api/all_works", apiAuthHandler(
func(u *User, _ httprouter.Params, _ []byte) HTTPResponse {
c.JSON(http.StatusOK, response)
})
router.GET("/all_works", func(c *gin.Context) {
var u *User
if user, ok := c.Get("LoggedUser"); ok {
u = user.(*User)
}
var works []*OneWork
var err error
if u == nil {
return formatApiResponse(allWorks(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC, end_availability ASC", currentPromo)))
works, err = allWorks(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC, end_availability ASC", currentPromo))
} else if u.IsAdmin {
return formatApiResponse(allWorks("ORDER BY promo DESC, start_availability ASC"))
works, err = allWorks("ORDER BY promo DESC, start_availability ASC")
} else {
works, err := allWorks(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND promo = %d ORDER BY start_availability ASC, end_availability ASC", u.Promo))
if err != nil {
return APIErrorResponse{err: err}
works, err = allWorks(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND promo = %d ORDER BY start_availability ASC, end_availability ASC", u.Promo))
}
var response []OneWork
if err != nil {
log.Println("Unable to getWorks:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Impossible de récupérer la liste des travaux. Veuillez réessayer dans quelques instants."})
return
}
var response []*OneWork
if u == nil || u.IsAdmin {
response = works
} else {
for _, w := range works {
if w.Group == "" || strings.Contains(u.Groups, ","+w.Group+",") {
w.Group = ""
response = append(response, w)
}
}
return formatApiResponse(response, nil)
}
}))
router.POST("/api/works", apiHandler(func(_ httprouter.Params, body []byte) HTTPResponse {
c.JSON(http.StatusOK, response)
})
}
func declareAPIAdminWorksRoutes(router *gin.RouterGroup) {
router.POST("/works", func(c *gin.Context) {
var new Work
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if new.Promo == 0 {
new.Promo = currentPromo
}
return formatApiResponse(NewWork(new.Title, new.Promo, new.Group, new.Shown, new.SubmissionURL, new.StartAvailability, new.EndAvailability))
}, adminRestricted))
router.GET("/api/works/:wid", apiAuthHandler(workAuthHandler(
func(w Work, u *User, _ []byte) HTTPResponse {
if u.IsAdmin {
return APIResponse{w}
} else if w.Shown && w.StartAvailability.Before(time.Now()) && (w.Group == "" || strings.Contains(u.Groups, ","+w.Group+",")) {
return APIResponse{w}
} else {
return APIErrorResponse{status: http.StatusForbidden, err: fmt.Errorf("Permission denied")}
work, err := NewWork(new.Title, new.Promo, new.Group, new.Shown, new.SubmissionURL, new.StartAvailability, new.EndAvailability)
if err != nil {
log.Println("Unable to NewWork:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during work creation"})
return
}
}), loggedUser))
router.PUT("/api/works/:wid", apiHandler(workHandler(func(current Work, body []byte) HTTPResponse {
c.JSON(http.StatusOK, work)
})
worksRoutes := router.Group("/works/:wid")
worksRoutes.Use(workHandler)
worksRoutes.PUT("", func(c *gin.Context) {
current := c.MustGet("work").(*Work)
var new Work
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
new.Id = current.Id
return formatApiResponse(new.Update())
}), adminRestricted))
router.DELETE("/api/works/:wid", apiHandler(workHandler(
func(w Work, _ []byte) HTTPResponse {
return formatApiResponse(w.Delete())
}), adminRestricted))
// Grades related to works
router.GET("/api/works/:wid/grades", apiHandler(workHandler(
func(w Work, _ []byte) HTTPResponse {
return formatApiResponse(w.GetGrades(""))
}), adminRestricted))
router.PUT("/api/works/:wid/grades", apiHandler(workHandler(
func(w Work, body []byte) HTTPResponse {
_, err := w.DeleteGrades()
work, err := new.Update()
if err != nil {
return APIErrorResponse{err: err}
log.Println("Unable to Update work:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during work update."})
return
}
c.JSON(http.StatusOK, work)
})
worksRoutes.DELETE("", func(c *gin.Context) {
w := c.MustGet("work").(*Work)
_, err := w.Delete()
if err != nil {
log.Println("Unable to Delte work:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during work deletion."})
return
}
c.JSON(http.StatusOK, nil)
})
// Grades related to works
worksRoutes.GET("/grades", func(c *gin.Context) {
w := c.MustGet("work").(*Work)
grades, err := w.GetGrades("")
if err != nil {
log.Printf("Unable to GetGrades(wid=%d): %s", w.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during grades retrieval."})
return
}
c.JSON(http.StatusOK, grades)
})
worksRoutes.PUT("/grades", func(c *gin.Context) {
w := c.MustGet("work").(*Work)
var grades []WorkGrade
if err := json.Unmarshal(body, &grades); err != nil {
return APIErrorResponse{err: err}
if err := c.ShouldBindJSON(&grades); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
_, err := w.DeleteGrades()
if err != nil {
log.Printf("Unable to DeleteGrades(wid=%d): %s", w.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during grades deletion."})
return
}
err = w.AddGrades(grades)
if err != nil {
return APIErrorResponse{err: err}
log.Printf("Unable to AddGrades(wid=%d): %s", w.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during grades erasing."})
return
}
return APIResponse{true}
}), adminRestricted))
router.GET("/api/works/:wid/score", apiAuthHandler(workAuthHandler(
func(w Work, u *User, _ []byte) HTTPResponse {
if g, err := u.GetMyWorkGrade(&w); err != nil && errors.Is(err, sql.ErrNoRows) {
return APIErrorResponse{status: http.StatusNotFound, err: fmt.Errorf("Aucune note n'a été attribuée pour ce travail. Avez-vous rendu ce travail ?")}
c.JSON(http.StatusOK, true)
})
}
func declareAPIAuthWorksRoutes(router *gin.RouterGroup) {
worksRoutes := router.Group("/works/:wid")
worksRoutes.Use(workHandler)
worksRoutes.GET("", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
w := c.MustGet("work").(*Work)
if u.IsAdmin {
c.JSON(http.StatusOK, w)
} else if w.Shown && w.StartAvailability.Before(time.Now()) && (w.Group == "" || strings.Contains(u.Groups, ","+w.Group+",")) {
c.JSON(http.StatusOK, w)
} else {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Permission denied"})
}
})
// Grades related to works
worksRoutes.GET("/score", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
w := c.MustGet("work").(*Work)
if g, err := u.GetMyWorkGrade(w); err != nil && errors.Is(err, sql.ErrNoRows) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Aucune note n'a été attribuée pour ce travail. Avez-vous rendu ce travail ?"})
} else if err != nil {
return APIErrorResponse{err: err}
log.Printf("Unable to GetMyWorkGrade(uid=%d;wid=%d): %s", u.Id, w.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during grade calculation."})
} else {
return APIResponse{g}
c.JSON(http.StatusOK, g)
}
}), loggedUser))
})
}
func workHandler(f func(Work, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
return func(ps httprouter.Params, body []byte) HTTPResponse {
if wid, err := strconv.Atoi(string(ps.ByName("wid"))); err != nil {
return APIErrorResponse{err: err}
func workHandler(c *gin.Context) {
if wid, err := strconv.Atoi(string(c.Param("wid"))); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad work identifier."})
return
} else if work, err := getWork(wid); err != nil {
return APIErrorResponse{err: err}
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Work not found."})
return
} else {
return f(work, body)
}
}
}
func workAuthHandler(f func(Work, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse {
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
if wid, err := strconv.Atoi(string(ps.ByName("wid"))); err != nil {
return APIErrorResponse{err: err}
} else if work, err := getWork(wid); err != nil {
return APIErrorResponse{err: err}
} else {
return f(work, u, body)
}
c.Set("work", work)
c.Next()
}
}
@ -171,7 +246,7 @@ type OneWork struct {
EndAvailability time.Time `json:"end_availability"`
}
func allWorks(cnd string, param ...interface{}) (items []OneWork, err error) {
func allWorks(cnd string, param ...interface{}) (items []*OneWork, err error) {
if rows, errr := DBQuery("SELECT kind, id, title, promo, grp, shown, direct, submission_url, corrected, start_availability, end_availability FROM all_works "+cnd, param...); errr != nil {
return nil, errr
} else {
@ -182,7 +257,7 @@ func allWorks(cnd string, param ...interface{}) (items []OneWork, err error) {
if err = rows.Scan(&w.Kind, &w.Id, &w.Title, &w.Promo, &w.Group, &w.Shown, &w.Direct, &w.SubmissionURL, &w.Corrected, &w.StartAvailability, &w.EndAvailability); err != nil {
return
}
items = append(items, w)
items = append(items, &w)
}
if err = rows.Err(); err != nil {
return
@ -204,7 +279,7 @@ type Work struct {
EndAvailability time.Time `json:"end_availability"`
}
func getWorks(cnd string, param ...interface{}) (items []Work, err error) {
func getWorks(cnd string, param ...interface{}) (items []*Work, err error) {
if rows, errr := DBQuery("SELECT id_work, title, promo, grp, shown, submission_url, corrected, start_availability, end_availability FROM works "+cnd, param...); errr != nil {
return nil, errr
} else {
@ -215,7 +290,7 @@ func getWorks(cnd string, param ...interface{}) (items []Work, err error) {
if err = rows.Scan(&w.Id, &w.Title, &w.Promo, &w.Group, &w.Shown, &w.SubmissionURL, &w.Corrected, &w.StartAvailability, &w.EndAvailability); err != nil {
return
}
items = append(items, w)
items = append(items, &w)
}
if err = rows.Err(); err != nil {
return
@ -225,7 +300,8 @@ func getWorks(cnd string, param ...interface{}) (items []Work, err error) {
}
}
func getWork(id int) (w Work, err error) {
func getWork(id int) (w *Work, err error) {
w = new(Work)
err = DBQueryRow("SELECT id_work, title, promo, grp, shown, submission_url, corrected, start_availability, end_availability FROM works WHERE id_work=?", id).Scan(&w.Id, &w.Title, &w.Promo, &w.Group, &w.Shown, &w.SubmissionURL, &w.Corrected, &w.StartAvailability, &w.EndAvailability)
return
}