feat: implementar webapp app_compartir con visor universal, auth y purga 24h para guest
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user