package gitea import ( "bytes" "code.gitea.io/sdk/gitea" "code.mrmelon54.com/melon/tools/module/gitea/assets" "code.mrmelon54.com/melon/tools/utils" "context" _ "embed" "encoding/gob" "fmt" "github.com/google/uuid" "github.com/gorilla/mux" "golang.org/x/oauth2" "gopkg.in/yaml.v3" "html/template" "io" "net/http" "os" "path/filepath" "strings" "time" ) var ( //go:embed pages/index.go.html indexTemplate string //go:embed pages/waffle.go.html waffleTemplate string returnCookie = "melon-tools-return-gitea" ) type Module struct { sessionWrapper func(cb func(http.ResponseWriter, *http.Request, *utils.State)) func(rw http.ResponseWriter, req *http.Request) oauthClient *oauth2.Config } type giteaKeyType int const ( KeyOauthClient = giteaKeyType(iota) KeyUser KeyState KeyAccessToken KeyRefreshToken ) func New() *Module { gob.Register(new(giteaKeyType)) return &Module{} } func (m *Module) GetName() string { return "Gitea" } func (m *Module) GetEndpoint() string { return "/gitea" } func (m *Module) SetupModule(router *mux.Router, f func(cb func(http.ResponseWriter, *http.Request, *utils.State)) func(rw http.ResponseWriter, req *http.Request)) { m.sessionWrapper = f m.oauthClient = &oauth2.Config{ ClientID: os.Getenv("GITEA_CLIENT_ID"), ClientSecret: os.Getenv("GITEA_CLIENT_SECRET"), Scopes: []string{"openid"}, Endpoint: oauth2.Endpoint{ AuthURL: os.Getenv("GITEA_AUTHORIZE_URL"), TokenURL: os.Getenv("GITEA_TOKEN_URL"), }, RedirectURL: os.Getenv("GITEA_REDIRECT_URL"), } router.HandleFunc("/", m.getClient(m.homepage)) router.HandleFunc("/login", m.sessionWrapper(m.loginPage)) router.PathPrefix("/spec/{org}/{repo}/{sha}/{spec:.+}").HandlerFunc(m.getClient(m.specPage)) router.PathPrefix("/spec-raw/{org}/{repo}/{sha}/{spec:.+}").HandlerFunc(m.getClient(m.specRawPage)) router.PathPrefix("/waffle").HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { p := filepath.Join("waffle", "assets", filepath.Base(req.URL.Path)) open, err := assets.WaffleAssets.Open(p) if err != nil { http.NotFound(rw, req) return } stat, err := open.Stat() if err != nil { http.NotFound(rw, req) return } seeker, ok := open.(io.ReadSeeker) if ok { http.ServeContent(rw, req, p, stat.ModTime(), seeker) } else { http.NotFound(rw, req) } }) } func (m *Module) getClient(cb func(http.ResponseWriter, *http.Request, *utils.State, *gitea.Client)) func(rw http.ResponseWriter, req *http.Request) { return m.sessionWrapper(func(rw http.ResponseWriter, req *http.Request, state *utils.State) { if v, ok := utils.GetStateValue[*gitea.Client](state, KeyOauthClient); ok { cb(rw, req, state, v) return } http.SetCookie(rw, &http.Cookie{ Name: returnCookie, Value: req.RequestURI, Path: "/gitea", Expires: time.Now().Add(time.Hour * 1), MaxAge: 3600, }) http.Redirect(rw, req, "/gitea/login", http.StatusTemporaryRedirect) }) } func (m *Module) homepage(rw http.ResponseWriter, req *http.Request, state *utils.State, giteaClient *gitea.Client) { cookie, err := req.Cookie(returnCookie) if err == nil { if cookie.Valid() != nil { http.SetCookie(rw, &http.Cookie{Name: returnCookie, Value: "", Path: "/gitea", Expires: time.Now().Add(-time.Hour), MaxAge: 0}) http.Redirect(rw, req, cookie.Value, http.StatusTemporaryRedirect) return } } myUser, _, err := giteaClient.GetMyUserInfo() if err != nil { state.Del(KeyOauthClient) http.Error(rw, err.Error(), http.StatusInternalServerError) return } orgs, _, err := giteaClient.ListMyOrgs(gitea.ListOrgsOptions{}) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } orgSimple := make([]struct{ Name string }, len(orgs)) for i, j := range orgs { orgSimple[i] = struct{ Name string }{j.UserName} } selOrg := "" myOrg := "" repoSimple := make([]struct { Name string Private bool }, 0) selRepo := "" selModule := "" selCommitTime := "" selCommitHash := "" selCommitHashShort := "" mySpecs := make([]struct { Name string Code int }, 0) q := req.URL.Query() if q.Has("org") { selOrg = q.Get("org") var repos []*gitea.Repository if selOrg == "!me" { myOrg = myUser.UserName repos, _, err = giteaClient.ListMyRepos(gitea.ListReposOptions{ListOptions: gitea.ListOptions{Page: 0, PageSize: 50}}) } else { myOrg = selOrg repos, _, err = giteaClient.ListOrgRepos(myOrg, gitea.ListOrgReposOptions{ListOptions: gitea.ListOptions{Page: 0, PageSize: 50}}) } if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } repoSimple = make([]struct { Name string Private bool }, len(repos)) for i, j := range repos { repoSimple[i] = struct { Name string Private bool }{Name: j.Name, Private: j.Private} } if q.Has("repo") { selRepo = q.Get("repo") repo, resp, err := giteaClient.GetRepo(myOrg, selRepo) if err != nil { if resp.StatusCode != http.StatusNotFound { http.Error(rw, "GetRepo: "+err.Error(), http.StatusInternalServerError) return } } refs, resp, err := giteaClient.GetRepoRefs(myOrg, selRepo, "heads/"+repo.DefaultBranch) if err != nil { if resp.StatusCode != http.StatusNotFound { http.Error(rw, "GetRepoRefs: "+err.Error(), http.StatusInternalServerError) return } refs = make([]*gitea.Reference, 0) } if len(refs) == 1 { ref := refs[0] commit, _, err := giteaClient.GetSingleCommit(myOrg, selRepo, ref.Object.SHA) if err != nil { http.Error(rw, "GetSingleCommit: "+err.Error(), http.StatusInternalServerError) return } selCommitTime = commit.CommitMeta.Created.UTC().Format("20060102150405") selCommitHash = commit.CommitMeta.SHA selCommitHashShort = commit.CommitMeta.SHA[:12] goMod, resp, err := giteaClient.GetFile(myOrg, selRepo, ref.Object.SHA, "go.mod") if err != nil { if resp.StatusCode != http.StatusNotFound { http.Error(rw, "go.mod: "+err.Error(), http.StatusInternalServerError) return } } goModStr := string(goMod) goModIdx := strings.Index(goModStr, "\n") goModLine := goModStr[:goModIdx] goModSpace := strings.Index(goModLine, " ") selModule = goModLine[goModSpace+1:] if resp.StatusCode == http.StatusNotFound { selModule = "" } trees, resp, err := giteaClient.GetTrees(myOrg, selRepo, ref.Object.SHA, true) if err != nil { if resp.StatusCode != http.StatusNotFound { http.Error(rw, "%s: "+err.Error(), http.StatusInternalServerError) return } } for _, i := range trees.Entries { switch filepath.Ext(i.Path) { case ".yml", ".yaml": file, resp, err := giteaClient.GetFile(myOrg, selRepo, ref.Object.SHA, i.Path) if err != nil { switch resp.StatusCode { case http.StatusForbidden, http.StatusUnauthorized, http.StatusNotFound: mySpecs = append(mySpecs, struct { Name string Code int }{Name: i.Path, Code: resp.StatusCode}) } continue } a := struct { OpenAPI string `yaml:"openapi"` }{} err = yaml.Unmarshal(file, &a) if err != nil || a.OpenAPI == "" { continue } mySpecs = append(mySpecs, struct { Name string Code int }{Name: i.Path, Code: http.StatusOK}) } } } } } tmp, err := template.New("homepage").Parse(indexTemplate) if err != nil { fmt.Println("Template parse error:", err) return } err = tmp.Execute(rw, struct { Username string Orgs []struct{ Name string } Repos []struct { Name string Private bool } MyOrg string SelOrg string SelRepo string ShowOrg bool SelModule string ShowGoMod bool CommitTime string CommitHash string CommitHashShort string ShowSpec bool Specs []struct { Name string Code int } }{ Username: myUser.UserName, Orgs: orgSimple, Repos: repoSimple, MyOrg: myOrg, SelOrg: selOrg, SelRepo: selRepo, SelModule: selModule, ShowOrg: myOrg != "", ShowGoMod: selModule != "", CommitTime: selCommitTime, CommitHash: selCommitHash, CommitHashShort: selCommitHashShort, ShowSpec: len(mySpecs) > 0, Specs: mySpecs, }) if err != nil { fmt.Println("Template execute error:", err) return } } func (m *Module) loginPage(rw http.ResponseWriter, req *http.Request, state *utils.State) { if myUser, ok := utils.GetStateValue[*string](state, KeyUser); ok { if myUser != nil { http.Redirect(rw, req, "/gitea", http.StatusTemporaryRedirect) return } } if flowState, ok := utils.GetStateValue[uuid.UUID](state, KeyState); ok { q := req.URL.Query() if q.Has("code") && q.Has("state") { if q.Get("state") == flowState.String() { exchange, err := m.oauthClient.Exchange(context.Background(), q.Get("code")) if err != nil { fmt.Println("Exchange token error:", err) return } c, err := gitea.NewClient(os.Getenv("GITEA_SERVER"), gitea.SetToken(exchange.AccessToken)) if err != nil { fmt.Println("Create client error:", err) return } state.Put(KeyOauthClient, c) state.Put(KeyAccessToken, exchange.AccessToken) state.Put(KeyRefreshToken, exchange.RefreshToken) http.Redirect(rw, req, "/gitea", http.StatusTemporaryRedirect) return } http.Error(rw, "OAuth flow state doesn't match\n", http.StatusBadRequest) return } } flowState := uuid.New() state.Put(KeyState, flowState) http.Redirect(rw, req, m.oauthClient.AuthCodeURL(flowState.String(), oauth2.AccessTypeOffline), http.StatusTemporaryRedirect) } func (m *Module) specPage(rw http.ResponseWriter, req *http.Request, state *utils.State, giteaClient *gitea.Client) { vars := mux.Vars(req) myOrg := vars["org"] myRepo := vars["repo"] mySha := vars["sha"] mySpec := vars["spec"] contents, _, err := giteaClient.GetContents(myOrg, myRepo, mySha, mySpec) if err != nil { http.Error(rw, "OpenAPI spec: "+err.Error(), http.StatusInternalServerError) return } tmp, err := template.New("waffle").Parse(waffleTemplate) if err != nil { fmt.Println("Template parse error:", err) return } err = tmp.Execute(rw, struct { LoadUrls []struct { Url string `json:"url"` Name string `json:"name"` } LoadMain string }{ LoadUrls: []struct { Url string `json:"url"` Name string `json:"name"` }{ {Url: fmt.Sprintf("/gitea/spec-raw/%s/%s/%s/%s", myOrg, myRepo, mySha, mySpec), Name: contents.Name}, }, LoadMain: contents.Name, }) if err != nil { fmt.Println("Template execute error:", err) return } } func (m *Module) specRawPage(rw http.ResponseWriter, req *http.Request, state *utils.State, giteaClient *gitea.Client) { vars := mux.Vars(req) myOrg := vars["org"] myRepo := vars["repo"] mySha := vars["sha"] mySpec := vars["spec"] open, _, err := giteaClient.GetFile(myOrg, myRepo, mySha, mySpec) if err != nil { http.Error(rw, "OpenAPI spec raw: "+err.Error(), http.StatusInternalServerError) return } http.ServeContent(rw, req, mySpec, time.Now(), bytes.NewReader(open)) }