diff --git a/auth/client.go b/auth/client.go deleted file mode 100644 index 7200c34..0000000 --- a/auth/client.go +++ /dev/null @@ -1,42 +0,0 @@ -// staletea -// Copyright (C) 2019 Jonas Franz -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package auth - -import ( - "fmt" - "gitea.com/jonasfranz/staletea/config" - "golang.org/x/oauth2" -) - -func endpoint() oauth2.Endpoint { - // Endpoint is Google's OAuth 2.0 endpoint. - return oauth2.Endpoint{ - AuthURL: fmt.Sprintf("%s/login/oauth/authorize", config.GiteaURL.Get().(string)), - TokenURL: fmt.Sprintf("%s/login/oauth/access_token", config.GiteaURL.Get().(string)), - AuthStyle: oauth2.AuthStyleInParams, - } -} - -// Config returns an oauth2 config -func Config() *oauth2.Config { - return &oauth2.Config{ - ClientID: config.GiteaClientID.Get().(string), - ClientSecret: config.GiteaClientSecret.Get().(string), - Endpoint: endpoint(), - RedirectURL: fmt.Sprintf("%s/callback", config.BaseURL.Get().(string)), - } -} diff --git a/cmd/run.go b/cmd/run.go index 850e079..aa126a6 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -17,7 +17,8 @@ package cmd import ( - "gitea.com/jonasfranz/staletea/web" + "gitea.com/jonasfranz/staletea/config" + "gitea.com/jonasfranz/staletea/jobs" "github.com/urfave/cli" ) @@ -25,14 +26,13 @@ import ( var CmdRun = cli.Command{ Action: run, Name: "run", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "address", - Value: ":3030", - }, - }, + Flags: []cli.Flag{}, } func run(ctx *cli.Context) error { - return web.StartServer(ctx.String("address")) + println("Startup!") + if err := config.SetupConfig(); err != nil { + return err + } + return jobs.StartCronJobs() } diff --git a/config/config.go b/config/config.go index ee1358b..d86fe65 100644 --- a/config/config.go +++ b/config/config.go @@ -41,56 +41,40 @@ func (k Key) setDefault(defaultValue interface{}) { // This block contains all config keys const ( - DaysUntilStale Key = "daysUntilStale" - DaysUntilClose Key = "daysUntilClose" - LabelBlacklist Key = "onlyLabels" - LabelWhitelist Key = "exemptLabels" - StaleLabel Key = "staleLabel" + GiteaURL Key = "gitea_url" + BotAccessToken Key = "gitea_bot_access_token" - MarkComment Key = "markComment" - UnmarkComment Key = "unmarkComment" - CloseComment Key = "closeComment" + SessionSecret Key = "session_secret" + DatabaseDriver Key = "database_driver" + DatabaseURI Key = "database_uri" - TypeBlacklist Key = "only" + RepositoryScanInterval Key = "repository_scan_interval" + IssueScanInterval Key = "issue_scan_interval" - BaseURL Key = "host" - - GiteaURL Key = "gitea.url" - GiteaClientID Key = "gitea.client_id" - GiteaClientSecret Key = "gitea.client_secret" - - SessionSecret Key = "session_secret" - DatabasePath Key = "./database_path" + RepositoryConfigFileName Key = "repository_config_file_name" ) // SetupConfig fills all default values, reads the config and/or writes the default config. func SetupConfig() error { - DaysUntilStale.setDefault(60) - DaysUntilClose.setDefault(7) - LabelBlacklist.setDefault([]string{}) - LabelWhitelist.setDefault([]string{}) - StaleLabel.setDefault("wontfix") - - MarkComment.setDefault(`This issue has been automatically marked as stale because it has not had - recent activity. It will be closed in %d days if no further activity occurs. Thank you - for your contributions.`) - UnmarkComment.setDefault("") - CloseComment.setDefault("This issue was closed automatically since it was marked as stale and had no recent activity.") - TypeBlacklist.setDefault([]string{}) - BaseURL.setDefault("http://localhost") GiteaURL.setDefault("https://gitea.com") - GiteaClientID.setDefault("") - GiteaClientSecret.setDefault("") + BotAccessToken.setDefault("YOUR_BOT_TOKEN") secret, err := utils.NewSecret(32) if err != nil { return err } SessionSecret.setDefault(secret) - DatabasePath.setDefault("staletea.db") + DatabaseDriver.setDefault("sqlite3") + DatabaseURI.setDefault("staletea.db") + + RepositoryScanInterval.setDefault("@every 30s") + IssueScanInterval.setDefault("@every 2m") + + RepositoryConfigFileName.setDefault(".stale.yml") viper.SetConfigName("config") viper.SetConfigType("yml") + viper.AutomaticEnv() viper.AddConfigPath(".") if err := viper.SafeWriteConfigAs("config.yml"); err != nil { if os.IsNotExist(err) { diff --git a/go.mod b/go.mod index f3a1abf..5f4098c 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/lib/pq v1.1.0 // indirect github.com/mattn/go-sqlite3 v1.10.0 github.com/pkg/errors v0.8.1 // indirect + github.com/robfig/cron v1.2.0 github.com/spf13/viper v1.4.0 github.com/urfave/cli v1.20.0 golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 // indirect @@ -19,4 +20,5 @@ require ( golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed // indirect golang.org/x/text v0.3.2 // indirect + gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index cfed8b3..5f366f6 100644 --- a/go.sum +++ b/go.sum @@ -137,6 +137,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= diff --git a/jobs/issue_sync.go b/jobs/issue_sync.go new file mode 100644 index 0000000..a768a42 --- /dev/null +++ b/jobs/issue_sync.go @@ -0,0 +1,21 @@ +// staletea +// Copyright (C) 2020 Jonas Franz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package jobs + +func syncIssues() error { + return nil +} diff --git a/jobs/job.go b/jobs/job.go new file mode 100644 index 0000000..9c99ccc --- /dev/null +++ b/jobs/job.go @@ -0,0 +1,51 @@ +// staletea +// Copyright (C) 2020 Jonas Franz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package jobs + +import "github.com/robfig/cron" + +import "gitea.com/jonasfranz/staletea/config" + +type Job struct { + Interval config.Key + Action func() error +} + +var registeredCronJobs []Job = []Job{ + {Interval: config.RepositoryScanInterval, Action: syncRepositories}, + {Interval: config.IssueScanInterval, Action: syncIssues}, +} + +func handleError(unsafeFunc func() error) func() { + return func() { + err := unsafeFunc() + if err != nil { + panic(err) + } + } +} + +func StartCronJobs() error { + c := cron.New() + for _, job := range registeredCronJobs { + if err := c.AddFunc(job.Interval.Get().(string), handleError(job.Action)); err != nil { + return err + } + } + c.Run() + return nil +} diff --git a/jobs/repository_sync.go b/jobs/repository_sync.go new file mode 100644 index 0000000..c6ec506 --- /dev/null +++ b/jobs/repository_sync.go @@ -0,0 +1,56 @@ +// staletea +// Copyright (C) 2020 Jonas Franz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package jobs + +import ( + "fmt" + "gitea.com/jonasfranz/staletea/config" + "gitea.com/jonasfranz/staletea/loader" + "gitea.com/jonasfranz/staletea/models" +) + +func syncRepositories() error { + savedRepos, err := models.FindAllRepositories() + if err != nil { + return err + } + savedRepoIDs := make(map[int64]*models.Repository) + for _, savedRepo := range savedRepos { + savedRepoIDs[savedRepo.ID] = savedRepo + } + l := loader.NewGiteaLoader(config.GiteaURL.Get().(string), config.BotAccessToken.Get().(string)) + loadedRepos, err := l.LoadRepositories() + if err != nil { + return err + } + newRepos := make([]*models.Repository, 0) + for _, loadedRepo := range loadedRepos { + if _, ok := savedRepoIDs[loadedRepo.ID]; !ok { + repoConfig, err := l.DownloadConfig(loadedRepo) + if err != nil { + fmt.Printf("cannot download config for repo %s/%s: %v\n", loadedRepo.Owner, loadedRepo.Name, err) + continue + } + newRepos = append(newRepos, &models.Repository{ + ID: loadedRepo.ID, + Config: repoConfig, + Activated: true, + }) + } + } + return models.CreateRepositories(newRepos) +} diff --git a/loader/gitea.go b/loader/gitea.go new file mode 100644 index 0000000..f8753b4 --- /dev/null +++ b/loader/gitea.go @@ -0,0 +1,63 @@ +// staletea +// Copyright (C) 2020 Jonas Franz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package loader + +import ( + "bytes" + "code.gitea.io/sdk/gitea" + "gitea.com/jonasfranz/staletea/config" + "gopkg.in/yaml.v2" +) + +func NewGiteaLoader(url, token string) Loader { + return &GiteaLoader{ + client: gitea.NewClient(url, token), + } +} + +type GiteaLoader struct { + client *gitea.Client +} + +func (l *GiteaLoader) LoadRepositories() ([]*Repository, error) { + repos, err := l.client.ListMyRepos() + if err != nil { + return nil, err + } + convertedRepos := make([]*Repository, len(repos)) + for index, repo := range repos { + convertedRepos[index] = &Repository{ + ID: repo.ID, + Owner: repo.Owner.UserName, + Name: repo.Name, + ConfigBranch: repo.DefaultBranch, + } + } + return convertedRepos, nil +} + +func (l *GiteaLoader) DownloadConfig(repo *Repository) (*RepositoryConfig, error) { + file, err := l.client.GetFile(repo.Owner, repo.Name, repo.ConfigBranch, config.RepositoryConfigFileName.Get().(string)) + if err != nil { + return nil, err + } + repoConfig := new(RepositoryConfig) + if err := yaml.NewDecoder(bytes.NewReader(file)).Decode(repoConfig); err != nil { + return nil, err + } + return repoConfig, nil +} diff --git a/loader/loader.go b/loader/loader.go new file mode 100644 index 0000000..7c229b6 --- /dev/null +++ b/loader/loader.go @@ -0,0 +1,22 @@ +// staletea +// Copyright (C) 2020 Jonas Franz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package loader + +type Loader interface { + LoadRepositories() ([]*Repository, error) + DownloadConfig(repo *Repository) (*RepositoryConfig, error) +} diff --git a/models/user.go b/loader/models.go similarity index 51% rename from models/user.go rename to loader/models.go index 356423d..7170a3a 100644 --- a/models/user.go +++ b/loader/models.go @@ -1,5 +1,5 @@ // staletea -// Copyright (C) 2019 Jonas Franz +// Copyright (C) 2020 Jonas Franz // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,26 +14,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package models +package loader -import ( - "code.gitea.io/sdk/gitea" - "context" - "gitea.com/jonasfranz/staletea/auth" - "gitea.com/jonasfranz/staletea/config" - "golang.org/x/oauth2" -) - -// User represents a signed in oauth2 gitea user saved in a session -type User struct { - ID int64 - Username string - Token *oauth2.Token +type Repository struct { + ID int64 + Owner string + Name string + ConfigBranch string } -// GiteaClient will return a gitea client with authentication of the user -func (user *User) GiteaClient() *gitea.Client { - client := gitea.NewClient(config.GiteaURL.Get().(string), "") - client.SetHTTPClient(auth.Config().Client(context.Background(), user.Token)) - return client +type RepositoryConfig struct { + Enabled bool `yaml:"enabled"` } diff --git a/models/models.go b/models/models.go index 6543f20..fe14a3a 100644 --- a/models/models.go +++ b/models/models.go @@ -30,7 +30,7 @@ var x *xorm.Engine // SetupDatabase will init a sqlite3 database and sync all models func SetupDatabase() { var err error - x, err = xorm.NewEngine("sqlite3", config.DatabasePath.Get().(string)) + x, err = xorm.NewEngine("sqlite3", config.DatabaseURI.Get().(string)) if err != nil { panic(err) } diff --git a/models/repository.go b/models/repository.go index 265c807..a0c580f 100644 --- a/models/repository.go +++ b/models/repository.go @@ -16,24 +16,36 @@ package models -import "github.com/go-xorm/xorm" +import ( + "gitea.com/jonasfranz/staletea/loader" + "github.com/go-xorm/xorm" +) // Repository represents a Gitea repository indexed at the local database type Repository struct { - ID int64 `xorm:"pk"` - UserID int64 `xorm:"index"` - Owner string `xorm:"unique(owner_name)"` - Name string `xorm:"unique(owner_name)"` + ID int64 `xorm:"pk"` + Config *loader.RepositoryConfig `xorm:"json"` Activated bool } -// FindRepositoriesByUserID returns all repos of an user -func FindRepositoriesByUserID(userID int64) ([]*Repository, error) { - return findRepositoriesByUserID(x, userID) +func FindAllRepositories() ([]*Repository, error) { + return findAllRepositories(x) } -func findRepositoriesByUserID(e *xorm.Engine, userID int64) ([]*Repository, error) { +func findAllRepositories(e *xorm.Engine) ([]*Repository, error) { repos := make([]*Repository, 0) - return repos, e.Where("user_id = ?", userID).Find(&repos) + if err := e.Find(&repos); err != nil { + return nil, err + } + return repos, nil +} + +func CreateRepositories(repos []*Repository) error { + return createRepositories(x, repos) +} + +func createRepositories(e *xorm.Engine, repos []*Repository) error { + _, err := e.Insert(&repos) + return err } diff --git a/web/auth.go b/web/auth.go deleted file mode 100644 index 697eaa3..0000000 --- a/web/auth.go +++ /dev/null @@ -1,83 +0,0 @@ -// staletea -// Copyright (C) 2019 Jonas Franz -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package web - -import ( - "code.gitea.io/sdk/gitea" - "context" - "gitea.com/jonasfranz/staletea/auth" - "gitea.com/jonasfranz/staletea/config" - "gitea.com/jonasfranz/staletea/models" - "gitea.com/jonasfranz/staletea/utils" - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" - "net/http" -) - -func handleLogin(ctx *gin.Context) { - session := sessions.Default(ctx) - state, err := utils.NewSecret(32) - if err != nil { - _ = ctx.AbortWithError(500, err) - return - } - session.Set("state", state) - if err := session.Save(); err != nil { - _ = ctx.AbortWithError(http.StatusInternalServerError, err) - return - } - redirectURL := auth.Config().AuthCodeURL(state) - ctx.Redirect(http.StatusTemporaryRedirect, redirectURL) -} - -func handleCallback(ctx *gin.Context) { - session := sessions.Default(ctx) - state := ctx.Query("state") - savedState := session.Get("state") - if savedState == nil { - ctx.String(http.StatusUnauthorized, "Invalid state") - return - } - session.Delete("state") - if parsedState, ok := savedState.(string); !ok || parsedState != state { - ctx.String(http.StatusUnauthorized, "Invalid state") - return - } - token, err := auth.Config().Exchange(ctx, ctx.Query("code")) - if err != nil { - _ = ctx.AbortWithError(http.StatusUnauthorized, err) - return - } - client := gitea.NewClient(config.GiteaURL.Get().(string), "") - client.SetHTTPClient(auth.Config().Client(context.Background(), token)) - user, err := client.GetMyUserInfo() - if err != nil { - _ = ctx.AbortWithError(http.StatusUnauthorized, err) - return - } - storedUser := &models.User{ - ID: user.ID, - Username: user.UserName, - Token: token, - } - session.Set("user", storedUser) - if err := session.Save(); err != nil { - _ = ctx.AbortWithError(http.StatusInternalServerError, err) - return - } - ctx.Redirect(http.StatusTemporaryRedirect, config.BaseURL.Get().(string)) -} diff --git a/web/dashboard.go b/web/dashboard.go deleted file mode 100644 index 8ee0091..0000000 --- a/web/dashboard.go +++ /dev/null @@ -1,71 +0,0 @@ -// staletea -// Copyright (C) 2019 Jonas Franz -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package web - -import ( - "fmt" - "gitea.com/jonasfranz/staletea/config" - "gitea.com/jonasfranz/staletea/models" - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" - "net/http" -) - -func showDashboard(ctx *gin.Context) { - session := sessions.Default(ctx) - user, ok := session.Get("user").(*models.User) - if !ok || user == nil { - ctx.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login", config.BaseURL.Get().(string))) - return - } - activatedRepos, err := models.FindRepositoriesByUserID(user.ID) - if err != nil { - _ = ctx.AbortWithError(500, err) - return - } - remoteRepos, err := user.GiteaClient().ListMyRepos() - if err != nil { - _ = ctx.AbortWithError(500, err) - return - } - combinedRepos := make(map[int64]*models.Repository, len(activatedRepos)) - - for _, repo := range activatedRepos { - combinedRepos[repo.ID] = repo - } - - for _, repo := range remoteRepos { - combinedRepos[repo.ID] = &models.Repository{ - ID: repo.ID, - UserID: user.ID, - Owner: repo.Owner.UserName, - Name: repo.Name, - Activated: false, - } - } - - data := map[string]interface{}{ - "repos": combinedRepos, - "user": user, - } - - ctx.HTML(200, "dashboard.tmpl", data) -} - -func handleActivate(ctx *gin.Context) { - -} diff --git a/web/router.go b/web/router.go deleted file mode 100644 index c789d33..0000000 --- a/web/router.go +++ /dev/null @@ -1,44 +0,0 @@ -// staletea -// Copyright (C) 2019 Jonas Franz -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package web - -import ( - "encoding/gob" - "gitea.com/jonasfranz/staletea/config" - "gitea.com/jonasfranz/staletea/models" - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" - "github.com/gin-gonic/gin" -) - -// StartServer will start the webserver at the given addresses -func StartServer(addr ...string) error { - r := gin.Default() - gob.Register(new(models.User)) - store := cookie.NewStore([]byte(config.SessionSecret.Get().(string))) - r.Use(sessions.Sessions("sessions", store)) - r.LoadHTMLFiles("templates/dashboard.tmpl") - setupRoutes(r) - return r.Run(addr...) -} - -func setupRoutes(r *gin.Engine) { - r.GET("/", showDashboard) - r.POST("/", handleActivate) - r.GET("/login", handleLogin) - r.GET("/callback", handleCallback) -}