320 lines
7.9 KiB
Go
320 lines
7.9 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) 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_<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
|
|
}
|