Initial import with modified Dockerfile for env-based config generation
This commit is contained in:
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/event"
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/identity"
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/storage"
|
||||
)
|
||||
|
||||
// CachedResolver implements identity.Resolver with caching
|
||||
// It stores resolved identities in the database and only queries
|
||||
// external APIs when a user is not found in the cache
|
||||
type CachedResolver struct {
|
||||
repo storage.Repository
|
||||
emailLookup identity.EmailLookup
|
||||
slackLookup identity.SlackLookup
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// NewCachedResolver creates a new cached identity resolver
|
||||
func NewCachedResolver(
|
||||
repo storage.Repository,
|
||||
emailLookup identity.EmailLookup,
|
||||
slackLookup identity.SlackLookup,
|
||||
logger zerolog.Logger,
|
||||
) *CachedResolver {
|
||||
return &CachedResolver{
|
||||
repo: repo,
|
||||
emailLookup: emailLookup,
|
||||
slackLookup: slackLookup,
|
||||
logger: logger.With().Str("component", "identity-resolver").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve returns the external identity for a Gitea user
|
||||
// It follows this strategy:
|
||||
// 1. Check DB by Gitea username - if found with Slack ID, return cached result
|
||||
// 2. If not found, use Gitea API to get the real email (not the webhook email which may be noreply)
|
||||
// 3. Lookup Slack by email
|
||||
// 4. Cache the result for future lookups
|
||||
func (r *CachedResolver) Resolve(ctx context.Context, user event.User) (*identity.ResolvedIdentity, error) {
|
||||
logger := r.logger.With().
|
||||
Str("gitea_username", user.GiteaUsername).
|
||||
Str("webhook_email", user.Email).
|
||||
Logger()
|
||||
|
||||
// Step 1: Try to find by Gitea username in cache
|
||||
if user.GiteaUsername != "" {
|
||||
dbUser, err := r.repo.GetUserByGiteaUsername(ctx, user.GiteaUsername)
|
||||
if err == nil && dbUser.SlackID != "" {
|
||||
logger.Debug().
|
||||
Str("cached_email", dbUser.Email).
|
||||
Str("slack_id", dbUser.SlackID).
|
||||
Msg("found user in cache by username")
|
||||
return &identity.ResolvedIdentity{
|
||||
Email: dbUser.Email,
|
||||
SlackID: dbUser.SlackID,
|
||||
SlackName: dbUser.SlackName,
|
||||
}, nil
|
||||
}
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
logger.Error().Err(err).Msg("error querying user by username")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Not in cache - need to lookup real email via Gitea API
|
||||
// We don't trust the webhook email because it might be a noreply address
|
||||
logger.Info().Msg("user not in cache, querying Gitea API for real email")
|
||||
|
||||
if user.GiteaUsername == "" {
|
||||
return nil, fmt.Errorf("no username available to lookup user")
|
||||
}
|
||||
|
||||
if r.emailLookup == nil {
|
||||
return nil, fmt.Errorf("no email lookup provider configured")
|
||||
}
|
||||
|
||||
// Get real email from Gitea API
|
||||
email, err := r.emailLookup.LookupEmail(ctx, user.GiteaUsername)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to lookup email from Gitea API")
|
||||
return nil, fmt.Errorf("looking up email for %s: %w", user.GiteaUsername, err)
|
||||
}
|
||||
|
||||
logger.Info().Str("real_email", email).Msg("got real email from Gitea API")
|
||||
|
||||
// Step 3: Lookup Slack ID by real email
|
||||
slackID, slackName, err := r.slackLookup.LookupSlackIDByEmail(ctx, email)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Str("email", email).Msg("failed to lookup Slack ID")
|
||||
return nil, fmt.Errorf("looking up Slack ID for %s: %w", email, err)
|
||||
}
|
||||
|
||||
// Step 4: Cache the result
|
||||
dbUser := &storage.User{
|
||||
GiteaUsername: user.GiteaUsername,
|
||||
GiteaID: user.GiteaID,
|
||||
Email: strings.ToLower(email),
|
||||
FullName: user.FullName,
|
||||
SlackID: slackID,
|
||||
SlackName: slackName,
|
||||
}
|
||||
|
||||
if err := r.repo.UpsertUser(ctx, dbUser); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to cache user identity")
|
||||
// Continue anyway, we have the identity
|
||||
} else {
|
||||
logger.Info().
|
||||
Str("slack_id", slackID).
|
||||
Str("slack_name", slackName).
|
||||
Msg("cached user identity")
|
||||
}
|
||||
|
||||
return &identity.ResolvedIdentity{
|
||||
Email: email,
|
||||
SlackID: slackID,
|
||||
SlackName: slackName,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/config"
|
||||
)
|
||||
|
||||
// GiteaUser represents a user from Gitea API
|
||||
type GiteaUser struct {
|
||||
ID int64 `json:"id"`
|
||||
Login string `json:"login"`
|
||||
FullName string `json:"full_name"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
// Provider implements email lookup via Gitea API
|
||||
type Provider struct {
|
||||
baseURL string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// New creates a new Gitea API provider
|
||||
func New(cfg *config.GiteaConfig, logger zerolog.Logger) *Provider {
|
||||
return &Provider{
|
||||
baseURL: cfg.URL,
|
||||
token: cfg.Token,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
logger: logger.With().Str("component", "gitea-provider").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// LookupEmail fetches the real email for a Gitea username via API
|
||||
func (p *Provider) LookupEmail(ctx context.Context, username string) (string, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/users/%s", p.baseURL, username)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
// Add authorization header if token is provided
|
||||
if p.token != "" {
|
||||
req.Header.Set("Authorization", "token "+p.token)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetching user from Gitea: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return "", fmt.Errorf("user %s not found in Gitea", username)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("Gitea API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var user GiteaUser
|
||||
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
|
||||
return "", fmt.Errorf("decoding Gitea response: %w", err)
|
||||
}
|
||||
|
||||
if user.Email == "" {
|
||||
return "", fmt.Errorf("user %s has no email in Gitea", username)
|
||||
}
|
||||
|
||||
p.logger.Debug().
|
||||
Str("username", username).
|
||||
Str("email", user.Email).
|
||||
Str("full_name", user.FullName).
|
||||
Msg("found user email via Gitea API")
|
||||
|
||||
return user.Email, nil
|
||||
}
|
||||
|
||||
// GetUser fetches full user info from Gitea API
|
||||
func (p *Provider) GetUser(ctx context.Context, username string) (*GiteaUser, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/users/%s", p.baseURL, username)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
if p.token != "" {
|
||||
req.Header.Set("Authorization", "token "+p.token)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching user from Gitea: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("user %s not found in Gitea", username)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Gitea API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var user GiteaUser
|
||||
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
|
||||
return nil, fmt.Errorf("decoding Gitea response: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/event"
|
||||
)
|
||||
|
||||
// ResolvedIdentity represents a resolved external identity
|
||||
type ResolvedIdentity struct {
|
||||
Email string `json:"email"`
|
||||
SlackID string `json:"slack_id"`
|
||||
SlackName string `json:"slack_name"`
|
||||
}
|
||||
|
||||
// Resolver resolves Gitea users to external identities (e.g., Slack)
|
||||
// This interface allows for different identity providers (Gitea API, LDAP, etc.)
|
||||
type Resolver interface {
|
||||
// Resolve returns the external identity for a Gitea user
|
||||
Resolve(ctx context.Context, user event.User) (*ResolvedIdentity, error)
|
||||
}
|
||||
|
||||
// EmailLookup provides email lookup functionality
|
||||
type EmailLookup interface {
|
||||
// LookupEmail looks up a user's email address by username
|
||||
LookupEmail(ctx context.Context, username string) (string, error)
|
||||
}
|
||||
|
||||
// SlackLookup provides Slack user lookup functionality
|
||||
type SlackLookup interface {
|
||||
// LookupSlackIDByEmail finds a Slack user ID by email
|
||||
LookupSlackIDByEmail(ctx context.Context, email string) (slackID, slackName string, err error)
|
||||
}
|
||||
Reference in New Issue
Block a user