diff --git a/.gitignore b/.gitignore
index 3434c45..c3c0b13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
atsebay.t
bindata.go
-vendor
\ No newline at end of file
+vendor
+.gitlab-oauth-token
\ No newline at end of file
diff --git a/api.go b/api.go
index a5c700e..7336023 100644
--- a/api.go
+++ b/api.go
@@ -15,6 +15,12 @@ func declareAPIRoutes(router *gin.Engine) {
declareAPISurveysRoutes(apiRoutes)
declareAPIWorksRoutes(apiRoutes)
+ authRoutes := router.Group("")
+ authRoutes.Use(authMiddleware(loggedUser))
+ adminRoutes := router.Group("")
+ adminRoutes.Use(authMiddleware(adminRestricted))
+ initializeGitLabOIDC(router, authRoutes, adminRoutes)
+
apiAuthRoutes := router.Group("/api")
apiAuthRoutes.Use(authMiddleware(loggedUser))
@@ -43,6 +49,7 @@ func declareAPIRoutes(router *gin.Engine) {
declareAPIAuthGradesRoutes(apiAdminRoutes)
declareAPIAdminHelpRoutes(apiAdminRoutes)
declareAPIAdminQuestionsRoutes(apiAdminRoutes)
+ declareAPIAuthRepositoriesRoutes(apiAdminRoutes)
declareAPIAdminSurveysRoutes(apiAdminRoutes)
declareAPIAdminUsersRoutes(apiAdminRoutes)
declareAPIAdminWorksRoutes(apiAdminRoutes)
diff --git a/db.go b/db.go
index 1352077..14335c6 100644
--- a/db.go
+++ b/db.go
@@ -223,6 +223,20 @@ CREATE TABLE IF NOT EXISTS user_work_grades(
FOREIGN KEY(id_user) REFERENCES users(id_user),
FOREIGN KEY(id_work) REFERENCES works(id_work)
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
+`); err != nil {
+ return err
+ }
+ if _, err := db.Exec(`
+CREATE TABLE IF NOT EXISTS user_work_repositories(
+ id_repository INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ id_user INTEGER NOT NULL,
+ id_work INTEGER NOT NULL,
+ uri VARCHAR(255) NOT NULL,
+ last_check TIMESTAMP,
+ FOREIGN KEY(id_user) REFERENCES users(id_user),
+ FOREIGN KEY(id_work) REFERENCES works(id_work),
+ UNIQUE one_repo_per_work (id_user, id_work)
+) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
`); err != nil {
return err
}
diff --git a/gitlab.go b/gitlab.go
new file mode 100644
index 0000000..e9e0ea7
--- /dev/null
+++ b/gitlab.go
@@ -0,0 +1,163 @@
+package main
+
+import (
+ "context"
+ "encoding/hex"
+ "encoding/json"
+ "flag"
+ "log"
+ "net/http"
+ "os"
+
+ "golang.org/x/oauth2"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ OAUTH_GITLAB_FILE = ".gitlab-oauth-token"
+)
+
+var (
+ gitlabBaseURL = "https://gitlab.cri.epita.fr"
+ gitlabClientID = ""
+ gitlabSecret = ""
+ gitlaboauth2Config oauth2.Config
+ gitlaboidcVerifier *oidc.IDTokenVerifier
+ gitlabToken *oauth2.Token
+)
+
+func init() {
+ flag.StringVar(&gitlabClientID, "gitlab-clientid", gitlabClientID, "ClientID for GitLab's OIDC")
+ flag.StringVar(&gitlabSecret, "gitlab-secret", gitlabSecret, "Secret for GitLab's OIDC")
+}
+
+func initializeGitLabOIDC(router *gin.Engine, authrouter *gin.RouterGroup, adminrouter *gin.RouterGroup) {
+ adminrouter.GET("/auth/gitlabcri", redirectOAuth_GitLab)
+ router.GET("/callback/gitlabcri/complete", GitLab_OAuth_complete)
+
+ if _, err := os.Stat(OAUTH_GITLAB_FILE); err == nil {
+ gitlabToken, err = loadOAuth2Token(OAUTH_GITLAB_FILE)
+ if err != nil {
+ log.Println("Unable to load OAuth2 Token:", err.Error())
+ }
+ }
+
+ if gitlabClientID != "" && gitlabSecret != "" {
+ provider, err := oidc.NewProvider(context.Background(), gitlabBaseURL)
+ if err != nil {
+ log.Fatal("Unable to setup oidc:", err)
+ }
+
+ gitlaboauth2Config = oauth2.Config{
+ ClientID: gitlabClientID,
+ ClientSecret: gitlabSecret,
+ RedirectURL: oidcRedirectURL + baseURL + "/callback/gitlabcri/complete",
+
+ // Discovery returns the OAuth2 endpoints.
+ Endpoint: provider.Endpoint(),
+
+ // "openid" is a required scope for OpenID Connect flows.
+ Scopes: []string{"api", "read_repository", "email"},
+ }
+
+ oidcConfig := oidc.Config{
+ ClientID: gitlabClientID,
+ }
+ gitlaboidcVerifier = provider.Verifier(&oidcConfig)
+
+ authrouter.GET("/api/gitlabcri/repositories", GitLab_getRepositories)
+ }
+}
+
+func loadOAuth2Token(file string) (*oauth2.Token, error) {
+ fd, err := os.Open(file)
+ if err != nil {
+ return nil, err
+ }
+ defer fd.Close()
+
+ var tok oauth2.Token
+ err = json.NewDecoder(fd).Decode(&tok)
+
+ return &tok, err
+}
+
+func saveOAuth2Token(file string, tok *oauth2.Token) error {
+ fd, err := os.Create(file)
+ if err != nil {
+ return err
+ }
+ defer fd.Close()
+
+ return json.NewEncoder(fd).Encode(tok)
+}
+
+func redirectOAuth_GitLab(c *gin.Context) {
+ session := c.MustGet("Session").(*Session)
+
+ // Save next parameter
+ if len(c.Request.URL.Query().Get("next")) > 0 {
+ session.SetKey("gitlab-oidc-source", c.Request.URL.Query().Get("next"))
+ }
+
+ c.Redirect(http.StatusFound, gitlaboauth2Config.AuthCodeURL(hex.EncodeToString(session.Id)))
+}
+
+func GitLab_OAuth_complete(c *gin.Context) {
+ idsession, err := hex.DecodeString(c.Request.URL.Query().Get("state"))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ session, err := getSession(idsession)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ oauth2Token, err := gitlaboauth2Config.Exchange(c.Request.Context(), c.Request.URL.Query().Get("code"))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to exchange token: " + err.Error()})
+ return
+ }
+
+ gitlabToken = oauth2Token
+ err = saveOAuth2Token(OAUTH_GITLAB_FILE, oauth2Token)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to save OAuth2 token: " + err.Error()})
+ return
+
+ }
+ log.Println("New GitLab OAuth2 session opened")
+
+ if source, ok := session.GetKey("gitlab-oidc-source"); ok {
+ session.DeleteKey("gitlab-oidc-source")
+ c.Redirect(http.StatusFound, baseURL+source.(string))
+ } else {
+ c.Redirect(http.StatusFound, baseURL+"/works")
+ }
+ session.Update()
+}
+
+func GitLab_getRepositories(c *gin.Context) {
+ client := gitlaboauth2Config.Client(c.Request.Context(), gitlabToken)
+
+ req, err := http.NewRequest("GET", gitlabBaseURL+"/api/v4/projects", nil)
+ if err != nil {
+ log.Println("Unable to create NewRequest before GitLab call: ", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when performing the GitLab request."})
+ return
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Println("Unable to perform the GitLab request: ", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when performing the GitLab request."})
+ return
+ }
+
+ c.DataFromReader(resp.StatusCode, resp.ContentLength, resp.Header.Get("content-type"), resp.Body, nil)
+}
diff --git a/repositories.go b/repositories.go
new file mode 100644
index 0000000..2df5341
--- /dev/null
+++ b/repositories.go
@@ -0,0 +1,207 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+func declareAPIAuthRepositoriesRoutes(router *gin.RouterGroup) {
+ router.GET("/repositories", func(c *gin.Context) {
+ var u *User
+ if user, ok := c.Get("user"); ok {
+ u = user.(*User)
+ } else {
+ u = c.MustGet("LoggedUser").(*User)
+ }
+
+ repositories, err := u.GetRepositories()
+ if err != nil {
+ log.Println("Unable to GetRepositories:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve your repositories. Please try again in a few moment."})
+ return
+ }
+
+ c.JSON(http.StatusOK, repositories)
+ })
+ router.POST("/repositories", func(c *gin.Context) {
+ var u *User
+ if user, ok := c.Get("user"); ok {
+ u = user.(*User)
+ } else {
+ u = c.MustGet("LoggedUser").(*User)
+ }
+
+ var repository Repository
+ if err := c.ShouldBindJSON(&repository); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ w := c.MustGet("work").(*Work)
+ k, err := u.NewRepository(w, repository.URI)
+ if err != nil {
+ log.Println("Unable to NewRepository:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register your public repository. Please try again in a few moment."})
+ return
+ }
+
+ c.JSON(http.StatusOK, k)
+ })
+
+ repositoriesRoutes := router.Group("/repositories/:rid")
+ repositoriesRoutes.Use(repositoryHandler)
+
+ repositoriesRoutes.GET("", func(c *gin.Context) {
+ repo := c.MustGet("repository").(*Repository)
+
+ c.JSON(http.StatusOK, repo)
+ })
+ repositoriesRoutes.PUT("", func(c *gin.Context) {
+ current := c.MustGet("repository").(*Repository)
+
+ var new Repository
+ if err := c.ShouldBindJSON(&new); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ new.Id = current.Id
+
+ u := c.MustGet("LoggedUser").(*User)
+ if new.IdUser != current.IdUser && !u.IsAdmin {
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Operation not allowed."})
+ return
+ }
+
+ if repository, err := new.Update(); err != nil {
+ log.Println("Unable to Update repository:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during repository updation: %s", err.Error())})
+ return
+ } else {
+ c.JSON(http.StatusOK, repository)
+ }
+ })
+ repositoriesRoutes.DELETE("", func(c *gin.Context) {
+ repository := c.MustGet("repository").(*Repository)
+
+ if _, err := repository.Delete(); err != nil {
+ log.Println("Unable to Delete repository:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during repository deletion: %s", err.Error())})
+ return
+ } else {
+ c.JSON(http.StatusOK, nil)
+ }
+ })
+}
+
+func repositoryHandler(c *gin.Context) {
+ var u *User
+ if user, ok := c.Get("user"); ok {
+ u = user.(*User)
+ } else {
+ u = c.MustGet("LoggedUser").(*User)
+ }
+
+ if rid, err := strconv.Atoi(string(c.Param("rid"))); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad repository identifier."})
+ return
+ } else if u.IsAdmin {
+ if repository, err := getRepository(rid); err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Repository not found."})
+ return
+ } else {
+ c.Set("repository", repository)
+ c.Next()
+ }
+ } else if repository, err := u.getRepository(rid); err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Repository not found."})
+ return
+ } else {
+ c.Set("repository", repository)
+ c.Next()
+ }
+}
+
+type Repository struct {
+ Id int64 `json:"id"`
+ IdUser int64 `json:"id_user"`
+ IdWork int64 `json:"id_work"`
+ URI string `json:"uri"`
+ LastCheck *time.Time `json:"last_check"`
+}
+
+func (u *User) GetRepositories() (repositories []*Repository, err error) {
+ if rows, errr := DBQuery("SELECT id_repository, id_user, id_work, uri, last_check FROM user_work_repositories WHERE id_user=?", u.Id); errr != nil {
+ return nil, errr
+ } else {
+ defer rows.Close()
+
+ for rows.Next() {
+ var repo Repository
+ if err = rows.Scan(&repo.Id, &repo.IdUser, &repo.IdWork, &repo.URI, &repo.LastCheck); err != nil {
+ return
+ }
+ repositories = append(repositories, &repo)
+ }
+ if err = rows.Err(); err != nil {
+ return
+ }
+
+ return
+ }
+}
+
+func getRepository(id int) (r *Repository, err error) {
+ r = new(Repository)
+ err = DBQueryRow("SELECT id_repository, id_user, id_work, uri, lastcheck FROM user_work_repositories WHERE id_repository=?", id).Scan(&r.Id, &r.IdUser, &r.IdWork, &r.URI, &r.LastCheck)
+ return
+}
+
+func (u *User) getRepository(id int) (r *Repository, err error) {
+ r = new(Repository)
+ err = DBQueryRow("SELECT id_repository, id_user, id_work, uri FROM user_work_repositories WHERE id_repository=? AND id_user=?", id, u.Id).Scan(&r.Id, &r.IdUser, &r.IdWork, &r.URI, &r.LastCheck)
+ return
+}
+
+func (u *User) NewRepository(w *Work, uri string) (*Repository, error) {
+ if res, err := DBExec("INSERT INTO user_work_repositories (id_user, id_work, uri) VALUES (?, ?, ?)", u.Id, w.Id, uri); err != nil {
+ return nil, err
+ } else if rid, err := res.LastInsertId(); err != nil {
+ return nil, err
+ } else {
+ return &Repository{rid, u.Id, w.Id, uri, nil}, nil
+ }
+}
+
+func (r *Repository) Update() (*Repository, error) {
+ if _, err := DBExec("UPDATE user_work_repositories SET id_user = ?, id_work = ?, uri = ?, last_check = ? WHERE id_repository = ?", r.IdUser, r.IdWork, r.URI, r.LastCheck, r.Id); err != nil {
+ return nil, err
+ } else {
+ return r, err
+ }
+}
+
+func (r Repository) Delete() (int64, error) {
+ if res, err := DBExec("DELETE FROM user_work_repositories WHERE id_repository = ?", r.Id); err != nil {
+ return 0, err
+ } else if nb, err := res.RowsAffected(); err != nil {
+ return 0, err
+ } else {
+ return nb, err
+ }
+}
+
+func ClearRepositories() (int64, error) {
+ if res, err := DBExec("DELETE FROM user_work_repositories"); err != nil {
+ return 0, err
+ } else if nb, err := res.RowsAffected(); err != nil {
+ return 0, err
+ } else {
+ return nb, err
+ }
+}
diff --git a/ui/src/routes/users/index.svelte b/ui/src/routes/users/index.svelte
index c7c6980..c20c526 100644
--- a/ui/src/routes/users/index.svelte
+++ b/ui/src/routes/users/index.svelte
@@ -13,7 +13,14 @@
{#if $user && $user.is_admin}
-
+
+
+ OAuth GitLab
+
+
{#await getPromos() then promos}
diff --git a/works.go b/works.go
index 13d93a7..f660ed9 100644
--- a/works.go
+++ b/works.go
@@ -217,6 +217,8 @@ func declareAPIAuthWorksRoutes(router *gin.RouterGroup) {
c.JSON(http.StatusOK, g)
}
})
+
+ declareAPIAuthRepositoriesRoutes(worksRoutes)
}
func workHandler(c *gin.Context) {