Compare commits

..

No commits in common. "develop" and "master" have entirely different histories.

23 changed files with 1 additions and 2741 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 Hammer Technologies LLC
Copyright (c) 2024 LumeWeb
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -1,82 +0,0 @@
package api
import (
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"go.uber.org/zap"
"gorm.io/gorm"
)
type manifest struct {
Name string `json:"name"`
Url string `json:"url"`
HookAttributes hookAttributes `json:"hook_attributes"`
Public bool `json:"public"`
RedirectURL string `json:"redirect_url"`
Version string `json:"version"`
DefaultPermissions permissions `json:"default_permissions"`
}
type hookAttributes struct {
URL string `json:"url"`
}
type permissions struct {
Issues string `json:"issues"`
Metadata string `json:"metadata"`
PullRequests string `json:"pull_requests"`
}
type settingsApi struct {
config *config.Config
db *gorm.DB
logger *zap.Logger
}
type appSecrets struct {
PrivateKey string `json:"private_key"`
Code string `json:"code"`
}
func newApp() appSecrets {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
// Marshal the private key into PKCS#1 ASN.1 DER encoded form
pkcs1Bytes := x509.MarshalPKCS1PrivateKey(privateKey)
// Create a PEM block with the private key
privateKeyPEM := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: pkcs1Bytes,
}
return appSecrets{
PrivateKey: string(pem.EncodeToMemory(privateKeyPEM)),
Code: generateTempCode(),
}
}
func generateTempCode() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
panic(err)
}
return hex.EncodeToString(bytes)
}
func generatePayloadSignature(payload []byte, secret string) string {
hasher := hmac.New(sha256.New, []byte(secret))
hasher.Write(payload)
sha := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf("sha256=%s", sha)
}

View File

@ -1,521 +0,0 @@
package api
import (
"code.gitea.io/gitea/modules/structs"
gitea "code.gitea.io/sdk/gitea"
"fmt"
"github.com/google/go-github/v59/github"
"time"
)
func stringPtr(s string) *string {
return &s
}
func numberPtr(n int) *int {
return &n
}
func intPtr(n int) *int {
return &n
}
func int64Ptr(n int64) *int64 {
return &n
}
func boolPtr(b bool) *bool {
return &b
}
func timePtr(t time.Time) *github.Timestamp {
timestamp := github.Timestamp{Time: t}
return &timestamp
}
func timePtrIfNotNil(t *time.Time) *github.Timestamp {
if t == nil {
return nil
}
return timePtr(*t)
}
func convertUser(user *gitea.User) *github.User {
if user == nil {
return &github.User{}
}
return &github.User{
Login: stringPtr(user.UserName),
ID: int64Ptr(user.ID),
}
}
func convertCoreUser(user *structs.User) *github.User {
if user == nil {
return &github.User{}
}
return &github.User{
Login: stringPtr(user.UserName),
ID: int64Ptr(user.ID),
}
}
func convertUsers(users []*gitea.User) []*github.User {
var ghUsers []*github.User
for _, user := range users {
ghUsers = append(ghUsers, convertUser(user))
}
return ghUsers
}
func convertCoreUsers(users []*structs.User) []*github.User {
var ghUsers []*github.User
for _, user := range users {
ghUsers = append(ghUsers, convertCoreUser(user))
}
return ghUsers
}
func convertMilestone(milestone *gitea.Milestone) *github.Milestone {
if milestone == nil {
return &github.Milestone{}
}
return &github.Milestone{
Title: stringPtr(milestone.Title),
}
}
func convertCoreMilestone(milestone *structs.Milestone) *github.Milestone {
if milestone == nil {
return &github.Milestone{}
}
return &github.Milestone{
Title: stringPtr(milestone.Title),
}
}
func convertLabels(labels []*gitea.Label) []*github.Label {
if labels == nil {
return make([]*github.Label, 0)
}
var ghLabels []*github.Label
for _, label := range labels {
if label == nil {
continue
}
ghLabels = append(ghLabels, &github.Label{
Name: stringPtr(label.Name),
})
}
return ghLabels
}
func convertCoreLabels(labels []*structs.Label) []*github.Label {
if labels == nil {
return make([]*github.Label, 0)
}
var ghLabels []*github.Label
for _, label := range labels {
if label == nil {
continue
}
ghLabels = append(ghLabels, &github.Label{
Name: stringPtr(label.Name),
})
}
return ghLabels
}
func convertPRBranch(branch *gitea.PRBranchInfo) *github.PullRequestBranch {
if branch == nil {
return &github.PullRequestBranch{}
}
return &github.PullRequestBranch{
Label: stringPtr(branch.Name),
Ref: stringPtr(branch.Ref),
SHA: stringPtr(branch.Sha),
Repo: convertRepo(branch.Repository),
}
}
func convertCorePRBranch(branch *structs.PRBranchInfo) *github.PullRequestBranch {
if branch == nil {
return &github.PullRequestBranch{}
}
return &github.PullRequestBranch{
Label: stringPtr(branch.Name),
Ref: stringPtr(branch.Ref),
SHA: stringPtr(branch.Sha),
Repo: convertCoreRepo(branch.Repository),
}
}
func convertRepo(repo *gitea.Repository) *github.Repository {
if repo == nil {
return &github.Repository{}
}
return &github.Repository{
ID: int64Ptr(repo.ID),
Name: stringPtr(repo.Name),
Owner: convertUser(repo.Owner),
FullName: stringPtr(repo.FullName),
Description: stringPtr(repo.Description),
Homepage: stringPtr(repo.Website),
HTMLURL: stringPtr(repo.HTMLURL),
CloneURL: stringPtr(repo.CloneURL),
GitURL: stringPtr(repo.CloneURL),
SSHURL: stringPtr(repo.SSHURL),
DefaultBranch: stringPtr(repo.DefaultBranch),
CreatedAt: timePtr(repo.Created),
UpdatedAt: timePtr(repo.Updated),
Private: boolPtr(repo.Private),
Fork: boolPtr(repo.Fork),
Size: intPtr(repo.Size),
StargazersCount: intPtr(repo.Stars),
SubscribersCount: intPtr(repo.Watchers),
ForksCount: intPtr(repo.Forks),
Watchers: intPtr(repo.Watchers),
WatchersCount: intPtr(repo.Stars),
OpenIssuesCount: intPtr(repo.OpenIssues),
Archived: boolPtr(repo.Archived),
}
}
func convertCoreRepo(repo *structs.Repository) *github.Repository {
if repo == nil {
return &github.Repository{}
}
return &github.Repository{
ID: int64Ptr(repo.ID),
Name: stringPtr(repo.Name),
Owner: convertCoreUser(repo.Owner),
FullName: stringPtr(repo.FullName),
Description: stringPtr(repo.Description),
Homepage: stringPtr(repo.Website),
HTMLURL: stringPtr(repo.HTMLURL),
CloneURL: stringPtr(repo.CloneURL),
GitURL: stringPtr(repo.CloneURL),
SSHURL: stringPtr(repo.SSHURL),
DefaultBranch: stringPtr(repo.DefaultBranch),
CreatedAt: timePtr(repo.Created),
UpdatedAt: timePtr(repo.Updated),
Private: boolPtr(repo.Private),
Fork: boolPtr(repo.Fork),
Size: intPtr(repo.Size),
StargazersCount: intPtr(repo.Stars),
SubscribersCount: intPtr(repo.Watchers),
ForksCount: intPtr(repo.Forks),
Watchers: intPtr(repo.Watchers),
WatchersCount: intPtr(repo.Stars),
OpenIssuesCount: intPtr(repo.OpenIssues),
Archived: boolPtr(repo.Archived),
}
}
func convertPullRequest(request *gitea.PullRequest) *github.PullRequest {
if request == nil {
return &github.PullRequest{}
}
pr := &github.PullRequest{
ID: int64Ptr(request.ID),
Number: intPtr(int(request.Index)),
State: stringPtr(string(request.State)),
Title: stringPtr(request.Title),
Body: stringPtr(request.Body),
CreatedAt: timePtr(*request.Created),
UpdatedAt: timePtr(*request.Updated),
ClosedAt: timePtrIfNotNil(request.Closed),
MergedAt: timePtrIfNotNil(request.Merged),
Merged: boolPtr(request.HasMerged),
Mergeable: boolPtr(request.Mergeable),
MergeCommitSHA: request.MergedCommitID,
URL: stringPtr(request.URL),
HTMLURL: stringPtr(request.HTMLURL),
DiffURL: stringPtr(request.DiffURL),
PatchURL: stringPtr(request.PatchURL),
Comments: intPtr(request.Comments),
Assignee: convertUser(request.Assignee),
Assignees: convertUsers(request.Assignees),
Milestone: convertMilestone(request.Milestone),
Labels: convertLabels(request.Labels),
}
// Convert PR branch info
if request.Head != nil {
pr.Head = convertPRBranch(request.Head)
}
if request.Base != nil {
pr.Base = convertPRBranch(request.Base)
}
return pr
}
func convertCorePullRequest(request *structs.PullRequest) *github.PullRequest {
if request == nil {
return &github.PullRequest{}
}
pr := &github.PullRequest{
ID: int64Ptr(request.ID),
Number: intPtr(int(request.Index)),
State: stringPtr(string(request.State)),
Title: stringPtr(request.Title),
Body: stringPtr(request.Body),
CreatedAt: timePtr(*request.Created),
UpdatedAt: timePtr(*request.Updated),
ClosedAt: timePtrIfNotNil(request.Closed),
MergedAt: timePtrIfNotNil(request.Merged),
Merged: boolPtr(request.HasMerged),
Mergeable: boolPtr(request.Mergeable),
MergeCommitSHA: request.MergedCommitID,
URL: stringPtr(request.URL),
HTMLURL: stringPtr(request.HTMLURL),
DiffURL: stringPtr(request.DiffURL),
PatchURL: stringPtr(request.PatchURL),
Comments: intPtr(request.Comments),
Assignee: convertCoreUser(request.Assignee),
Assignees: convertCoreUsers(request.Assignees),
Milestone: convertCoreMilestone(request.Milestone),
Labels: convertCoreLabels(request.Labels),
}
// Convert PR branch info
if request.Head != nil {
pr.Head = convertCorePRBranch(request.Head)
}
if request.Base != nil {
pr.Base = convertCorePRBranch(request.Base)
}
return pr
}
func convertChanges(changes *structs.ChangesPayload) *github.EditChange {
if changes == nil {
return &github.EditChange{}
}
return &github.EditChange{
Title: &github.EditTitle{
From: stringPtr(changes.Title.From),
},
Body: &github.EditBody{
From: stringPtr(changes.Body.From),
},
Base: &github.EditBase{
Ref: &github.EditRef{
From: stringPtr(changes.Ref.From),
},
},
}
}
func convertLabel(label *gitea.Label) *github.Label {
if label == nil {
return &github.Label{}
}
return &github.Label{
ID: int64Ptr(label.ID),
Name: stringPtr(label.Name),
Color: stringPtr(label.Color),
Description: stringPtr(label.Description),
URL: stringPtr(label.URL),
}
}
func convertCoreLabel(label *structs.Label) *github.Label {
if label == nil {
return &github.Label{}
}
return &github.Label{
ID: int64Ptr(label.ID),
Name: stringPtr(label.Name),
Color: stringPtr(label.Color),
Description: stringPtr(label.Description),
URL: stringPtr(label.URL),
}
}
func convertCommitFile(file *gitea.ChangedFile) *github.CommitFile {
if file == nil {
return &github.CommitFile{}
}
return &github.CommitFile{
Filename: stringPtr(file.Filename),
PreviousFilename: stringPtr(file.PreviousFilename),
Additions: intPtr(file.Additions),
Deletions: intPtr(file.Deletions),
Changes: intPtr(file.Changes),
Status: stringPtr(file.Status),
BlobURL: stringPtr(file.ContentsURL),
RawURL: stringPtr(file.RawURL),
}
}
func convertGitTree(tree *gitea.GitTreeResponse) *github.Tree {
if tree == nil {
return &github.Tree{}
}
return &github.Tree{
SHA: stringPtr(tree.SHA),
Truncated: boolPtr(tree.Truncated),
Entries: convertGitEntries(tree.Entries),
}
}
func convertGitEntries(entries []gitea.GitEntry) []*github.TreeEntry {
if entries == nil {
return make([]*github.TreeEntry, 0)
}
var ghEntries []*github.TreeEntry
for _, entry := range entries {
ghEntries = append(ghEntries, convertGitEntry(&entry))
}
return ghEntries
}
func convertGitEntry(s *gitea.GitEntry) *github.TreeEntry {
if s == nil {
return &github.TreeEntry{}
}
return &github.TreeEntry{
Path: stringPtr(s.Path),
Mode: stringPtr(s.Mode),
Type: stringPtr(s.Type),
Size: intPtr(int(s.Size)),
SHA: stringPtr(s.SHA),
URL: stringPtr(s.URL),
}
}
func convertIssueComment(comment *gitea.Comment, reactions []*gitea.Reaction) *github.IssueComment {
if comment == nil {
return &github.IssueComment{}
}
return &github.IssueComment{
ID: int64Ptr(comment.ID),
Body: stringPtr(comment.Body),
CreatedAt: timePtr(comment.Created),
UpdatedAt: timePtr(comment.Updated),
User: convertUser(comment.Poster),
Reactions: convertReactions(reactions),
}
}
func convertReactions(reactions []*gitea.Reaction) *github.Reactions {
if reactions == nil {
return &github.Reactions{}
}
return &github.Reactions{
TotalCount: intPtr(len(reactions)),
PlusOne: intPtr(countReaction(reactions, "+1")),
MinusOne: intPtr(countReaction(reactions, "-1")),
Laugh: intPtr(countReaction(reactions, "laugh")),
Confused: intPtr(countReaction(reactions, "confused")),
Heart: intPtr(countReaction(reactions, "heart")),
Rocket: intPtr(countReaction(reactions, "rocket")),
Eyes: intPtr(countReaction(reactions, "eyes")),
}
}
func countReaction(reactions []*gitea.Reaction, reactionType string) int {
count := 0
for _, reaction := range reactions {
if reaction.Reaction == reactionType {
count++
}
}
return count
}
func translatePrAction(action structs.HookIssueAction, prefix bool) string {
translatedAction := ""
switch action {
case structs.HookIssueOpened:
translatedAction = "opened"
case structs.HookIssueClosed:
translatedAction = "closed"
case structs.HookIssueReOpened:
translatedAction = "reopened"
case structs.HookIssueEdited:
translatedAction = "edited"
case structs.HookIssueAssigned:
translatedAction = "assigned"
case structs.HookIssueUnassigned:
translatedAction = "unassigned"
case structs.HookIssueLabelUpdated:
// GitHub does not have a direct "label_updated" event; use "labeled" as the closest action
translatedAction = "labeled" // Assuming you handle the update as adding a label
case structs.HookIssueLabelCleared:
// GitHub does not have a direct "label_cleared" event; use "unlabeled" as the closest action
translatedAction = "unlabeled" // Assuming you handle the clearing as removing a label
case structs.HookIssueSynchronized:
translatedAction = "synchronize"
case structs.HookIssueMilestoned:
translatedAction = "milestoned"
case structs.HookIssueDemilestoned:
translatedAction = "demilestoned"
case structs.HookIssueReviewed:
// GitHub does not have a direct "reviewed" event for PRs; this might be closest to a review submitted
translatedAction = "review_submitted" // This is not a direct GitHub event, consider how best to map this action
case structs.HookIssueReviewRequested:
translatedAction = "review_requested"
case structs.HookIssueReviewRequestRemoved:
translatedAction = "review_request_removed"
default:
// Fallback for any unhandled actions
translatedAction = "unknown_action"
}
if prefix {
translatedAction = fmt.Sprintf("pull_request.%s", translatedAction)
}
return translatedAction
}
func convertRelease(release *gitea.Release) *github.RepositoryRelease {
if release == nil {
return &github.RepositoryRelease{}
}
return &github.RepositoryRelease{
ID: int64Ptr(release.ID),
TagName: stringPtr(release.TagName),
TargetCommitish: stringPtr(release.Target),
Name: stringPtr(release.Title),
Body: stringPtr(release.Note),
Draft: boolPtr(release.IsDraft),
Prerelease: boolPtr(release.IsPrerelease),
CreatedAt: timePtr(release.CreatedAt),
PublishedAt: timePtr(release.PublishedAt),
Assets: convertReleaseAttachments(release.Attachments),
URL: stringPtr(release.URL),
ZipballURL: stringPtr(release.ZipURL),
TarballURL: stringPtr(release.TarURL),
HTMLURL: stringPtr(release.HTMLURL),
}
}
func convertReleaseAttachments(attachments []*gitea.Attachment) []*github.ReleaseAsset {
if attachments == nil {
return make([]*github.ReleaseAsset, 0)
}
var ghAttachments []*github.ReleaseAsset
for _, attachment := range attachments {
ghAttachments = append(ghAttachments, convertReleaseAttachment(attachment))
}
return ghAttachments
}
func convertReleaseAttachment(attachment *gitea.Attachment) *github.ReleaseAsset {
if attachment == nil {
return &github.ReleaseAsset{}
}
return &github.ReleaseAsset{
ID: int64Ptr(attachment.ID),
Name: stringPtr(attachment.Name),
Size: intPtr(int(attachment.Size)),
DownloadCount: intPtr(int(attachment.DownloadCount)),
BrowserDownloadURL: stringPtr(attachment.DownloadURL),
}
}

View File

@ -1,28 +0,0 @@
package api
import (
"code.gitea.io/gitea/modules/structs"
"github.com/google/go-github/v59/github"
)
func convertPullRequestEvent(event *structs.PullRequestPayload) *github.PullRequestEvent {
if len(event.PullRequest.RequestedReviewers) == 0 {
event.PullRequest.RequestedReviewers = append(event.PullRequest.RequestedReviewers, nil)
}
if len(event.PullRequest.Labels) == 0 {
event.PullRequest.Labels = append(event.PullRequest.Labels, nil)
}
return &github.PullRequestEvent{
Action: stringPtr(translatePrAction(event.Action, false)),
PullRequest: convertCorePullRequest(event.PullRequest),
Repo: convertCoreRepo(event.Repository),
Assignee: convertCoreUser(event.PullRequest.Assignee),
Number: intPtr(int(event.Index)),
Changes: convertChanges(event.Changes),
RequestedReviewer: convertCoreUser(event.PullRequest.RequestedReviewers[0]),
Sender: convertCoreUser(event.Sender),
Label: convertCoreLabel(event.PullRequest.Labels[0]),
}
}

View File

@ -1,43 +0,0 @@
package api
import (
"net/http"
"time"
)
func deleteCookie(w http.ResponseWriter, name string) {
cookie := http.Cookie{
Name: name,
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, &cookie)
}
func setAuthCookie(jwt string, domain string, w http.ResponseWriter) {
setCookie(w, AuthCookieName, domain, jwt, int(time.Hour.Seconds()), http.SameSiteNoneMode)
}
func setCookie(w http.ResponseWriter, name string, domain string, value string, maxAge int, sameSite http.SameSite) {
cookie := http.Cookie{
Name: name,
Domain: domain,
Value: value,
Path: "/",
HttpOnly: true,
MaxAge: maxAge,
Secure: true,
SameSite: sameSite,
}
http.SetCookie(w, &cookie)
}
func getCookie(r *http.Request, name string) string {
cookie, err := r.Cookie(name)
if err != nil {
return ""
}
return cookie.Value
}

View File

@ -1,291 +0,0 @@
package api
import (
"code.gitea.io/sdk/gitea"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
"github.com/golang-jwt/jwt"
"github.com/gorilla/mux"
"go.uber.org/zap"
"gorm.io/gorm"
"io"
"net/http"
"strconv"
"strings"
"time"
)
const AUTHED_CONTEXT_KEY = "authed"
const REDIRECT_AFTER_AUTH = "redirect-after-auth"
const WEBHOOK_CONTEXT_KEY = "webhook"
const AuthCookieName = "auth-token"
var _ = jwt.Claims(&standardClaims{})
type standardClaims struct {
Issuer any `json:"iss,omitempty"`
ExpiresAt any `json:"exp,omitempty"`
jwt.StandardClaims
}
func (s *standardClaims) Valid() error {
if timeStr, ok := s.ExpiresAt.(string); ok {
t, err := time.Parse(time.RFC3339Nano, timeStr)
if err != nil {
return err
}
unixTimestamp := t.Unix()
s.ExpiresAt = unixTimestamp
}
return s.StandardClaims.Valid()
}
func findAuthToken(r *http.Request) string {
authHeader := parseAuthTokenHeader(r.Header)
if authHeader != "" {
return authHeader
}
cookie := getCookie(r, AuthCookieName)
if cookie != "" {
return cookie
}
return r.FormValue(AuthCookieName)
}
func giteaOauthVerifyMiddleware(cfg *config.Config) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := findAuthToken(r)
if token == "" {
addAuthStatusToRequestServ(false, r, w, next)
return
}
client, err := getClient(ClientParams{
Config: cfg,
AuthToken: token,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, _, err = client.AdminListUsers(gitea.AdminListUsersOptions{})
if err != nil {
addAuthStatusToRequestServ(false, r, w, next)
return
}
addAuthStatusToRequestServ(true, r, w, next)
})
}
}
func githubRestVerifyMiddleware(db *gorm.DB) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := findAuthToken(r)
if token != "" {
parseToken, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
if err != nil {
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
return
}
claims, ok := parseToken.Claims.(jwt.MapClaims)
if !ok {
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
return
}
var appId string
switch v := claims["iss"].(type) {
case string:
appId = v
case float64:
appId = strconv.FormatFloat(v, 'f', -1, 64)
default:
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
return
}
appIdInt, err := strconv.Atoi(appId)
if err != nil {
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
return
}
appRecord := &model.Apps{}
appRecord.ID = uint(appIdInt)
if err := db.First(appRecord).Error; err != nil {
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
return
}
block, _ := pem.Decode([]byte(appRecord.PrivateKey))
if block == nil {
// Handle error
http.Error(w, "Invalid Private Key", http.StatusInternalServerError)
return
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
// Handle error
http.Error(w, "Failed to parse Private Key", http.StatusInternalServerError)
return
}
publicKey := &privateKey.PublicKey
parseToken, err = jwt.ParseWithClaims(token, &standardClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Return the RSA public key
return publicKey, nil
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if mux.CurrentRoute(r).GetName() == "app-install-get-access-token" {
installId := mux.Vars(r)["installation_id"]
installIdInt, err := strconv.Atoi(installId)
if err != nil {
http.Error(w, "Invalid Install", http.StatusUnauthorized)
return
}
if appIdInt != installIdInt {
http.Error(w, "Invalid Install", http.StatusUnauthorized)
return
}
}
}
addAuthStatusToRequestServ(true, r, w, next)
})
}
}
func storeWebhookDataMiddleware(logger *zap.Logger) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var webhook map[string]interface{}
body, err := io.ReadAll(r.Body)
if err != nil {
logger.Error("Failed to read request body", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
err = json.Unmarshal(body, &webhook)
if err != nil {
logger.Error("Failed to unmarshal webhook", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
if len(webhook) == 0 {
logger.Error("Webhook data is empty")
w.WriteHeader(http.StatusBadRequest)
return
}
ctx := context.WithValue(r.Context(), WEBHOOK_CONTEXT_KEY, body)
r = r.WithContext(ctx)
r.Body = io.NopCloser(strings.NewReader(string(body)))
next.ServeHTTP(w, r)
})
}
}
func requireAuthMiddleware(cfg *config.Config) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
status := getAuthedStatusFromRequest(r)
if !status {
setCookie(w, REDIRECT_AFTER_AUTH, cfg.Domain, r.Referer(), 0, http.SameSiteLaxMode)
http.Redirect(w, r, "/setup", http.StatusFound)
return
}
next.ServeHTTP(w, r)
})
}
}
func githubRestRequireAuthMiddleware(cfg *config.Config) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
status := getAuthedStatusFromRequest(r)
if !status {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
func loggingMiddleware(logger *zap.Logger) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do stuff here
logger.Debug("Request", zap.String("method", r.Method), zap.String("url", r.RequestURI))
// Call the next handler, which can be another middleware in the chain, or the final handler.
next.ServeHTTP(w, r)
})
}
}
func addAuthStatusToRequestServ(status bool, r *http.Request, w http.ResponseWriter, next http.Handler) {
ctx := context.WithValue(r.Context(), AUTHED_CONTEXT_KEY, status)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}
func parseAuthTokenHeader(headers http.Header) string {
authHeader := headers.Get("Authorization")
if authHeader == "" {
return ""
}
authHeader = strings.TrimPrefix(authHeader, "Bearer ")
authHeader = strings.TrimPrefix(authHeader, "bearer ")
authHeader = strings.TrimPrefix(authHeader, "Token ")
authHeader = strings.TrimPrefix(authHeader, "token ")
return authHeader
}
func getAuthedStatusFromRequest(r *http.Request) bool {
authed, ok := r.Context().Value(AUTHED_CONTEXT_KEY).(bool)
if !ok {
return false
}
return authed
}

View File

@ -1,187 +0,0 @@
package api
import (
"code.gitea.io/sdk/gitea"
"context"
"fmt"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"github.com/golang-jwt/jwt"
"go.uber.org/fx"
"go.uber.org/zap"
"golang.org/x/oauth2"
"time"
)
type Oauth struct {
cfg *config.Config
logger *zap.Logger
token *oauth2.Token
refresher oauth2.TokenSource
keepAliveRunning bool
oauthCfg *oauth2.Config
}
func NewOauth(lc fx.Lifecycle, cfg *config.Config, logger *zap.Logger) *Oauth {
oa := &Oauth{cfg: cfg, logger: logger}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
oa.config()
return nil
},
})
return oa
}
func (o Oauth) config() *oauth2.Config {
if o.oauthCfg == nil {
o.oauthCfg = &oauth2.Config{
ClientID: o.cfg.Oauth.ClientId,
ClientSecret: o.cfg.Oauth.ClientSecret,
Scopes: []string{"admin"},
RedirectURL: fmt.Sprintf("https://%s/setup/callback", o.cfg.Domain),
Endpoint: oauth2.Endpoint{
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", o.cfg.GiteaUrl),
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", o.cfg.GiteaUrl),
},
}
}
if o.loadToken(o.oauthCfg) {
o.keepAlive()
}
return o.oauthCfg
}
func (o Oauth) authUrl() string {
return o.config().AuthCodeURL("state")
}
func (o *Oauth) loadToken(config *oauth2.Config) bool {
token := &oauth2.Token{}
if o.cfg.Oauth.Token != "" {
o.token = &oauth2.Token{AccessToken: o.cfg.Oauth.Token}
}
if o.cfg.Oauth.RefreshToken != "" {
o.token.RefreshToken = o.cfg.Oauth.RefreshToken
}
if o.token != nil {
valid := false
parseToken, _, err := new(jwt.Parser).ParseUnverified(o.cfg.Oauth.Token, jwt.MapClaims{})
if err != nil {
o.logger.Error("Error parsing token", zap.Error(err))
} else {
// Assert the token's claims to the desired type (MapClaims in this case)
if claims, ok := parseToken.Claims.(jwt.MapClaims); ok {
if exp, ok := claims["exp"].(float64); ok {
expirationTime := time.Unix(int64(exp), 0)
if time.Now().Before(expirationTime) {
valid = true
o.token.Expiry = expirationTime
}
}
}
if valid {
token = o.token
} else {
o.logger.Info("Token is expired, ignoring")
return false
}
}
}
o.refresher = config.TokenSource(context.Background(), token)
return true
}
func (o Oauth) keepAlive() {
if o.cfg.Oauth.Token == "" || o.cfg.Oauth.RefreshToken == "" {
o.logger.Error("No token or refresh token provided.")
return
}
if o.refresher == nil {
o.logger.Error("No refresher provided.")
return
}
if o.keepAliveRunning {
return
}
ticker := time.NewTicker(30 * time.Minute)
o.keepAliveRunning = true
go func() {
for {
select {
case <-ticker.C:
if !o.isTokenValid() {
if err := o.refreshToken(); err != nil {
o.logger.Error("Error refreshing token", zap.Error(err))
}
}
}
}
}()
}
func (o *Oauth) isTokenValid() bool {
if o.token == nil {
return false
}
return o.token.Valid()
}
func (o *Oauth) refreshToken() error {
o.logger.Info("Refreshing token...")
token, err := o.refresher.Token()
if err != nil {
return err
}
o.token = token
return nil
}
func (o *Oauth) exchange(code string) (*oauth2.Token, error) {
cfg := o.config()
token, err := o.config().Exchange(context.Background(), code)
if err != nil {
return nil, err
}
o.cfg.Oauth.Token = token.AccessToken
o.cfg.Oauth.RefreshToken = token.RefreshToken
err = config.SaveConfig(o.cfg)
if err != nil {
return nil, err
}
o.loadToken(cfg)
o.keepAlive()
return token, nil
}
func (o *Oauth) client() *gitea.Client {
client, err := getClient(ClientParams{
Config: o.cfg,
})
if err != nil {
o.logger.Fatal("Error creating gitea client", zap.Error(err))
}
return client
}

View File

@ -1,91 +0,0 @@
package api
import (
"encoding/json"
"fmt"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
"github.com/gorilla/mux"
"go.uber.org/zap"
"gorm.io/gorm"
"net/http"
)
type manifests struct {
config *config.Config
db *gorm.DB
logger *zap.Logger
}
type createdApp struct {
Id uint `json:"id"`
PEM string `json:"pem"`
WebhookSecret string `json:"webhook_secret"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
HTMLUrl string `json:"html_url"`
}
func newManifests(config *config.Config, db *gorm.DB, logger *zap.Logger) *manifests {
return &manifests{
config: config,
db: db,
logger: logger,
}
}
func (m *manifests) handlerConversion(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
code := vars["code"]
if len(code) == 0 {
http.Error(w, "No code provided", http.StatusBadRequest)
return
}
appRecord := &model.Apps{Code: code}
if err := m.db.First(appRecord).Error; err != nil {
m.logger.Error("App not found", zap.Error(err))
http.Error(w, "App not found", http.StatusNotFound)
return
}
app := createdApp{
Id: appRecord.ID,
PEM: appRecord.PrivateKey,
ClientId: "",
ClientSecret: "",
WebhookSecret: appRecord.WebhookSecret,
HTMLUrl: fmt.Sprintf("https://%s/apps/%s", m.config.Domain, appRecord.Name),
}
appRecord.Code = generateTempCode()
tx := m.db.Save(appRecord)
if tx.Error != nil {
m.logger.Error("Error updating app", zap.Error(tx.Error))
http.Error(w, "Error updating app", http.StatusInternalServerError)
return
}
appData, err := json.Marshal(app)
if err != nil {
m.logger.Error("Error marshalling app", zap.Error(err))
http.Error(w, "Error marshalling app", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
w.Write(appData)
}
func setupManifestsRoutes(params RouteParams) {
r := params.R
manifestApi := newManifests(params.Config, params.Db, params.Logger)
manifestsRouter := r.PathPrefix("/api/v3/app-manifests").Subrouter()
manifestsRouter.HandleFunc("/{code}/conversions", manifestApi.handlerConversion).Methods("POST")
}

View File

@ -1,11 +0,0 @@
package api
import (
"github.com/gorilla/mux"
)
func NewRouter() *mux.Router {
r := mux.NewRouter()
return r
}

View File

@ -1,62 +0,0 @@
package api
import (
"code.gitea.io/sdk/gitea"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"github.com/gorilla/mux"
"go.uber.org/fx"
"go.uber.org/zap"
"gorm.io/gorm"
)
type RouteParams struct {
fx.In
Config *config.Config
Db *gorm.DB
Logger *zap.Logger
R *mux.Router
WebhookManager *WebhookManager
Oauth *Oauth
}
func SetupRoutes(params RouteParams) {
logger := params.Logger
r := params.R
r.Use(loggingMiddleware(logger))
setupApiRoutes(params)
setupSettingsRoutes(params)
setupManifestsRoutes(params)
setupAppRoutes(params)
setupAppInstallRoutes(params)
setupWebhookRoutes(params)
setupRestRoutes(params)
}
type ClientParams struct {
Config *config.Config
AuthToken string
}
func getClient(params ClientParams) (*gitea.Client, error) {
options := make([]gitea.ClientOption, 0)
authToken := ""
if len(params.AuthToken) > 0 {
authToken = params.AuthToken
}
if len(authToken) == 0 {
authToken = params.Config.Oauth.Token
}
options = append(options, gitea.SetToken(authToken))
client, err := gitea.NewClient(params.Config.GiteaUrl, options...)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -1,117 +0,0 @@
package api
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
"github.com/golang-jwt/jwt"
"github.com/gorilla/mux"
"go.uber.org/zap"
"gorm.io/gorm"
"net/http"
"time"
)
type appApi struct {
config *config.Config
db *gorm.DB
logger *zap.Logger
}
func newAppApi(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *appApi {
return &appApi{config: cfg, db: db, logger: logger}
}
func (a *appApi) handlerNewAppInstall(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("App installations are not needed on this proxy. All webhooks are broadcasted to all registered apps."))
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
}
func (a *appApi) handlerAppGetAccessToken(w http.ResponseWriter, r *http.Request) {
now := time.Now().Add(-30 * time.Second)
expirationTime := now.Add(100 * 365 * 24 * time.Hour)
appName := mux.Vars(r)["app"]
appRecord := &model.Apps{}
appRecord.Name = appName
if err := a.db.First(appRecord).Error; err != nil {
http.Error(w, "Failed to find app", http.StatusNotFound)
a.logger.Error("Failed to find app", zap.Error(err))
return
}
block, _ := pem.Decode([]byte(appRecord.PrivateKey))
if block == nil {
http.Error(w, "Failed to parse PEM block containing the key", http.StatusInternalServerError)
a.logger.Error("Failed to parse PEM block containing the key")
return
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
http.Error(w, "Failed to parse DER encoded private key", http.StatusInternalServerError)
a.logger.Error("Failed to parse DER encoded private key", zap.Error(err))
}
claims := jwt.MapClaims{
"iss": appRecord.ID,
"iat": now.Unix(),
"exp": expirationTime,
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signedToken, err := token.SignedString(privateKey)
if err != nil {
http.Error(w, "Failed to sign token", http.StatusInternalServerError)
a.logger.Error("Failed to sign token", zap.Error(err))
return
}
out := struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}{
Token: signedToken,
ExpiresAt: expirationTime,
}
a.respond(w, http.StatusCreated, out)
}
func (r appApi) respond(w http.ResponseWriter, status int, data interface{}) {
jsonData, err := json.Marshal(data)
if err != nil {
http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
r.logger.Error("Failed to marshal response", zap.Error(err))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if data != nil {
_, _ = w.Write(jsonData)
}
}
func setupAppRoutes(params RouteParams) {
logger := params.Logger
cfg := params.Config
db := params.Db
r := params.R
appApi := newAppApi(cfg, db, logger)
appRouter := r.PathPrefix("/apps").Subrouter()
appRouter.Use(giteaOauthVerifyMiddleware(cfg))
appRouter.Use(requireAuthMiddleware(cfg))
appRouter.HandleFunc("/{app}/installations/new", appApi.handlerNewAppInstall).Methods("GET")
appRouter.HandleFunc("/{app}/access_token", appApi.handlerAppGetAccessToken).Methods("GET")
}

View File

@ -1,119 +0,0 @@
package api
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
"github.com/golang-jwt/jwt"
"github.com/gorilla/mux"
"go.uber.org/zap"
"gorm.io/gorm"
"net/http"
"strconv"
"time"
)
type appInstallApi struct {
config *config.Config
logger *zap.Logger
db *gorm.DB
}
func newAppInstallApi(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *appInstallApi {
return &appInstallApi{config: cfg, logger: logger, db: db}
}
func (a *appInstallApi) handlerAppGetAccessToken(w http.ResponseWriter, r *http.Request) {
now := time.Now().Add(-30 * time.Second)
expirationTime := now.Add(10 * time.Minute)
appId := mux.Vars(r)["installation_id"]
appIdInt, err := strconv.ParseInt(appId, 10, 64)
if err != nil {
http.Error(w, "Failed to parse app id", http.StatusBadRequest)
a.logger.Error("Failed to parse app id", zap.Error(err))
return
}
appRecord := &model.Apps{}
appRecord.ID = uint(appIdInt)
if err := a.db.First(appRecord).Error; err != nil {
http.Error(w, "Failed to find app", http.StatusNotFound)
a.logger.Error("Failed to find app", zap.Error(err))
return
}
block, _ := pem.Decode([]byte(appRecord.PrivateKey))
if block == nil {
http.Error(w, "Failed to parse PEM block containing the key", http.StatusInternalServerError)
a.logger.Error("Failed to parse PEM block containing the key")
return
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
http.Error(w, "Failed to parse DER encoded private key", http.StatusInternalServerError)
a.logger.Error("Failed to parse DER encoded private key", zap.Error(err))
}
claims := jwt.MapClaims{
"iss": appId,
"iat": now.Unix(),
"exp": expirationTime,
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signedToken, err := token.SignedString(privateKey)
if err != nil {
http.Error(w, "Failed to sign token", http.StatusInternalServerError)
a.logger.Error("Failed to sign token", zap.Error(err))
return
}
out := struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}{
Token: signedToken,
ExpiresAt: expirationTime,
}
a.respond(w, http.StatusCreated, out)
}
func (r appInstallApi) respond(w http.ResponseWriter, status int, data interface{}) {
jsonData, err := json.Marshal(data)
if err != nil {
http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
r.logger.Error("Failed to marshal response", zap.Error(err))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if data != nil {
_, _ = w.Write(jsonData)
}
}
func setupAppInstallRoutes(params RouteParams) {
logger := params.Logger
cfg := params.Config
db := params.Db
r := params.R
appInstallApi := newAppInstallApi(cfg, db, logger)
appRouter := r.PathPrefix("/api/v3/app").Subrouter()
appRouter.Use(githubRestVerifyMiddleware(params.Db))
appRouter.Use(githubRestRequireAuthMiddleware(params.Config))
appRouter.HandleFunc("/installations/{installation_id}/access_tokens", appInstallApi.handlerAppGetAccessToken).Methods("POST").Name("app-install-get-access-token")
}

View File

@ -1,473 +0,0 @@
package api
import (
gitea "code.gitea.io/sdk/gitea"
"encoding/json"
"fmt"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"github.com/google/go-github/v59/github"
"github.com/gorilla/mux"
"go.uber.org/zap"
"net/http"
"strconv"
)
type restApi struct {
config *config.Config
logger *zap.Logger
}
func newRestApi(cfg *config.Config, logger *zap.Logger) *restApi {
return &restApi{config: cfg, logger: logger}
}
func (r restApi) handlerGetPullRequestFiles(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
pullNumber := vars["pull_number"]
client := r.getClientOrError(w)
if client == nil {
return
}
parsedPullNumber, err := strconv.ParseInt(pullNumber, 10, 64)
if err != nil {
http.Error(w, "Failed to parse pull number", http.StatusBadRequest)
r.logger.Error("Failed to parse pull number", zap.Error(err))
return
}
files, r2, err := client.ListPullRequestFiles(owner, repo, parsedPullNumber, gitea.ListPullRequestFilesOptions{
ListOptions: r.getPagingOptions(request),
})
if err != nil {
http.Error(w, "Failed to get pull request files", http.StatusInternalServerError)
r.logger.Error("Failed to get pull request files", zap.Error(err))
return
}
githubFiles := make([]*github.CommitFile, len(files))
for i, file := range files {
githubFiles[i] = convertCommitFile(file)
}
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusOK, githubFiles)
}
func (r restApi) getPagingOptions(request *http.Request) gitea.ListOptions {
page, _ := strconv.Atoi(request.URL.Query().Get("page"))
perPage, _ := strconv.Atoi(request.URL.Query().Get("per_page"))
return gitea.ListOptions{
Page: page,
PageSize: perPage,
}
}
func (r restApi) sendPagingHeaders(w http.ResponseWriter, apiResponse *gitea.Response) {
links := []string{}
baseURL := fmt.Sprintf("https://%s", r.config.Domain)
joinStrings := func(elements []string, separator string) string {
if len(elements) == 0 {
return ""
}
result := elements[0]
for i := 1; i < len(elements); i++ {
result += separator + elements[i]
}
return result
}
joinLinks := func(links []string) string {
return fmt.Sprintf("%s", joinStrings(links, ", "))
}
if apiResponse.FirstPage > 0 {
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="first"`, baseURL, apiResponse.FirstPage))
}
if apiResponse.PrevPage > 0 {
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="prev"`, baseURL, apiResponse.PrevPage))
}
if apiResponse.NextPage > 0 {
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="next"`, baseURL, apiResponse.NextPage))
}
if apiResponse.LastPage > 0 {
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="last"`, baseURL, apiResponse.LastPage))
}
if len(links) > 0 {
w.Header().Add("Link", fmt.Sprintf("%s", joinLinks(links)))
}
}
func (r restApi) respond(w http.ResponseWriter, status int, data interface{}) {
jsonData, err := json.Marshal(data)
if err != nil {
http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
r.logger.Error("Failed to marshal response", zap.Error(err))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if data != nil {
_, _ = w.Write(jsonData)
}
}
func (r restApi) getClientOrError(w http.ResponseWriter) *gitea.Client {
client, err := getClient(ClientParams{
Config: r.config,
})
if err != nil {
http.Error(w, "Failed to get Gitea client", http.StatusInternalServerError)
r.logger.Error("Failed to get Gitea client", zap.Error(err))
return nil
}
return client
}
func (r restApi) handlerGetTree(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
treeSha := vars["tree_sha"]
recursive := false
if request.URL.Query().Has("recursive") {
recursive = true
}
client := r.getClientOrError(w)
if client == nil {
return
}
tree, r2, err := client.GetTrees(owner, repo, treeSha, recursive)
if err != nil {
http.Error(w, "Failed to get tree", http.StatusInternalServerError)
r.logger.Error("Failed to get tree", zap.Error(err))
return
}
treeResponse := convertGitTree(tree)
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusOK, treeResponse)
}
func (r restApi) handleGetIssueComments(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
issueNumber := vars["issue_number"]
client := r.getClientOrError(w)
if client == nil {
return
}
issueNumberInt, err := strconv.Atoi(issueNumber)
if err != nil {
http.Error(w, "Failed to parse issue number", http.StatusBadRequest)
r.logger.Error("Failed to parse issue number", zap.Error(err))
return
}
comments, r2, err := client.ListIssueComments(owner, repo, int64(issueNumberInt), gitea.ListIssueCommentOptions{ListOptions: r.getPagingOptions(request)})
if err != nil {
http.Error(w, "Failed to get issue comments", http.StatusInternalServerError)
r.logger.Error("Failed to get issue comments", zap.Error(err))
return
}
ghComments := make([]*github.IssueComment, len(comments))
for i, comment := range comments {
reactions, _, _ := client.GetIssueCommentReactions(owner, repo, comment.ID)
ghComments[i] = convertIssueComment(comment, reactions)
}
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusOK, ghComments)
}
func (r restApi) handlerCreateIssueComment(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
issueNumber := vars["issue_number"]
client := r.getClientOrError(w)
if client == nil {
return
}
ghComment := github.IssueComment{}
if err := r.parseJsonBody(request, &ghComment); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
issueNumberInt, err := strconv.Atoi(issueNumber)
if err != nil {
http.Error(w, "Failed to parse issue number", http.StatusBadRequest)
r.logger.Error("Failed to parse issue number", zap.Error(err))
return
}
comment := gitea.CreateIssueCommentOption{
Body: ghComment.GetBody(),
}
commentResponse, _, err := client.CreateIssueComment(owner, repo, int64(issueNumberInt), comment)
if err != nil {
http.Error(w, "Failed to create comment", http.StatusInternalServerError)
r.logger.Error("Failed to create comment", zap.Error(err))
return
}
newGhComment := convertIssueComment(commentResponse, nil)
r.respond(w, http.StatusCreated, newGhComment)
}
func (r restApi) handlerUpdateIssueComment(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
commentID := vars["comment_id"]
client := r.getClientOrError(w)
if client == nil {
return
}
ghComment := github.IssueComment{}
if err := r.parseJsonBody(request, &ghComment); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
commentIDInt, err := strconv.Atoi(commentID)
if err != nil {
http.Error(w, "Failed to parse comment ID", http.StatusBadRequest)
r.logger.Error("Failed to parse comment ID", zap.Error(err))
return
}
comment := gitea.EditIssueCommentOption{
Body: ghComment.GetBody(),
}
commentResponse, _, err := client.EditIssueComment(owner, repo, int64(commentIDInt), comment)
if err != nil {
http.Error(w, "Failed to update comment", http.StatusInternalServerError)
r.logger.Error("Failed to update comment", zap.Error(err))
return
}
newGhComment := convertIssueComment(commentResponse, nil)
r.respond(w, http.StatusOK, newGhComment)
}
func (r restApi) handlerCreateRelease(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
client := r.getClientOrError(w)
if client == nil {
return
}
ghRelease := github.RepositoryRelease{}
if err := r.parseJsonBody(request, &ghRelease); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
release := gitea.CreateReleaseOption{
TagName: ghRelease.GetTagName(),
Target: ghRelease.GetTargetCommitish(),
Title: ghRelease.GetName(),
Note: ghRelease.GetBody(),
IsDraft: ghRelease.GetDraft(),
IsPrerelease: ghRelease.GetPrerelease(),
}
releaseResponse, r2, err := client.CreateRelease(owner, repo, release)
if err != nil {
http.Error(w, "Failed to create release", http.StatusInternalServerError)
r.logger.Error("Failed to create release", zap.Error(err))
return
}
newGhRelease := convertRelease(releaseResponse)
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusCreated, newGhRelease)
}
func (r restApi) handlerCreatePullRequest(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
client := r.getClientOrError(w)
if client == nil {
return
}
ghPullRequest := github.NewPullRequest{}
if err := r.parseJsonBody(request, &ghPullRequest); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
pullRequest := gitea.CreatePullRequestOption{
Title: ghPullRequest.GetTitle(),
Head: ghPullRequest.GetHead(),
Base: ghPullRequest.GetBase(),
Body: ghPullRequest.GetBody(),
}
pullRequestResponse, r2, err := client.CreatePullRequest(owner, repo, pullRequest)
if err != nil {
http.Error(w, "Failed to create pull request", http.StatusInternalServerError)
r.logger.Error("Failed to create pull request", zap.Error(err))
return
}
newGhPullRequest := convertPullRequest(pullRequestResponse)
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusCreated, newGhPullRequest)
}
func (r restApi) handlerUpdatePullRequest(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
pullNumber := vars["pull_number"]
parsedPullNumber, err := strconv.ParseInt(pullNumber, 10, 64)
if err != nil {
http.Error(w, "Failed to parse pull number", http.StatusBadRequest)
r.logger.Error("Failed to parse pull number", zap.Error(err))
return
}
client := r.getClientOrError(w)
if client == nil {
return
}
type pullRequestUpdate struct {
Title *string `json:"title,omitempty"`
Body *string `json:"body,omitempty"`
State *string `json:"state,omitempty"`
Base *string `json:"base,omitempty"`
MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"`
}
ghPullRequest := pullRequestUpdate{}
pullRequest := gitea.EditPullRequestOption{}
if err := r.parseJsonBody(request, &ghPullRequest); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
if ghPullRequest.State != nil {
if *ghPullRequest.State != "open" && *ghPullRequest.State != "closed" {
http.Error(w, "Invalid state", http.StatusBadRequest)
return
}
state := *ghPullRequest.State
stateType := gitea.StateType(state)
pullRequest.State = &stateType
}
if ghPullRequest.Title != nil {
pullRequest.Title = *ghPullRequest.Title
}
if ghPullRequest.Body != nil {
pullRequest.Body = *ghPullRequest.Body
}
pullRequestResponse, r2, err := client.EditPullRequest(owner, repo, parsedPullNumber, pullRequest)
if err != nil {
http.Error(w, "Failed to update pull request", http.StatusInternalServerError)
r.logger.Error("Failed to update pull request", zap.Error(err))
return
}
newGhPullRequest := convertPullRequest(pullRequestResponse)
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusOK, newGhPullRequest)
}
func (r restApi) parseJsonBody(request *http.Request, obj interface{}) error {
if obj == nil {
obj = make(map[string]interface{})
}
decoder := json.NewDecoder(request.Body)
return decoder.Decode(obj)
}
func setupRestRoutes(params RouteParams) {
logger := params.Logger
cfg := params.Config
r := params.R
restApi := newRestApi(cfg, logger)
setupRoutes := func(r *mux.Router) {
r.HandleFunc("/repos/{owner}/{repo}/pulls/{pull_number}/files", restApi.handlerGetPullRequestFiles).Methods("GET")
r.HandleFunc("/repos/{owner}/{repo}/git/trees/{tree_sha}", restApi.handlerGetTree).Methods("GET")
// Comment routes
r.HandleFunc("/repos/{owner}/{repo}/issues/{issue_number}/comments", restApi.handleGetIssueComments).Methods("GET")
r.HandleFunc("/repos/{owner}/{repo}/issues/{issue_number}/comments", restApi.handlerCreateIssueComment).Methods("POST")
r.HandleFunc("/repos/{owner}/{repo}/issues/comments/{comment_id}", restApi.handlerUpdateIssueComment).Methods("PATCH")
// Repo Release routes
r.HandleFunc("/repos/{owner}/{repo}/releases", restApi.handlerCreateRelease).Methods("POST")
// Pull Request routes
r.HandleFunc("/repos/{owner}/{repo}/pulls", restApi.handlerCreatePullRequest).Methods("POST")
r.HandleFunc("/repos/{owner}/{repo}/pulls/{pull_number}", restApi.handlerUpdatePullRequest).Methods("PATCH")
}
restRouter := r.PathPrefix("/api").Subrouter()
restRouter.Use(githubRestVerifyMiddleware(params.Db))
restRouter.Use(githubRestRequireAuthMiddleware(params.Config))
setupRoutes(restRouter)
setupRoutes(restRouter.PathPrefix("/v3").Subrouter())
}

View File

@ -1,83 +0,0 @@
package api
import (
"encoding/json"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
"go.uber.org/zap"
"gorm.io/gorm"
"net/http"
"net/url"
)
func newSettingsApi(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *settingsApi {
return &settingsApi{config: cfg, db: db, logger: logger}
}
func (s settingsApi) handlerNewApp(w http.ResponseWriter, r *http.Request) {
manifestData := r.FormValue("manifest")
var manifestObj manifest
err := json.Unmarshal([]byte(manifestData), &manifestObj)
if err != nil {
http.Error(w, "Failed to parse manifest", http.StatusBadRequest)
return
}
appData := newApp()
appRecord := &model.Apps{
Name: manifestObj.Name,
Url: manifestObj.Url,
WebhookUrl: manifestObj.HookAttributes.URL,
Code: generateTempCode(),
WebhookSecret: generateTempCode(),
PrivateKey: appData.PrivateKey,
}
tx := s.db.Create(appRecord)
if tx.Error != nil {
s.logger.Error("Error creating app", zap.Error(tx.Error))
http.Error(w, "Error creating app", http.StatusInternalServerError)
return
}
if len(manifestObj.RedirectURL) == 0 {
s.logger.Error("Redirect URL is required")
http.Error(w, "Redirect URL is required", http.StatusBadRequest)
return
}
redirectUrl, err := url.Parse(manifestObj.RedirectURL)
if err != nil {
s.logger.Error("Error parsing redirect URL", zap.Error(err))
http.Error(w, "Error parsing redirect URL", http.StatusInternalServerError)
return
}
query := redirectUrl.Query()
query.Add("code", appRecord.Code)
if r.URL.Query().Get("state") != "" {
query.Add("state", r.URL.Query().Get("state"))
}
redirectUrl.RawQuery = query.Encode()
http.Redirect(w, r, redirectUrl.String(), http.StatusFound)
}
func setupSettingsRoutes(params RouteParams) {
r := params.R
settingsRouter := r.PathPrefix("/settings").Subrouter()
settingsRouter.Use(giteaOauthVerifyMiddleware(params.Config))
settingsRouter.Use(requireAuthMiddleware(params.Config))
settingsApi := newSettingsApi(params.Config, params.Db, params.Logger)
settingsRouter.HandleFunc("/apps/new", settingsApi.handlerNewApp).Methods("POST")
}

View File

@ -1,70 +0,0 @@
package api
import (
"errors"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"go.uber.org/zap"
"net/http"
)
type setupApi struct {
config *config.Config
logger *zap.Logger
oauth *Oauth
}
func newSetupApi(config *config.Config, logger *zap.Logger, oauth *Oauth) *setupApi {
return &setupApi{config: config, logger: logger, oauth: oauth}
}
func (s setupApi) setupHandler(w http.ResponseWriter, r *http.Request) {
status := getAuthedStatusFromRequest(r)
if status {
redirectCookie := getCookie(r, REDIRECT_AFTER_AUTH)
if redirectCookie != "" {
deleteCookie(w, REDIRECT_AFTER_AUTH)
http.Redirect(w, r, redirectCookie, http.StatusFound)
return
}
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Setup is complete, you are authorized to use the proxy."))
return
}
http.Redirect(w, r, s.oauth.authUrl(), http.StatusFound)
}
func (s setupApi) callbackHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("error") != "" {
http.Error(w, errors.Join(errors.New("Error authorizing with Gitea: "), errors.New(r.URL.Query().Get("error"))).Error(), http.StatusBadRequest)
}
code := r.URL.Query().Get("code")
if len(code) == 0 {
http.Error(w, "No code provided", http.StatusBadRequest)
return
}
token, err := s.oauth.exchange(code)
if err != nil {
http.Error(w, "Failed to exchange code for token", http.StatusInternalServerError)
return
}
setAuthCookie(token.AccessToken, s.config.Domain, w)
http.Redirect(w, r, "/setup", http.StatusFound)
}
func setupApiRoutes(params RouteParams) {
r := params.R
setupRouter := r.PathPrefix("/setup").Subrouter()
setupRouter.Use(giteaOauthVerifyMiddleware(params.Config))
setupApi := newSetupApi(params.Config, params.Logger, params.Oauth)
setupRouter.HandleFunc("", setupApi.setupHandler).Methods("GET")
setupRouter.HandleFunc("/callback", setupApi.callbackHandler).Methods("GET")
}

View File

@ -1,73 +0,0 @@
package api
import (
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/sdk/gitea"
"encoding/json"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"go.uber.org/zap"
"gorm.io/gorm"
"net/http"
)
type webhookApi struct {
config *config.Config
logger *zap.Logger
db *gorm.DB
whm *WebhookManager
}
func newWebhookApi(cfg *config.Config, db *gorm.DB, logger *zap.Logger, whm *WebhookManager) *webhookApi {
return &webhookApi{config: cfg, db: db, logger: logger, whm: whm}
}
func (a *webhookApi) handlePullRequest(w http.ResponseWriter, r *http.Request) {
webhook := a.getWebhookData(w, r)
payload := &structs.PullRequestPayload{}
err := json.Unmarshal(webhook, payload)
if a.unmarshallError(w, err) {
return
}
a.whm.HandlePullRequest(payload, r)
}
func setupWebhookRoutes(params RouteParams) {
cfg := params.Config
db := params.Db
logger := params.Logger
r := params.R
whm := params.WebhookManager
webhookApi := newWebhookApi(cfg, db, logger, whm)
webhookRouter := r.PathPrefix("/api").Subrouter()
webhookRouter.Use(gitea.VerifyWebhookSignatureMiddleware(cfg.GiteaWebHookSecret))
webhookRouter.Use(storeWebhookDataMiddleware(logger))
webhookRouter.HandleFunc("/webhooks/pull_request", webhookApi.handlePullRequest).Methods("POST")
}
func (a *webhookApi) getWebhookData(w http.ResponseWriter, r *http.Request) []byte {
return getWebhookData(r, a.logger)
}
func (a *webhookApi) unmarshallError(w http.ResponseWriter, err error) bool {
if err != nil {
a.logger.Error("Failed to unmarshal webhook data", zap.Error(err))
http.Error(w, "Failed to unmarshal webhook data", http.StatusInternalServerError)
}
return err != nil
}
func getWebhookData(r *http.Request, logger *zap.Logger) []byte {
webhook := r.Context().Value(WEBHOOK_CONTEXT_KEY)
webhookData, ok := webhook.([]byte)
if !ok {
logger.Fatal("Failed to get webhook data from context")
}
return webhookData
}

View File

@ -1,133 +0,0 @@
package api
import (
"bytes"
giteaTypes "code.gitea.io/gitea/modules/structs"
"encoding/json"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
"go.uber.org/zap"
"gorm.io/gorm"
"io"
"net/http"
"regexp"
"strings"
)
type WebhookManager struct {
config *config.Config
db *gorm.DB
logger *zap.Logger
}
type IncomingWebhookData struct {
Headers http.Header
}
func NewWebhookManager(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *WebhookManager {
return &WebhookManager{config: cfg, db: db, logger: logger}
}
func (whm *WebhookManager) HandlePullRequest(request *giteaTypes.PullRequestPayload, r *http.Request) {
ghEvent := convertPullRequestEvent(request)
githubAction := translatePrAction(request.Action, true)
r.Header.Set("X-GitHub-Event", githubAction)
whm.sendWebhooks(ghEvent, r)
}
func (whm *WebhookManager) sendWebhooks(request interface{}, r *http.Request) {
var apps []model.Apps
result := whm.db.Find(&apps)
if result.Error != nil {
whm.logger.Error("Failed to query apps", zap.Error(result.Error))
return
}
for _, app := range apps {
go func(app model.Apps) {
payloadBytes, err := json.Marshal(request)
if err != nil {
whm.logger.Error("Failed to marshal payload", zap.Error(err))
return
}
var rawMap map[string]interface{}
err = json.Unmarshal(payloadBytes, &rawMap)
if err != nil {
whm.logger.Error("Failed to unmarshal payload", zap.Error(err))
return
}
rawMap["installation"] = struct {
ID uint `json:"id"`
}{ID: app.ID}
payloadBytes, err = json.Marshal(rawMap)
if err != nil {
whm.logger.Error("Failed to marshal payload", zap.Error(err))
return
}
req, err := http.NewRequest("POST", app.WebhookUrl, bytes.NewBuffer(payloadBytes))
if err != nil {
whm.logger.Error("Failed to create request", zap.Error(err), zap.String("url", app.WebhookUrl))
return
}
req.Header.Set("Content-Type", "application/json")
for key, values := range r.Header {
for _, value := range values {
req.Header.Add(key, value)
}
}
payload := toNormalizedJson(payloadBytes)
signature := generatePayloadSignature(payload, app.WebhookSecret)
req.Header.Set("X-Hub-Signature-256", signature)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
whm.logger.Error("Failed to send webhook", zap.Error(err), zap.String("url", app.WebhookUrl))
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
whm.logger.Error("Failed to close response body", zap.Error(err))
}
}(resp.Body)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
whm.logger.Info("Webhook sent successfully", zap.String("url", app.WebhookUrl))
} else {
whm.logger.Error("Webhook failed", zap.String("url", app.WebhookUrl), zap.Int("status", resp.StatusCode))
}
}(app)
}
}
/*
* Ported from https://github.com/octokit/webhooks.js/blob/984f3f51e077dbc7637aa55406c3bb114dbb7f82/src/to-normalized-json-string.ts
*/
func toNormalizedJson(payload []byte) []byte {
jsonString := string(payload)
// Regex to find Unicode escape sequences
re := regexp.MustCompile(`\\u([\da-fA-F]{4})`)
// Function to convert found sequences to uppercase
replaceFunc := func(match string) string {
parts := strings.Split(match, "\\u")
// Convert the Unicode sequence part to uppercase
return parts[0] + "\\u" + strings.ToUpper(parts[1])
}
// Replace the matches in the jsonString
normalizedString := re.ReplaceAllStringFunc(jsonString, replaceFunc)
return []byte(normalizedString)
}

View File

@ -1,34 +0,0 @@
package main
import (
"git.lumeweb.com/LumeWeb/gitea-github-proxy/api"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db"
"go.uber.org/fx"
"go.uber.org/fx/fxevent"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net/http"
)
func main() {
logger, _ := zap.NewDevelopment()
fx.New(
fx.Supply(logger),
fx.WithLogger(func(logger *zap.Logger) fxevent.Logger {
log := &fxevent.ZapLogger{Logger: logger}
log.UseLogLevel(zapcore.InfoLevel)
log.UseErrorLevel(zapcore.ErrorLevel)
return log
}),
config.Module,
db.Module,
fx.Provide(api.NewOauth),
fx.Provide(api.NewRouter),
fx.Provide(NewServer),
fx.Provide(api.NewWebhookManager),
fx.Invoke(api.SetupRoutes),
fx.Invoke(func(*http.Server) {}),
).Run()
}

View File

@ -1,48 +0,0 @@
package main
import (
"context"
"fmt"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"github.com/gorilla/mux"
"go.uber.org/fx"
"go.uber.org/zap"
"net"
"net/http"
)
type ServerParams struct {
fx.In
Logger *zap.Logger
Router *mux.Router
Config *config.Config
}
func NewServer(lc fx.Lifecycle, params ServerParams) (*http.Server, error) {
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", params.Config.Host, params.Config.Port),
Handler: params.Router,
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
return err
}
go func() {
err := srv.Serve(ln)
if err != nil {
params.Logger.Fatal("Failed to serve", zap.Error(err))
}
}()
return nil
},
OnStop: func(ctx context.Context) error {
return srv.Shutdown(ctx)
},
})
return srv, nil
}

View File

@ -1,158 +0,0 @@
package config
import (
"encoding"
"encoding/base64"
"fmt"
"github.com/fatih/structs"
"github.com/iancoleman/strcase"
"github.com/spf13/viper"
"go.uber.org/fx"
"go.uber.org/zap"
"reflect"
)
import (
"crypto/ed25519"
)
type PrivateKey ed25519.PrivateKey
var (
_ encoding.TextUnmarshaler = (*PrivateKey)(nil)
_ encoding.TextMarshaler = (*PrivateKey)(nil)
)
func (p *PrivateKey) UnmarshalText(text []byte) error {
dec, err := base64.StdEncoding.DecodeString(string(text))
if err != nil {
return err
}
*p = dec
return nil
}
func (p *PrivateKey) MarshalText() ([]byte, error) {
return []byte(base64.StdEncoding.EncodeToString(*p)), nil
}
type Config struct {
Host string `mapstructure:"host" structs:"host"`
Port int `mapstructure:"port" structs:"port"`
GiteaUrl string `mapstructure:"gitea_url" structs:"gitea_url"`
DbPath string `mapstructure:"db_path" structs:"db_path"`
Oauth OauthConfig
JwtPrivateKey PrivateKey
Domain string `mapstructure:"domain" structs:"domain"`
GiteaWebHookSecret string `mapstructure:"gitea_webhook_secret" structs:"gitea_webhook_secret"`
}
type OauthConfig struct {
Authorization string `mapstructure:"authorization" structs:"authorization"`
Token string `mapstructure:"token" structs:"token"`
ClientId string `mapstructure:"client_id" structs:"client_id"`
ClientSecret string `mapstructure:"client_secret" structs:"client_secret"`
RefreshToken string `mapstructure:"refresh_token" structs:"refresh_token"`
}
var Module = fx.Module("config",
fx.Options(
fx.Provide(NewConfig),
),
)
func NewConfig(logger *zap.Logger) *Config {
c := &Config{}
_, sk, err := ed25519.GenerateKey(nil)
if err != nil {
logger.Fatal("Error generating private key", zap.Error(err))
}
// Set the default values
viper.SetDefault("host", "localhost")
viper.SetDefault("port", 8080)
viper.SetDefault("gitea_url", "")
viper.SetDefault("db_path", "data.db")
viper.SetDefault("jwt_private_key", base64.StdEncoding.EncodeToString(sk))
viper.SetDefault("oauth.client_id", "")
viper.SetDefault("oauth.client_secret", "")
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/gitea-github-proxy")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
err = viper.ReadInConfig()
if err != nil {
err = viper.SafeWriteConfig()
if err != nil {
logger.Fatal("Error writing config file", zap.Error(err))
}
}
err = viper.Unmarshal(c)
if err != nil {
logger.Fatal("Error unmarshalling config", zap.Error(err))
}
/* err = viper.UnmarshalKey("jwt_private_key", &c.JwtPrivateKey, viper.DecodeHook(mapstructure.TextUnmarshallerHookFunc()))
if err != nil {
logger.Fatal("Error unmarshalling jwtPrivateKey", zap.Error(err))
}*/
if len(c.GiteaUrl) == 0 {
logger.Fatal("Gitea URL is required")
}
return c
}
func SaveConfig(cfg *Config) error {
data := structs.New(cfg)
for _, field := range data.Fields() {
processFields(field, nil)
}
/* if field, ok := interface{}(cfg.JwtPrivateKey).(encoding.TextMarshaler); ok {
text, err := field.MarshalText()
if err != nil {
return err
}
viper.Set("jwt_private_key", string(text))
}
*/
return viper.WriteConfig()
}
func processFields(f *structs.Field, parent *structs.Field) {
if f.IsZero() {
return
}
if f.Kind() == reflect.Struct {
fields := f.Fields()
if len(fields) > 0 {
for _, field := range fields {
processFields(field, f)
}
return
}
}
name := ""
if parent != nil {
name = fmt.Sprintf("%s.%s", parent.Name(), f.Name())
} else {
name = f.Name()
}
name = strcase.ToSnakeWithIgnore(name, ".")
viper.Set(name, f.Value())
}

View File

@ -1,47 +0,0 @@
package db
import (
"context"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
"go.uber.org/fx"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type DatabaseParams struct {
fx.In
Config *config.Config
}
var Module = fx.Module("db",
fx.Options(
fx.Provide(NewDatabase),
),
)
func NewDatabase(lc fx.Lifecycle, params DatabaseParams) *gorm.DB {
db, err := gorm.Open(sqlite.Open(params.Config.DbPath), &gorm.Config{})
if err != nil {
panic(err)
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return db.AutoMigrate(
&model.Apps{},
)
},
OnStop: func(ctx context.Context) error {
sqlDb, err := db.DB()
if err != nil {
return err
}
return sqlDb.Close()
},
})
return db
}

View File

@ -1,14 +0,0 @@
package model
import "gorm.io/gorm"
type Apps struct {
gorm.Model
Name string
Url string
WebhookUrl string
WebhookSecret string
Code string `gorm:"uniqueIndex"`
PrivateKey string
}

55
go.mod
View File

@ -1,55 +0,0 @@
module git.lumeweb.com/LumeWeb/gitea-github-proxy
go 1.21
require (
code.gitea.io/gitea v1.21.5
code.gitea.io/sdk/gitea v0.17.1
github.com/fatih/structs v1.1.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-github/v59 v59.0.0
github.com/gorilla/mux v1.8.1
github.com/iancoleman/strcase v0.3.0
github.com/spf13/viper v1.18.2
go.uber.org/fx v1.20.1
go.uber.org/zap v1.26.0
golang.org/x/oauth2 v0.17.0
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.7
)
require (
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)