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 package main
import ( import (
"encoding/json" "log"
"net/http"
"time" "time"
"github.com/gin-gonic/gin"
) )
func init() { func declareAPIAuthAsksRoutes(router *gin.RouterGroup) {
router.POST("/api/surveys/:sid/ask", apiAuthHandler(surveyAuthHandler(func(s Survey, u *User, body []byte) HTTPResponse { router.POST("/ask", func(c *gin.Context) {
u := c.MustGet("LoggedUser").(*User)
s := c.MustGet("survey").(*Survey)
var ask Ask var ask Ask
if err := json.Unmarshal(body, &ask); err != nil { if err := c.ShouldBindJSON(&ask); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
a, err := s.NewAsk(u.Id, ask.Content) a, err := s.NewAsk(u.Id, ask.Content)
if err != nil { 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 { if s.Direct != nil {
s.WSAdminWriteAll(WSMessage{Action: "new_ask", UserId: &u.Id, QuestionId: &a.Id, Response: ask.Content}) s.WSAdminWriteAll(WSMessage{Action: "new_ask", UserId: &u.Id, QuestionId: &a.Id, Response: ask.Content})
} }
return formatApiResponse(a, err) c.JSON(http.StatusOK, a)
}), loggedUser)) })
router.GET("/api/surveys/:sid/ask", apiHandler(surveyHandler( }
func(s Survey, _ []byte) HTTPResponse {
return formatApiResponse(s.GetAsks(true)) func declareAPIAdminAsksRoutes(router *gin.RouterGroup) {
}), adminRestricted)) 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 { type Ask struct {

66
auth.go
View File

@ -2,12 +2,10 @@ package main
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt"
"net/http" "net/http"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var LocalAuthFunc = checkAuthKrb5 var LocalAuthFunc = checkAuthKrb5
@ -18,12 +16,12 @@ type loginForm struct {
Password string `json:"password"` Password string `json:"password"`
} }
func init() { func declareAPIAuthRoutes(router *gin.RouterGroup) {
router.GET("/api/auth", apiAuthHandler(validateAuthToken)) router.GET("/auth", validateAuthToken)
router.POST("/api/auth", apiRawHandler(func(w http.ResponseWriter, ps httprouter.Params, body []byte) HTTPResponse { router.POST("/auth", func(c *gin.Context) {
return formatApiResponse(LocalAuthFunc(w, ps, body)) LocalAuthFunc(c)
})) })
router.POST("/api/auth/logout", apiRawHandler(logout)) router.POST("/auth/logout", logout)
} }
type authToken struct { type authToken struct {
@ -31,20 +29,21 @@ type authToken struct {
CurrentPromo uint `json:"current_promo"` CurrentPromo uint `json:"current_promo"`
} }
func validateAuthToken(u *User, _ httprouter.Params, _ []byte) HTTPResponse { func validateAuthToken(c *gin.Context) {
if u == nil { if u, ok := c.Get("LoggedUser"); !ok || u.(*User) == nil {
return APIErrorResponse{status: http.StatusUnauthorized, err: fmt.Errorf("Not connected")} c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Not connected"})
return
} else { } 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 { func logout(c *gin.Context) {
eraseCookie(w) eraseCookie(c)
return APIResponse{true} 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 !userExists(username) {
if usr, err = NewUser(username, email, firstname, lastname, groups); err != nil { if usr, err = NewUser(username, email, firstname, lastname, groups); err != nil {
return return
@ -64,9 +63,7 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
} }
if session == nil { if session == nil {
var s Session session, err = usr.NewSession()
s, err = usr.NewSession()
session = &s
} else { } else {
_, err = session.SetUser(usr) _, err = session.SetUser(usr)
} }
@ -75,7 +72,7 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
return return
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(c.Writer, &http.Cookie{
Name: "auth", Name: "auth",
Value: base64.StdEncoding.EncodeToString(session.Id), Value: base64.StdEncoding.EncodeToString(session.Id),
Path: baseURL + "/", Path: baseURL + "/",
@ -88,11 +85,28 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
return return
} }
func dummyAuth(w http.ResponseWriter, _ httprouter.Params, body []byte) (interface{}, error) { func eraseCookie(c *gin.Context) {
var lf map[string]string http.SetCookie(c.Writer, &http.Cookie{
if err := json.Unmarshal(body, &lf); err != nil { Name: "auth",
return nil, err Value: "",
Path: baseURL + "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
} }
return completeAuth(w, lf["username"], lf["email"], lf["firstname"], lf["lastname"], "", nil) func dummyAuth(c *gin.Context) {
var lf map[string]string
if err := c.ShouldBindJSON(&lf); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
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 package main
import ( import (
"encoding/json" "log"
"errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/gin-gonic/gin"
"github.com/jcmturner/gokrb5/v8/client" "github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/config" "github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/iana/etypeID" "github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/krberror" "github.com/jcmturner/gokrb5/v8/krberror"
"github.com/julienschmidt/httprouter"
) )
func parseETypes(s []string, w bool) []int32 { func parseETypes(s []string, w bool) []int32 {
@ -37,10 +35,11 @@ func parseETypes(s []string, w bool) []int32 {
return eti return eti
} }
func checkAuthKrb5(w http.ResponseWriter, _ httprouter.Params, body []byte) (interface{}, error) { func checkAuthKrb5(c *gin.Context) {
var lf loginForm var lf loginForm
if err := json.Unmarshal(body, &lf); err != nil { if err := c.ShouldBindJSON(&lf); err != nil {
return nil, err c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
found := false found := false
@ -52,7 +51,8 @@ func checkAuthKrb5(w http.ResponseWriter, _ httprouter.Params, body []byte) (int
} }
if !userExists(lf.Login) && !found { 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() 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.DefaultTktEnctypeIDs = parseETypes(cnf.LibDefaults.DefaultTktEnctypes, cnf.LibDefaults.AllowWeakCrypto)
cnf.LibDefaults.PermittedEnctypeIDs = parseETypes(cnf.LibDefaults.PermittedEnctypes, cnf.LibDefaults.AllowWeakCrypto) cnf.LibDefaults.PermittedEnctypeIDs = parseETypes(cnf.LibDefaults.PermittedEnctypes, cnf.LibDefaults.AllowWeakCrypto)
c := client.NewWithPassword(lf.Login, "CRI.EPITA.FR", lf.Password, cnf) cl := client.NewWithPassword(lf.Login, "CRI.EPITA.FR", lf.Password, cnf)
if err := c.Login(); err != nil { if err := cl.Login(); err != nil {
if errk, ok := err.(krberror.Krberror); ok { if errk, ok := err.(krberror.Krberror); ok {
if errk.RootCause == krberror.NetworkingError { 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 { } 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 log.Println("Unable to login through Kerberos: unknown error:", err)
} else { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Invalid credentials (unknown error)."})
return completeAuth(w, lf.Login, lf.Login+"@epita.fr", "", "", "", nil) return
} }
completeAuth(c, lf.Login, lf.Login+"@epita.fr", "", "", "", nil)
} }

View File

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

View File

@ -1,160 +1,173 @@
package main package main
import ( import (
"encoding/json" "log"
"net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareAPIAdminCorrectionsRoutes(router *gin.RouterGroup) {
router.GET("/api/surveys/:sid/questions/:qid/corrections", apiHandler(questionHandler( router.GET("/corrections", func(c *gin.Context) {
func(q Question, _ []byte) HTTPResponse { q := c.MustGet("question").(*Question)
if cts, err := q.GetCorrectionTemplates(); err != nil {
return APIErrorResponse{err: err} cts, err := q.GetCorrectionTemplates()
} else { if err != nil {
return APIResponse{cts} log.Println("Unable to GetCorrectionTemplates:", err)
} c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrive correction's templates"})
}), adminRestricted)) return
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}
} }
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination)) c.JSON(http.StatusOK, cts)
}), adminRestricted)) })
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 var new CorrectionTemplate
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} 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 new.Id = current.Id
if err := new.Update(); err != nil { if err := new.Update(); err != nil {
return APIErrorResponse{err: err} log.Println("Unable to Update correctionTemplate:", err)
} else { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update the correction template"})
return APIResponse{new} return
}
}), 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}
} }
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination)) c.JSON(http.StatusOK, new)
}), adminRestricted)) })
correctionsRoutes.DELETE("", func(c *gin.Context) {
ct := c.MustGet("correctiontemplate").(*CorrectionTemplate)
router.GET("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler( if _, err := ct.Delete(); err != nil {
func(ct CorrectionTemplate, _ []byte) HTTPResponse { log.Println("Unable to Delete correctionTemplate:", err)
if users, err := ct.GetUserCorrected(); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete the correction template."})
return APIErrorResponse{err: err} return
} 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}
} }
new.Id = current.Id c.JSON(http.StatusOK, nil)
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))
router.GET("/api/users/:uid/questions/:qid", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse { func declareAPIAdminUserCorrectionsRoutes(router *gin.RouterGroup) {
return userHandler(func(u User, _ []byte) HTTPResponse { router.GET("/corrections", func(c *gin.Context) {
if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err != nil { user := c.MustGet("user").(*User)
return APIErrorResponse{err: err}
} else if question, err := getQuestion(qid); err != nil { corrections, err := user.GetCorrections()
return APIErrorResponse{err: err} if err != nil {
} else { log.Printf("Unable to GetCorrections(uid=%d): %s", user.Id, err.Error())
return formatApiResponse(question.ComputeScoreQuestion(&u)) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve corrections."})
return
} }
})(ps, body)
}, adminRestricted))
router.GET("/api/users/:uid/corrections", apiHandler(userHandler( c.JSON(http.StatusOK, corrections)
func(u User, _ []byte) HTTPResponse { })
return formatApiResponse(u.GetCorrections()) router.POST("/corrections", func(c *gin.Context) {
}), adminRestricted)) user := c.MustGet("user").(*User)
router.POST("/api/users/:uid/corrections", apiHandler(userHandler(func(u User, body []byte) HTTPResponse {
var new UserCorrection var new UserCorrection
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
return formatApiResponse(u.NewCorrection(new.IdTemplate)) correction, err := user.NewCorrection(new.IdTemplate)
}), adminRestricted)) if err != nil {
router.PUT("/api/users/:uid/corrections", apiHandler(userHandler(func(u User, body []byte) HTTPResponse { 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 var new map[int64]bool
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
return formatApiResponse(u.EraseCorrections(new)) correction, err := user.EraseCorrections(new)
}), adminRestricted)) if err != nil {
router.DELETE("/api/users/:uid/corrections/:cid", apiHandler(userCorrectionHandler(func(u User, uc UserCorrection, body []byte) HTTPResponse { log.Printf("Unable to EraseCorrections(uid=%d): %s", user.Id, err.Error())
return formatApiResponse(uc.Delete(u)) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to erase the correction."})
}), adminRestricted)) return
} }
func correctionHandler(f func(CorrectionTemplate, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { c.JSON(http.StatusOK, correction)
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 { correctionsRoutes := router.Group("/corrections/:cid")
return APIErrorResponse{err: err} 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(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 { } 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 { } else {
return f(correction, body) c.Set("correctiontemplate", correction)
}
})(ps, body)
}
}
func userCorrectionHandler(f func(User, UserCorrection, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { c.Next()
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)
} }
} }
@ -167,7 +180,7 @@ type CorrectionTemplate struct {
ScoreExplaination string `json:"score_explaination,omitempty"` 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 { 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 return nil, errr
} else { } 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 { if err = rows.Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination); err != nil {
return return
} }
ct = append(ct, c) ct = append(ct, &c)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return 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) 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 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) 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 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 { 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 { } else if cid, err := res.LastInsertId(); err != nil {
return CorrectionTemplate{}, err return nil, err
} else { } 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 { if rows, errr := DBQuery("SELECT id_correction, id_user, id_template FROM student_corrected WHERE id_template=?", t.Id); errr != nil {
return nil, errr return nil, errr
} else { } 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 { if err = rows.Scan(&c.Id, &c.IdUser, &c.IdTemplate); err != nil {
return return
} }
ucs = append(ucs, c) ucs = append(ucs, &c)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return return
@ -260,6 +275,22 @@ type UserCorrection struct {
IdTemplate int64 `json:"id_template"` 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) { 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 { if rows, errr := DBQuery("SELECT id_correction, id_template FROM student_corrected WHERE id_user=?", u.Id); errr != nil {
return nil, errr 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) 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 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 { if res, err := DBExec("DELETE FROM student_corrected WHERE id_correction = ?", c.Id); err != nil {
return nil, err return nil, err
} else if _, err := res.RowsAffected(); err != nil { } 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 { } else if corrections, err := u.GetCorrectionsTemplate(); err != nil {
return nil, err return nil, err
} else { } else {
tpls := map[int64]CorrectionTemplate{} tpls := map[int64]*CorrectionTemplate{}
for _, tpl := range templates { for _, tpl := range templates {
tpls[tpl.Id] = tpl tpls[tpl.Id] = tpl
} }

View File

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

3
go.mod
View File

@ -4,13 +4,12 @@ go 1.16
require ( require (
github.com/coreos/go-oidc/v3 v3.2.0 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/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/jcmturner/gokrb5/v8 v8.4.3
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
github.com/russross/blackfriday/v2 v2.1.0 github.com/russross/blackfriday/v2 v2.1.0
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c 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 gopkg.in/square/go-jose.v2 v2.6.0 // indirect
nhooyr.io/websocket v1.8.7 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-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-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/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 h1:2eR2MGR7thBXSQ2YbODlF0fcmgtliLCfr9iX6RW11fc=
github.com/coreos/go-oidc/v3 v3.2.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= 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= 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/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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 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.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 v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 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 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 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.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 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 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.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.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.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.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.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.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/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@ -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 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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/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.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 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 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/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 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 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 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 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 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 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 h1:iTonLeSJOn7MVUtyMT+arAn5AKAPrkilzhGw8wE/Tq8=
github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0= 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= 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/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 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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.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.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.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/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 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 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-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-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-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 h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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= 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-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-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-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-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-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-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-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-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-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-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 h1:2yWTtPWWRcISTw3/o+s/Y4UOMnQL71DWyToOANFusCg=
golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= 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-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-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-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-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-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 h1:q3gFqPqH7NVofKo3c3yETAP//pPI+G5mvB7qqj1Y5kY=
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= 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= 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-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-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-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-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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/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-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-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-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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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-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-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-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-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-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/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 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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 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/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/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= 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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 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.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.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= 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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/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 package main
import ( import (
"errors" "log"
"net/http" "net/http"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareAPIAuthGradesRoutes(router *gin.RouterGroup) {
router.GET("/api/users/:uid/surveys/:sid/grades", apiAuthHandler(func(uauth *User, ps httprouter.Params, body []byte) HTTPResponse { router.GET("/grades", func(c *gin.Context) {
return surveyAuthHandler(func(s Survey, uauth *User, _ []byte) HTTPResponse { uauth := c.MustGet("LoggedUser").(*User)
return userHandler(func(u User, _ []byte) HTTPResponse {
if uauth != nil && ((s.Shown && u.Id == uauth.Id) || uauth.IsAdmin) { if survey, ok := c.Get("survey"); !ok {
if score, err := s.GetUserGrades(&u); err != nil { if uauth == nil || !uauth.IsAdmin {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
} else if score == nil { return
return APIResponse{"N/A"} }
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 { } 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 { } else {
return APIErrorResponse{ c.JSON(http.StatusOK, score)
status: http.StatusForbidden,
err: errors.New("Not accessible"),
} }
} 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) c.JSON(http.StatusOK, scores)
}, 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"}
} else { } else {
return APIResponse{scores} c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
} return
}), 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"),
} }
} }
}, adminRestricted)) })
} }
func GetAllGrades() (scores map[int64]map[int64]*float64, err error) { 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 package main
import ( import (
"log"
"net/http"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareAPIAdminHelpRoutes(router *gin.RouterGroup) {
router.GET("/api/help", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse { router.GET("/help", func(c *gin.Context) {
return formatApiResponse(getNeedHelps()) nhs, err := getNeedHelps()
}, adminRestricted)) if err != nil {
router.POST("/api/help", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse { log.Println("Unable to getNeedHelps:", err)
return formatApiResponse(u.NewNeedHelp()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during need helps retrieval. Please retry."})
}, loggedUser)) 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 { type NeedHelp struct {

30
main.go
View File

@ -1,9 +1,7 @@
package main package main
import ( import (
"context"
"flag" "flag"
"fmt"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
@ -83,16 +81,6 @@ func main() {
LocalAuthFunc = dummyAuth 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 // Initialize contents
log.Println("Opening database...") log.Println("Opening database...")
if err := DBInit(*dsn); err != nil { if err := DBInit(*dsn); err != nil {
@ -105,25 +93,19 @@ func main() {
log.Fatal("Cannot create database: ", err) log.Fatal("Cannot create database: ", err)
} }
a := NewApp()
go a.Start(*bind)
initializeOIDC(a.router)
// Prepare graceful shutdown // Prepare graceful shutdown
interrupt := make(chan os.Signal, 1) interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
srv := &http.Server{
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 // Wait shutdown signal
<-interrupt <-interrupt
log.Print("The service is shutting down...") log.Print("The service is shutting down...")
srv.Shutdown(context.Background()) a.Stop()
log.Println("done") log.Println("done")
} }

View File

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

View File

@ -1,132 +1,200 @@
package main package main
import ( import (
"encoding/json" "log"
"errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
"github.com/russross/blackfriday/v2" "github.com/russross/blackfriday/v2"
) )
func init() { func declareAPIAuthQuestionsRoutes(router *gin.RouterGroup) {
router.GET("/api/questions", apiHandler( router.GET("/questions", func(c *gin.Context) {
func(httprouter.Params, []byte) HTTPResponse { var s *Survey
return formatApiResponse(getQuestions()) if survey, ok := c.Get("survey"); ok {
}, adminRestricted)) s = survey.(*Survey)
router.GET("/api/surveys/:sid/questions", apiAuthHandler(surveyAuthHandler( }
func(s Survey, u *User, _ []byte) HTTPResponse {
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 { 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 { if s.StartAvailability.After(time.Now()) && !u.IsAdmin {
return APIErrorResponse{status: http.StatusPaymentRequired, err: errors.New("Not available yet")} c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not accessible yet"})
} return
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")}
} }
var new Question if questions, err := s.GetQuestions(); err != nil {
if err := json.Unmarshal(body, &new); err != nil { log.Println("Unable to GetQuestions:", err)
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve questions. Please try again later."})
} return
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}
} else { } 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} questionsRoutes := router.Group("/questions/:qid")
}), adminRestricted)) questionsRoutes.Use(questionHandler)
router.PUT("/api/questions/:qid", apiHandler(questionHandler(func(current Question, body []byte) HTTPResponse {
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 var new Question
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} 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 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}
}
new.Id = current.Id if q, err := new.Update(); err != nil {
return formatApiResponse(new.Update()) log.Println("Unable to Update question:", err)
}), adminRestricted)) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during question update."})
router.DELETE("/api/questions/:qid", apiHandler(questionHandler( return
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))
}
func questionHandler(f func(Question, []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 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 { } else {
return f(question, body) 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
}
c.JSON(http.StatusOK, nil)
})
declareAPIAdminCorrectionsRoutes(questionsRoutes)
declareAPIAdminProposalsRoutes(questionsRoutes)
declareAPIAdminResponsesRoutes(questionsRoutes)
}
func declareAPIAdminUserQuestionsRoutes(router *gin.RouterGroup) {
questionsRoutes := router.Group("/questions/:qid")
questionsRoutes.Use(questionHandler)
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
}
c.JSON(http.StatusOK, score)
})
}
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 { } else {
if question, err := survey.GetQuestion(qid); err != nil { question, err = survey.GetQuestion(qid)
return APIErrorResponse{err: err} if err != nil {
} else { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Question not found"})
return f(question, body) return
}
}
} }
} }
func questionAuthHandler(f func(Question, *User, []byte) HTTPResponse, access ...func(*User, *Question) error) func(*User, httprouter.Params, []byte) HTTPResponse { c.Set("question", question)
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,
}
}
}
return f(q, u, body) c.Next()
})(ps, body)
}
} }
type Question struct { type Question struct {
@ -139,7 +207,7 @@ type Question struct {
Kind string `json:"kind"` 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 { if rows, errr := DBQuery("SELECT id_question, id_survey, title, description, placeholder, kind FROM survey_quests"); errr != nil {
return nil, errr return nil, errr
} else { } else {
@ -151,7 +219,7 @@ func getQuestions() (questions []Question, err error) {
return return
} }
q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw))) q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw)))
questions = append(questions, q) questions = append(questions, &q)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return 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 { 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 return nil, errr
} else { } else {
@ -173,7 +241,7 @@ func (s *Survey) GetQuestions() (questions []Question, err error) {
return return
} }
q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw))) q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw)))
questions = append(questions, q) questions = append(questions, &q)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return 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) 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))) q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw)))
return 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) 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))) q.Description = string(blackfriday.Run([]byte(q.DescriptionRaw)))
return 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 { 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 { } else if qid, err := res.LastInsertId(); err != nil {
return Question{}, err return nil, err
} else { } 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)) 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 { 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 { } else {
return q, err 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 { if res, err := DBExec("DELETE FROM survey_quests WHERE id_question = ?", q.Id); err != nil {
return 0, err return 0, err
} else if nb, err := res.RowsAffected(); err != nil { } else if nb, err := res.RowsAffected(); err != nil {

View File

@ -1,33 +1,53 @@
package main package main
import ( import (
"encoding/json" "log"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareAPIAuthResponsesRoutes(router *gin.RouterGroup) {
router.POST("/api/surveys/:sid", apiAuthHandler(surveyAuthHandler(func(s Survey, u *User, body []byte) HTTPResponse { 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 var responses []Response
if err := json.Unmarshal(body, &responses); err != nil { if err := c.ShouldBindJSON(responses); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
// Check the survey is open // Check the survey is open
if !uauth.IsAdmin {
now := time.Now() now := time.Now()
if now.Before(s.StartAvailability) { 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)) { } 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 { for _, response := range responses {
if !s.Shown && (s.Direct == nil || *s.Direct != response.IdQuestion) { if !uauth.IsAdmin && !s.Shown && (s.Direct == nil || *s.Direct != response.IdQuestion) {
return APIErrorResponse{err: fmt.Errorf("Cette question n'est pas disponible")} c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Cette question n'est pas disponible"})
return
} else if len(response.Answer) > 0 { } else if len(response.Answer) > 0 {
// Check if the response has changed // Check if the response has changed
if response.Id != 0 { if response.Id != 0 {
@ -39,7 +59,9 @@ func init() {
} }
if _, err := s.NewResponse(response.IdQuestion, u.Id, response.Answer); err != nil { 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 { if s.Direct != nil {
@ -48,60 +70,54 @@ func init() {
} }
} }
return APIResponse{true} c.JSON(http.StatusOK, true)
}), loggedUser)) })
router.POST("/api/users/:uid/surveys/:sid", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse { router.GET("/responses", func(c *gin.Context) {
return surveyAuthHandler(func(s Survey, u *User, _ []byte) HTTPResponse { u := c.MustGet("LoggedUser").(*User)
return userHandler(func(u User, _ []byte) HTTPResponse { s := c.MustGet("survey").(*Survey)
var responses []Response
if err := json.Unmarshal(body, &responses); err != nil { if user, ok := c.Get("user"); ok {
return APIErrorResponse{err: err} if !u.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Not authorized"})
return
} }
for _, response := range responses { u = user.(*User)
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
}
}
} }
if _, err := s.NewResponse(response.IdQuestion, u.Id, response.Answer); err != nil { responses, err := s.GetMyResponses(u, s.Corrected)
return APIErrorResponse{err: err} 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 { 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) { if r.TimeScored == nil || r.TimeReported == nil || r.TimeReported.Before(*r.TimeScored) {
@ -111,35 +127,56 @@ func init() {
r.TimeReported = nil r.TimeReported = nil
} }
if _, err := r.Update(); err != 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} c.JSON(http.StatusOK, r)
}), loggedUser)) })
router.GET("/api/surveys/:sid/questions/:qid/responses", apiAuthHandler(questionAuthHandler( }
func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetResponses()) func declareAPIAuthQuestionResponsesRoutes(router *gin.RouterGroup) {
}), adminRestricted)) router.GET("/response", func(c *gin.Context) {
router.PUT("/api/surveys/:sid/questions/:qid/responses/:rid", apiAuthHandler(responseAuthHandler(func(current Response, u *User, body []byte) HTTPResponse { 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 var new Response
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
} return
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 new.Score != nil && (current.Score == nil || *new.Score != *current.Score) { if new.Score != nil && (current.Score == nil || *new.Score != *current.Score) {
@ -147,6 +184,7 @@ func init() {
new.IdCorrector = &u.Id new.IdCorrector = &u.Id
new.TimeScored = &now new.TimeScored = &now
// Remove from cache
if _, ok := _score_cache[current.IdUser]; ok { if _, ok := _score_cache[current.IdUser]; ok {
if surveyId, err := current.GetSurveyId(); err == nil { if surveyId, err := current.GetSurveyId(); err == nil {
if _, ok = _score_cache[current.IdUser][surveyId]; ok { if _, ok = _score_cache[current.IdUser][surveyId]; ok {
@ -158,60 +196,42 @@ func init() {
new.Id = current.Id new.Id = current.Id
new.IdUser = current.IdUser 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
} }
func responseHandler(f func(Response, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { c.JSON(http.StatusOK, response)
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 surveyResponseHandler(f func(*Survey, Response, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { func responseHandler(c *gin.Context) {
return func(ps httprouter.Params, body []byte) HTTPResponse { var survey *Survey
var survey *Survey = nil
if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err == nil { if s, ok := c.Get("survey"); ok {
if s, err := getSurvey(sid); err == nil { survey = s.(*Survey)
survey = &s
}
} }
if rid, err := strconv.Atoi(string(ps.ByName("rid"))); err != nil { var response *Response
return APIErrorResponse{err: err} 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 { } else if survey == nil {
if response, err := getResponse(rid); err != nil { if response, err = getResponse(rid); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Response not found."})
} else { return
return f(survey, response, body)
}
} 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 { c.Set("response", response)
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)
}
}
func responseAuthHandler(f func(Response, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse { c.Next()
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)
}
} }
type Response struct { type Response struct {
@ -227,7 +247,7 @@ type Response struct {
TimeReported *time.Time `json:"time_reported,omitempty"` 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 { 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 return nil, errr
} else { } 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 { 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 return
} }
responses = append(responses, r) responses = append(responses, &r)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return 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 { 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 return nil, errr
} else { } else {
@ -263,7 +283,7 @@ func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []Response,
r.Score = nil r.Score = nil
r.ScoreExplaination = nil r.ScoreExplaination = nil
} }
responses = append(responses, r) responses = append(responses, &r)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return 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) 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 { if !showScore {
r.Score = nil r.Score = nil
@ -282,7 +303,7 @@ func (q *Question) GetMyResponse(u *User, showScore bool) (r Response, err error
return 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 { 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 return nil, errr
} else { } 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 { 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 return
} }
responses = append(responses, r) responses = append(responses, &r)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return 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) 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 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) 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 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 { 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 { } else if rid, err := res.LastInsertId(); err != nil {
return Response{}, err return nil, err
} else { } 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"` 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) err = DBQueryRow("SELECT id_session, id_user, time FROM user_sessions WHERE id_session=?", id).Scan(&s.Id, &s.IdUser, &s.Time)
return return
} }
func NewSession() (Session, error) { func NewSession() (*Session, error) {
session_id := make([]byte, 255) session_id := make([]byte, 255)
if _, err := rand.Read(session_id); err != nil { 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 { } 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 { } 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) session_id := make([]byte, 255)
if _, err := rand.Read(session_id); err != nil { 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 { } 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 { } 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 s.IdUser = &user.Id
_, err := s.Update() _, err := s.Update()
return s, err return &s, err
} }
func (s Session) Update() (int64, error) { func (s Session) Update() (int64, error) {

View File

@ -6,62 +6,70 @@ import (
"net/url" "net/url"
"path" "path"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var DevProxy string var DevProxy string
func serveOrReverse(forced_url string) func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { func serveOrReverse(forced_url string) func(c *gin.Context) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { return func(c *gin.Context) {
if DevProxy != "" { if DevProxy != "" {
if u, err := url.Parse(DevProxy); err != nil { if u, err := url.Parse(DevProxy); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
} else { } else {
if forced_url != "" { if forced_url != "" {
u.Path = path.Join(u.Path, forced_url) u.Path = path.Join(u.Path, forced_url)
} else { } 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 { if r, err := http.NewRequest(c.Request.Method, u.String(), c.Request.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
} else if resp, err := http.DefaultClient.Do(r); err != nil { } 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 { } else {
defer resp.Body.Close() defer resp.Body.Close()
for key := range resp.Header { 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 { } else {
if forced_url != "" { 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() { func declareStaticRoutes(router *gin.Engine) {
Router().GET("/@fs/*_", serveOrReverse("")) router.GET("/@fs/*_", serveOrReverse(""))
Router().GET("/", serveOrReverse("")) router.GET("/", serveOrReverse(""))
Router().GET("/_app/*_", serveOrReverse("")) router.GET("/_app/*_", serveOrReverse(""))
Router().GET("/auth", serveOrReverse("/")) router.GET("/auth/", serveOrReverse("/"))
Router().GET("/bug-bounty", serveOrReverse("/")) router.GET("/bug-bounty", serveOrReverse("/"))
Router().GET("/grades", serveOrReverse("/")) router.GET("/grades", serveOrReverse("/"))
Router().GET("/help", serveOrReverse("/")) router.GET("/help", serveOrReverse("/"))
Router().GET("/surveys", serveOrReverse("/")) router.GET("/surveys", serveOrReverse("/"))
Router().GET("/surveys/*_", serveOrReverse("/")) router.GET("/surveys/*_", serveOrReverse("/"))
Router().GET("/users", serveOrReverse("/")) router.GET("/users", serveOrReverse("/"))
Router().GET("/users/*_", serveOrReverse("/")) router.GET("/users/*_", serveOrReverse("/"))
Router().GET("/works", serveOrReverse("/")) router.GET("/works", serveOrReverse("/"))
Router().GET("/works/*_", serveOrReverse("/")) router.GET("/works/*_", serveOrReverse("/"))
Router().GET("/css/*_", serveOrReverse("")) router.GET("/css/*_", serveOrReverse(""))
Router().GET("/fonts/*_", serveOrReverse("")) router.GET("/fonts/*_", serveOrReverse(""))
Router().GET("/img/*_", 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 package main
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var ( var (
_score_cache = map[int64]map[int64]*float64{} _score_cache = map[int64]map[int64]*float64{}
) )
func init() { func declareAPISurveysRoutes(router *gin.RouterGroup) {
router.GET("/api/surveys", apiAuthHandler( router.GET("/surveys", func(c *gin.Context) {
func(u *User, _ httprouter.Params, _ []byte) HTTPResponse { u := c.MustGet("LoggedUser").(*User)
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}
}
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 { for _, s := range surveys {
if s.Group == "" || strings.Contains(u.Groups, ","+s.Group+",") { if s.Group == "" || strings.Contains(u.Groups, ","+s.Group+",") {
s.Group = "" s.Group = ""
response = append(response, s) 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 var new Survey
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
if new.Promo == 0 { if new.Promo == 0 {
new.Promo = currentPromo new.Promo = currentPromo
} }
return formatApiResponse(NewSurvey(new.Title, new.Promo, new.Group, new.Shown, new.Direct, new.StartAvailability, new.EndAvailability)) if s, err := NewSurvey(new.Title, new.Promo, new.Group, new.Shown, new.Direct, new.StartAvailability, new.EndAvailability); err != nil {
}, adminRestricted)) log.Println("Unable to NewSurvey:", err)
router.GET("/api/surveys/:sid", apiAuthHandler(surveyAuthHandler( c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during survey creation: %s", err.Error())})
func(s Survey, u *User, _ []byte) HTTPResponse { return
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}
} else { } else {
return APIErrorResponse{ c.JSON(http.StatusOK, s)
status: http.StatusForbidden,
err: errors.New("Not accessible"),
} }
} })
})))
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 var new Survey
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
new.Id = current.Id new.Id = current.Id
@ -91,72 +156,41 @@ func init() {
} }
} }
return formatApiResponse(new.Update()) if survey, err := new.Update(); err != nil {
}), adminRestricted)) log.Println("Unable to Update survey:", err)
router.DELETE("/api/surveys/:sid", apiHandler(surveyHandler( c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during survey updation: %s", err.Error())})
func(s Survey, _ []byte) HTTPResponse { return
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"}}
} else { } 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 { } else {
return APIErrorResponse{ c.JSON(http.StatusOK, nil)
status: http.StatusForbidden,
err: errors.New("Not accessible"),
} }
} })
}), loggedUser))
router.GET("/api/users/:uid/surveys/:sid/score", apiAuthHandler(func(uauth *User, ps httprouter.Params, body []byte) HTTPResponse { declareAPIAdminAsksRoutes(surveysRoutes)
return surveyAuthHandler(func(s Survey, uauth *User, _ []byte) HTTPResponse { declareAPIAdminDirectRoutes(surveysRoutes)
return userHandler(func(u User, _ []byte) HTTPResponse { declareAPIAdminQuestionsRoutes(surveysRoutes)
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))
} }
func surveyHandler(f func(Survey, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { func surveyHandler(c *gin.Context) {
return func(ps httprouter.Params, body []byte) HTTPResponse { if sid, err := strconv.Atoi(string(c.Param("sid"))); err != nil {
if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad survey identifier."})
return APIErrorResponse{err: err} return
} else if survey, err := getSurvey(sid); err != nil { } else if survey, err := getSurvey(sid); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Survey not found."})
return
} else { } else {
return f(survey, body) c.Set("survey", survey)
} c.Next()
}
}
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)
}
} }
} }
@ -172,7 +206,7 @@ type Survey struct {
EndAvailability time.Time `json:"end_availability"` 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 { 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 return nil, errr
} else { } 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 { 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 return
} }
surveys = append(surveys, s) surveys = append(surveys, &s)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return 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) 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 return
} }

151
users.go
View File

@ -1,49 +1,106 @@
package main package main
import ( import (
"encoding/json" "log"
"net/http"
"strconv" "strconv"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
var currentPromo uint = 0 var currentPromo uint = 0
func init() { func declareAPIAuthUsersRoutes(router *gin.RouterGroup) {
router.GET("/api/promos", apiHandler( usersRoutes := router.Group("/users/:uid")
func(httprouter.Params, []byte) HTTPResponse { usersRoutes.Use(userHandler)
return formatApiResponse(getPromos()) usersRoutes.Use(sameUserMiddleware)
}, adminRestricted))
router.GET("/api/users", apiHandler( usersRoutes.GET("", func(c *gin.Context) {
func(httprouter.Params, []byte) HTTPResponse { u := c.MustGet("user").(*User)
return formatApiResponse(getUsers())
}, adminRestricted)) c.JSON(http.StatusOK, u)
router.GET("/api/users/:uid", apiHandler(userHandler( })
func(u User, _ []byte) HTTPResponse {
return APIResponse{u} declareAPIAuthSurveysRoutes(usersRoutes)
}), 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 userHandler(f func(User, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { func declareAPIAdminUsersRoutes(router *gin.RouterGroup) {
return func(ps httprouter.Params, body []byte) HTTPResponse { router.GET("/promos", func(c *gin.Context) {
if uid, err := strconv.Atoi(string(ps.ByName("uid"))); err != nil { promos, err := getPromos()
if user, err := getUserByLogin(ps.ByName("uid")); err != nil { if err != nil {
return APIErrorResponse{err: err} 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 { } else {
return f(user, body) user, err = getUser(uid)
} if err != nil {
} else if user, err := getUser(uid); err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "User not found."})
return APIErrorResponse{err: err} return
} else {
return f(user, body)
} }
} }
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 { 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) 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 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) 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 return
} }
@ -116,14 +175,14 @@ func userExists(login string) bool {
return err == nil && z == 1 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() 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 { 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 { } else if sid, err := res.LastInsertId(); err != nil {
return User{}, err return nil, err
} else { } 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 var new User
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
current.Login = new.Login current.Login = new.Login
@ -179,5 +241,12 @@ func updateUser(current User, body []byte) HTTPResponse {
current.Time = new.Time current.Time = new.Time
current.Promo = new.Promo current.Promo = new.Promo
current.Groups = new.Groups 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 ( import (
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/gin-gonic/gin"
) )
func init() { func declareAPIWorksRoutes(router *gin.RouterGroup) {
router.GET("/api/works", apiAuthHandler( router.GET("/works", func(c *gin.Context) {
func(u *User, _ httprouter.Params, _ []byte) HTTPResponse { var u *User
if u == nil { if user, ok := c.Get("LoggedUser"); ok {
return formatApiResponse(getWorks(fmt.Sprintf("WHERE shown = TRUE AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo))) u = user.(*User)
} 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}
} }
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 { for _, w := range works {
if w.Group == "" || strings.Contains(u.Groups, ","+w.Group+",") { if w.Group == "" || strings.Contains(u.Groups, ","+w.Group+",") {
w.Group = "" w.Group = ""
response = append(response, w) response = append(response, w)
} }
} }
return formatApiResponse(response, nil)
} }
}))
router.GET("/api/all_works", apiAuthHandler( c.JSON(http.StatusOK, response)
func(u *User, _ httprouter.Params, _ []byte) HTTPResponse { })
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 { 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 { } 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 { } 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)) 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}
} }
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 { for _, w := range works {
if w.Group == "" || strings.Contains(u.Groups, ","+w.Group+",") { if w.Group == "" || strings.Contains(u.Groups, ","+w.Group+",") {
w.Group = "" w.Group = ""
response = append(response, w) 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 var new Work
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
if new.Promo == 0 { if new.Promo == 0 {
new.Promo = currentPromo new.Promo = currentPromo
} }
return formatApiResponse(NewWork(new.Title, new.Promo, new.Group, new.Shown, new.SubmissionURL, new.StartAvailability, new.EndAvailability)) work, err := NewWork(new.Title, new.Promo, new.Group, new.Shown, new.SubmissionURL, new.StartAvailability, new.EndAvailability)
}, adminRestricted)) if err != nil {
router.GET("/api/works/:wid", apiAuthHandler(workAuthHandler( log.Println("Unable to NewWork:", err)
func(w Work, u *User, _ []byte) HTTPResponse { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during work creation"})
if u.IsAdmin { return
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")}
} }
}), 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 var new Work
if err := json.Unmarshal(body, &new); err != nil { if err := c.ShouldBindJSON(&new); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
} }
new.Id = current.Id new.Id = current.Id
return formatApiResponse(new.Update()) work, err := 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()
if err != nil { 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 var grades []WorkGrade
if err := json.Unmarshal(body, &grades); err != nil { if err := c.ShouldBindJSON(&grades); err != nil {
return APIErrorResponse{err: err} 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) err = w.AddGrades(grades)
if err != nil { 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} c.JSON(http.StatusOK, 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) { func declareAPIAuthWorksRoutes(router *gin.RouterGroup) {
return APIErrorResponse{status: http.StatusNotFound, err: fmt.Errorf("Aucune note n'a été attribuée pour ce travail. Avez-vous rendu ce travail ?")} 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 { } 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 { } else {
return APIResponse{g} c.JSON(http.StatusOK, g)
} }
}), loggedUser)) })
} }
func workHandler(f func(Work, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { func workHandler(c *gin.Context) {
return func(ps httprouter.Params, body []byte) HTTPResponse { if wid, err := strconv.Atoi(string(c.Param("wid"))); err != nil {
if wid, err := strconv.Atoi(string(ps.ByName("wid"))); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad work identifier."})
return APIErrorResponse{err: err} return
} else if work, err := getWork(wid); err != nil { } else if work, err := getWork(wid); err != nil {
return APIErrorResponse{err: err} c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Work not found."})
return
} else { } else {
return f(work, body) c.Set("work", work)
} c.Next()
}
}
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)
}
} }
} }
@ -171,7 +246,7 @@ type OneWork struct {
EndAvailability time.Time `json:"end_availability"` 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 { 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 return nil, errr
} else { } 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 { 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 return
} }
items = append(items, w) items = append(items, &w)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return return
@ -204,7 +279,7 @@ type Work struct {
EndAvailability time.Time `json:"end_availability"` 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 { 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 return nil, errr
} else { } 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 { 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 return
} }
items = append(items, w) items = append(items, &w)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return 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) 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 return
} }