feat: server bones and initial github apps setup support
This commit is contained in:
parent
99727e8614
commit
4456b54550
2
LICENSE
2
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:
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
Loading…
Reference in New Issue