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), }, } } 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) { 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") } } } o.refresher = config.TokenSource(context.Background(), token) } 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.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 }