From 4456b5455008019310b3e73425a22155dada7c38 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Sat, 10 Feb 2024 23:50:46 -0500 Subject: [PATCH] feat: server bones and initial github apps setup support --- LICENSE | 2 +- api/app.go | 72 ++++++++++++++++++++++++++ api/cookie.go | 43 ++++++++++++++++ api/middleware.go | 114 +++++++++++++++++++++++++++++++++++++++++ api/oauth.go | 34 ++++++++++++ api/route_manifests.go | 82 +++++++++++++++++++++++++++++ api/router.go | 18 +++++++ api/routes.go | 62 ++++++++++++++++++++++ api/routes_app.go | 22 ++++++++ api/routes_settings.go | 71 +++++++++++++++++++++++++ api/routes_setup.go | 59 +++++++++++++++++++++ cmd/proxy/main.go | 32 ++++++++++++ cmd/proxy/server.go | 48 +++++++++++++++++ config/config.go | 104 +++++++++++++++++++++++++++++++++++++ db/db.go | 47 +++++++++++++++++ db/model/apps.go | 14 +++++ go.mod | 49 ++++++++++++++++++ 17 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 api/app.go create mode 100644 api/cookie.go create mode 100644 api/middleware.go create mode 100644 api/oauth.go create mode 100644 api/route_manifests.go create mode 100644 api/router.go create mode 100644 api/routes.go create mode 100644 api/routes_app.go create mode 100644 api/routes_settings.go create mode 100644 api/routes_setup.go create mode 100644 cmd/proxy/main.go create mode 100644 cmd/proxy/server.go create mode 100644 config/config.go create mode 100644 db/db.go create mode 100644 db/model/apps.go create mode 100644 go.mod diff --git a/LICENSE b/LICENSE index 3f77cbd..cb53b0b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 LumeWeb +Copyright (c) 2024 Hammer Technologies LLC 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: diff --git a/api/app.go b/api/app.go new file mode 100644 index 0000000..6ff0f59 --- /dev/null +++ b/api/app.go @@ -0,0 +1,72 @@ +package api + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "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) +} diff --git a/api/cookie.go b/api/cookie.go new file mode 100644 index 0000000..96b0751 --- /dev/null +++ b/api/cookie.go @@ -0,0 +1,43 @@ +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 +} diff --git a/api/middleware.go b/api/middleware.go new file mode 100644 index 0000000..f9d4028 --- /dev/null +++ b/api/middleware.go @@ -0,0 +1,114 @@ +package api + +import ( + "code.gitea.io/sdk/gitea" + "context" + "git.lumeweb.com/LumeWeb/gitea-github-proxy/config" + "github.com/gorilla/mux" + "go.uber.org/zap" + "net/http" + "strings" +) + +const AUTHED_CONTEXT_KEY = "authed" +const REDIRECT_AFTER_AUTH = "redirect-after-auth" + +const AuthCookieName = "auth-token" + +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 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 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 ") + + return authHeader +} + +func getAuthedStatusFromRequest(r *http.Request) bool { + authed, ok := r.Context().Value(AUTHED_CONTEXT_KEY).(bool) + if !ok { + return false + } + + return authed +} diff --git a/api/oauth.go b/api/oauth.go new file mode 100644 index 0000000..db96924 --- /dev/null +++ b/api/oauth.go @@ -0,0 +1,34 @@ +package api + +import ( + "fmt" + "git.lumeweb.com/LumeWeb/gitea-github-proxy/config" + "go.uber.org/zap" + "golang.org/x/oauth2" +) + +type oauth struct { + cfg *config.Config + logger *zap.Logger +} + +func newOauth(cfg *config.Config, logger *zap.Logger) *oauth { + return &oauth{cfg: cfg, logger: logger} +} + +func (o oauth) config() *oauth2.Config { + return &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), + }, + } +} + +func (o oauth) authUrl() string { + return o.config().AuthCodeURL("state") +} diff --git a/api/route_manifests.go b/api/route_manifests.go new file mode 100644 index 0000000..7750801 --- /dev/null +++ b/api/route_manifests.go @@ -0,0 +1,82 @@ +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) +} diff --git a/api/router.go b/api/router.go new file mode 100644 index 0000000..2e5cf65 --- /dev/null +++ b/api/router.go @@ -0,0 +1,18 @@ +package api + +import ( + "git.lumeweb.com/LumeWeb/gitea-github-proxy/config" + "github.com/gorilla/mux" + "go.uber.org/fx" +) + +type RouterParams struct { + fx.In + Config *config.Config +} + +func NewRouter(params RouterParams) *mux.Router { + r := mux.NewRouter() + + return r +} diff --git a/api/routes.go b/api/routes.go new file mode 100644 index 0000000..8fc3b14 --- /dev/null +++ b/api/routes.go @@ -0,0 +1,62 @@ +package api + +import ( + "code.gitea.io/sdk/gitea" + "git.lumeweb.com/LumeWeb/gitea-github-proxy/config" + "github.com/gorilla/mux" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func SetupRoutes(r *mux.Router, cfg *config.Config, db *gorm.DB, logger *zap.Logger) { + + r.Use(loggingMiddleware(logger)) + + setupRouter := r.PathPrefix("/setup").Subrouter() + setupRouter.Use(giteaOauthVerifyMiddleware(cfg)) + + setupApi := newSetupApi(cfg, logger, newOauth(cfg, logger)) + setupRouter.HandleFunc("", setupApi.setupHandler).Methods("GET") + setupRouter.HandleFunc("/callback", setupApi.callbackHandler).Methods("GET") + + settingsRouter := r.PathPrefix("/settings").Subrouter() + settingsRouter.Use(giteaOauthVerifyMiddleware(cfg)) + settingsRouter.Use(requireAuthMiddleware(cfg)) + + settingsApi := newSettingsApi(cfg, db, logger) + + settingsRouter.HandleFunc("/apps/new", settingsApi.handlerNewApp).Methods("POST") + + manifestApi := newManifests(cfg, db, logger) + manifestsRouter := r.PathPrefix("/api/v3/app-manifests").Subrouter() + + manifestsRouter.HandleFunc("/{code}/conversions", manifestApi.handlerConversion).Methods("POST") + + appApi := newAppApi(cfg, logger) + appRouter := r.PathPrefix("/apps").Subrouter() + + appRouter.Use(giteaOauthVerifyMiddleware(cfg)) + appRouter.Use(requireAuthMiddleware(cfg)) + + appRouter.HandleFunc("/{app}/installations/new", appApi.handlerNewAppInstall).Methods("GET") +} + +type ClientParams struct { + Config *config.Config + AuthToken string +} + +func getClient(params ClientParams) (*gitea.Client, error) { + options := make([]gitea.ClientOption, 0) + + if len(params.AuthToken) > 0 { + options = append(options, gitea.SetToken(params.AuthToken)) + } + + client, err := gitea.NewClient(params.Config.GiteaUrl, options...) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/api/routes_app.go b/api/routes_app.go new file mode 100644 index 0000000..34b8f65 --- /dev/null +++ b/api/routes_app.go @@ -0,0 +1,22 @@ +package api + +import ( + "git.lumeweb.com/LumeWeb/gitea-github-proxy/config" + "go.uber.org/zap" + "net/http" +) + +type appApi struct { + config *config.Config + logger *zap.Logger +} + +func newAppApi(cfg *config.Config, logger *zap.Logger) *appApi { + return &appApi{config: cfg, 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") +} diff --git a/api/routes_settings.go b/api/routes_settings.go new file mode 100644 index 0000000..d965c4f --- /dev/null +++ b/api/routes_settings.go @@ -0,0 +1,71 @@ +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) + +} diff --git a/api/routes_setup.go b/api/routes_setup.go new file mode 100644 index 0000000..f3708ae --- /dev/null +++ b/api/routes_setup.go @@ -0,0 +1,59 @@ +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.config().Exchange(r.Context(), 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) +} diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go new file mode 100644 index 0000000..70bd548 --- /dev/null +++ b/cmd/proxy/main.go @@ -0,0 +1,32 @@ +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.NewRouter), + fx.Provide(NewServer), + fx.Invoke(api.SetupRoutes), + fx.Invoke(func(*http.Server) {}), + ).Run() +} diff --git a/cmd/proxy/server.go b/cmd/proxy/server.go new file mode 100644 index 0000000..7da59ab --- /dev/null +++ b/cmd/proxy/server.go @@ -0,0 +1,48 @@ +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 +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a9af201 --- /dev/null +++ b/config/config.go @@ -0,0 +1,104 @@ +package config + +import ( + "encoding" + "encoding/base64" + "github.com/mitchellh/mapstructure" + "github.com/spf13/viper" + "go.uber.org/fx" + "go.uber.org/zap" +) + +import ( + "crypto/ed25519" +) + +type PrivateKey ed25519.PrivateKey + +var ( + _ encoding.TextUnmarshaler = (*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 +} + +type Config struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + GiteaUrl string `mapstructure:"gitea_url"` + DbPath string `mapstructure:"db_path"` + Oauth OauthConfig + JwtPrivateKey PrivateKey + Domain string `mapstructure:"domain"` +} + +type OauthConfig struct { + Authorization string `mapstructure:"authorization"` + Token string `mapstructure:"token"` + ClientId string `mapstructure:"client_id"` + ClientSecret string `mapstructure:"client_secret"` + RefreshToken string `mapstructure:"refresh_token"` +} + +var Module = fx.Module("config", + fx.Options( + fx.Provide(NewConfig), + ), +) + +func NewConfig(logger *zap.Logger) *Config { + c := &Config{} + + cfg := viper.New() + + _, sk, err := ed25519.GenerateKey(nil) + + if err != nil { + logger.Fatal("Error generating private key", zap.Error(err)) + } + + // Set the default values + cfg.SetDefault("host", "localhost") + cfg.SetDefault("port", 8080) + cfg.SetDefault("gitea_url", "") + cfg.SetDefault("db_path", "data.db") + cfg.SetDefault("jwt_private_key", base64.StdEncoding.EncodeToString(sk)) + cfg.SetDefault("oauth.client_id", "") + cfg.SetDefault("oauth.client_secret", "") + + cfg.AddConfigPath(".") + cfg.AddConfigPath("/etc/gitea-github-proxy") + cfg.SetConfigName("config") + cfg.SetConfigType("yaml") + err = cfg.ReadInConfig() + if err != nil { + err = cfg.SafeWriteConfig() + if err != nil { + logger.Fatal("Error writing config file", zap.Error(err)) + } + } + + err = cfg.Unmarshal(c) + if err != nil { + logger.Fatal("Error unmarshalling config", zap.Error(err)) + } + + err = cfg.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 +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..11898e7 --- /dev/null +++ b/db/db.go @@ -0,0 +1,47 @@ +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 +} diff --git a/db/model/apps.go b/db/model/apps.go new file mode 100644 index 0000000..6a0a002 --- /dev/null +++ b/db/model/apps.go @@ -0,0 +1,14 @@ +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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c01590c --- /dev/null +++ b/go.mod @@ -0,0 +1,49 @@ +module git.lumeweb.com/LumeWeb/gitea-github-proxy + +go 1.21 + +require ( + code.gitea.io/sdk/gitea v0.17.1 + github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/gorilla/mux v1.8.1 + github.com/spf13/viper v1.18.2 + go.uber.org/fx v1.20.1 + go.uber.org/zap v1.23.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.0 // indirect + github.com/golang/protobuf v1.5.3 // 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/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/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/atomic v1.9.0 // indirect + go.uber.org/dig v1.17.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.7 // 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 +)