diff --git a/api/routes_app.go b/api/routes_app.go index e7ed615..e8dddfb 100644 --- a/api/routes_app.go +++ b/api/routes_app.go @@ -1,18 +1,27 @@ 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, logger *zap.Logger) *appApi { - return &appApi{config: cfg, logger: 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) { @@ -21,16 +30,88 @@ func (a *appApi) handlerNewAppInstall(w http.ResponseWriter, r *http.Request) { 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, logger) + 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") }