Files
app_compartir/internal/auth/auth.go
T

301 lines
7.4 KiB
Go

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) 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,
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,
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,
})
}
// 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_<nil>" {
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
}