package auth import ( "bytes" "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strings" "time" "app_compartir/internal/config" "app_compartir/internal/db" "app_compartir/internal/models" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) const ( CookieSessionName = "compartir_session" ) type AuthService struct { cfg *config.Config db *db.Database oauthConfig *oauth2.Config } func New(cfg *config.Config, database *db.Database) *AuthService { var oauthConf *oauth2.Config if cfg.GoogleClientID != "" && cfg.GoogleClientSecret != "" { oauthConf = &oauth2.Config{ ClientID: cfg.GoogleClientID, ClientSecret: cfg.GoogleClientSecret, RedirectURL: cfg.BaseURL + "/auth/google/callback", Scopes: []string{ "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email", }, Endpoint: google.Endpoint, } } return &AuthService{ cfg: cfg, db: database, oauthConfig: oauthConf, } } func (a *AuthService) GenerateToken(length int) string { bytes := make([]byte, length) if _, err := rand.Read(bytes); err != nil { return fmt.Sprintf("%d", time.Now().UnixNano()) } return hex.EncodeToString(bytes) } func (a *AuthService) isSecureRequest(r *http.Request) bool { if r.TLS != nil { return true } if strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") || strings.EqualFold(r.Header.Get("X-Forwarded-Ssl"), "on") || r.Header.Get("X-Forwarded-Port") == "443" { return true } if strings.HasPrefix(a.cfg.BaseURL, "https://") { return true } return false } func (a *AuthService) GetOrCreateSession(w http.ResponseWriter, r *http.Request) *models.Session { cookie, err := r.Cookie(CookieSessionName) if err == nil && cookie.Value != "" { session, err := a.db.GetSession(cookie.Value) if err == nil && session != nil { return session } } // Crear nueva sesión de invitado (Guest) token := a.GenerateToken(32) guestID := "guest_" + a.GenerateToken(8) now := time.Now() expiresAt := now.Add(30 * 24 * time.Hour) // La cookie de sesión dura 30 días, pero los archivos del guest expiran en 24h session := &models.Session{ Token: token, UserID: nil, IsGuest: true, GuestID: guestID, CreatedAt: now, ExpiresAt: expiresAt, } if err := a.db.CreateSession(session); err != nil { return session } http.SetCookie(w, &http.Cookie{ Name: CookieSessionName, Value: token, Path: "/", Expires: expiresAt, HttpOnly: true, Secure: a.isSecureRequest(r), SameSite: http.SameSiteLaxMode, }) return session } func (a *AuthService) AttachUserToSession(w http.ResponseWriter, r *http.Request, user *models.User) error { token := a.GenerateToken(32) now := time.Now() expiresAt := now.Add(30 * 24 * time.Hour) session := &models.Session{ Token: token, UserID: &user.ID, IsGuest: false, GuestID: "", CreatedAt: now, ExpiresAt: expiresAt, User: user, } if err := a.db.CreateSession(session); err != nil { return err } http.SetCookie(w, &http.Cookie{ Name: CookieSessionName, Value: token, Path: "/", Expires: expiresAt, HttpOnly: true, Secure: a.isSecureRequest(r), SameSite: http.SameSiteLaxMode, }) return nil } func (a *AuthService) Logout(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie(CookieSessionName); err == nil && cookie.Value != "" { _ = a.db.DeleteSession(cookie.Value) } http.SetCookie(w, &http.Cookie{ Name: CookieSessionName, Value: "", Path: "/", Expires: time.Unix(0, 0), MaxAge: -1, HttpOnly: true, Secure: a.isSecureRequest(r), SameSite: http.SameSiteLaxMode, }) } // LoginWebControl realiza autenticación con WebControl de Telcotronics func (a *AuthService) LoginWebControl(email, password string) (*models.User, error) { baseURL := strings.TrimRight(a.cfg.WebControlURL, "/") // 1. Consultar empresas disponibles para el usuario empresasURL := baseURL + "/api/auth/empresas" empReqBody, _ := json.Marshal(map[string]string{ "email": email, "paswd": password, }) client := &http.Client{Timeout: 8 * time.Second} resp, err := client.Post(empresasURL, "application/json", bytes.NewBuffer(empReqBody)) dba := "" if err == nil && resp.StatusCode == http.StatusOK { var empData struct { Empresas []struct { DBA string `json:"dba"` Nombre string `json:"nombre"` } `json:"empresas"` } if err := json.NewDecoder(resp.Body).Decode(&empData); err == nil && len(empData.Empresas) > 0 { dba = empData.Empresas[0].DBA } resp.Body.Close() } // 2. Realizar login final loginURL := baseURL + "/api/auth/login" loginPayload := map[string]string{ "email": email, "paswd": password, } if dba != "" { loginPayload["dba"] = dba } loginBody, _ := json.Marshal(loginPayload) loginResp, err := client.Post(loginURL, "application/json", bytes.NewBuffer(loginBody)) if err != nil { return nil, fmt.Errorf("error conectando con WebControl: %w", err) } defer loginResp.Body.Close() if loginResp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(loginResp.Body) return nil, fmt.Errorf("credenciales inválidas en WebControl (status %d): %s", loginResp.StatusCode, string(bodyBytes)) } var res struct { Token string `json:"token"` APIKey string `json:"apiKey"` Usuario struct { ID interface{} `json:"id"` Nombre string `json:"nombre"` Email string `json:"email"` } `json:"usuario"` } if err := json.NewDecoder(loginResp.Body).Decode(&res); err != nil { return nil, fmt.Errorf("error interpretando respuesta de WebControl: %w", err) } userIDStr := fmt.Sprintf("wc_%v", res.Usuario.ID) if res.Usuario.ID == nil || userIDStr == "wc_" { userIDStr = "wc_" + a.GenerateToken(8) } userName := res.Usuario.Nombre if userName == "" { userName = strings.Split(email, "@")[0] } userEmail := res.Usuario.Email if userEmail == "" { userEmail = email } user := &models.User{ ID: userIDStr, Provider: "webcontrol", ProviderID: fmt.Sprintf("%v", res.Usuario.ID), Email: userEmail, Name: userName, AvatarURL: "", CreatedAt: time.Now(), } if err := a.db.UpsertUser(user); err != nil { return nil, fmt.Errorf("error guardando usuario en base de datos: %w", err) } return user, nil } // Google OAuth func (a *AuthService) GetGoogleAuthURL(state string) (string, error) { if a.oauthConfig == nil { return "", fmt.Errorf("Google OAuth no está configurado (faltan GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET)") } return a.oauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline), nil } func (a *AuthService) HandleGoogleCallback(ctx context.Context, code string) (*models.User, error) { if a.oauthConfig == nil { return nil, fmt.Errorf("Google OAuth no está configurado") } token, err := a.oauthConfig.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("error intercambiando código OAuth: %w", err) } client := a.oauthConfig.Client(ctx, token) resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") if err != nil { return nil, fmt.Errorf("error obteniendo perfil de Google: %w", err) } defer resp.Body.Close() var gUser struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Picture string `json:"picture"` } if err := json.NewDecoder(resp.Body).Decode(&gUser); err != nil { return nil, fmt.Errorf("error leyendo perfil de Google: %w", err) } userID := "google_" + gUser.ID user := &models.User{ ID: userID, Provider: "google", ProviderID: gUser.ID, Email: gUser.Email, Name: gUser.Name, AvatarURL: gUser.Picture, CreatedAt: time.Now(), } if err := a.db.UpsertUser(user); err != nil { return nil, fmt.Errorf("error guardando usuario de Google: %w", err) } return user, nil } func (a *AuthService) HasGoogleConfigured() bool { return a.oauthConfig != nil }