package sfauthgin import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "net/url" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) const ( sessionUserIDKey = "sf_user_id" sessionUsernameKey = "sf_username" authenticateURLTemplate = "https://snazzyfellas.com/api/redirect/authenticate?redirect_uri=%s" validateURL = "https://snazzyfellas.com/api/redirect/validate" ) type validationRequest struct { UserID string `json:"user_id"` Key string `json:"key"` } type validationResponse struct { Valid bool `json:"valid"` UserID string `json:"user_id"` } // NewMiddleware creates a Gin middleware that checks for sf-auth session values // and redirects to the Snazzyfellas authenticate endpoint when missing. func NewMiddleware(redirectURL func(*gin.Context) string) gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) if session == nil { c.String(http.StatusInternalServerError, "session middleware not configured") c.Abort() return } userID := session.Get(sessionUserIDKey) username := session.Get(sessionUsernameKey) if !isNonEmptyString(userID) || !isNonEmptyString(username) { target := "" if redirectURL != nil { target = redirectURL(c) } if target == "" { c.String(http.StatusInternalServerError, "redirect URL is required") c.Abort() return } redirect := fmt.Sprintf(authenticateURLTemplate, url.QueryEscape(target)) c.Redirect(http.StatusFound, redirect) c.Abort() return } c.Next() } } // CreateAuthCallbackHandler returns a handler for the sf-auth callback route. // It validates the one-time key, stores session values, and redirects to redirectTo. func CreateAuthCallbackHandler(redirectTo string) gin.HandlerFunc { return func(c *gin.Context) { if redirectTo == "" { c.String(http.StatusInternalServerError, "redirect destination is required") return } userID := c.Query("user_id") username := c.Query("username") key := c.Query("key") if userID == "" || username == "" || key == "" { c.String(http.StatusBadRequest, "missing required query parameters") return } if err := validateAuth(c, userID, key); err != nil { c.String(http.StatusUnauthorized, err.Error()) return } session := sessions.Default(c) if session == nil { c.String(http.StatusInternalServerError, "session middleware not configured") return } session.Set(sessionUserIDKey, userID) session.Set(sessionUsernameKey, username) if err := session.Save(); err != nil { c.String(http.StatusInternalServerError, "failed to save session") return } c.Redirect(http.StatusFound, redirectTo) } } func validateAuth(c *gin.Context, userID string, key string) error { payload := validationRequest{UserID: userID, Key: key} body, err := json.Marshal(payload) if err != nil { return errors.New("failed to encode validation payload") } req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, validateURL, bytes.NewReader(body)) if err != nil { return errors.New("failed to create validation request") } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return errors.New("failed to reach validation service") } defer resp.Body.Close() if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return fmt.Errorf("validation service returned status %d", resp.StatusCode) } var result validationResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return errors.New("invalid validation response") } if !result.Valid || result.UserID != userID { return errors.New("invalid auth response") } return nil } func isNonEmptyString(value interface{}) bool { text, ok := value.(string) if !ok { return false } return text != "" }