diff --git a/api/routes.go b/api/routes.go index 3b04ed5..73d93e9 100644 --- a/api/routes.go +++ b/api/routes.go @@ -29,6 +29,7 @@ func SetupRoutes(params RouteParams) { setupSettingsRoutes(params) setupManifestsRoutes(params) setupAppRoutes(params) + setupAppInstallRoutes(params) setupWebhookRoutes(params) setupRestRoutes(params) } diff --git a/api/routes_app_installations.go b/api/routes_app_installations.go new file mode 100644 index 0000000..97a9a67 --- /dev/null +++ b/api/routes_app_installations.go @@ -0,0 +1,119 @@ +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)["app_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.SigningMethodHS256, 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("/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") +}