sync
This commit is contained in:
396
bridge/kosmi/auth.go
Normal file
396
bridge/kosmi/auth.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package bkosmi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// Token expiry buffer - refresh if token expires within this window
|
||||
tokenExpiryBuffer = 5 * time.Minute
|
||||
)
|
||||
|
||||
// AuthManager handles authentication with Kosmi
|
||||
type AuthManager struct {
|
||||
email string
|
||||
password string
|
||||
token string
|
||||
tokenExpiry time.Time
|
||||
refreshToken string
|
||||
userID string
|
||||
mu sync.RWMutex
|
||||
log *logrus.Entry
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewAuthManager creates a new authentication manager
|
||||
func NewAuthManager(email, password string, log *logrus.Entry) *AuthManager {
|
||||
return &AuthManager{
|
||||
email: email,
|
||||
password: password,
|
||||
log: log,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// Login performs email/password authentication
|
||||
//
|
||||
// NOTE: The actual login API format needs to be verified through monitoring.
|
||||
// This implementation is based on common GraphQL patterns and may need adjustment.
|
||||
func (a *AuthManager) Login() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.log.Info("Logging in to Kosmi...")
|
||||
|
||||
// Prepare login mutation
|
||||
// Based on common GraphQL patterns, likely something like:
|
||||
// mutation { login(email: "...", password: "...") { token user { id displayName } } }
|
||||
mutation := map[string]interface{}{
|
||||
"query": `mutation Login($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password) {
|
||||
token
|
||||
refreshToken
|
||||
expiresIn
|
||||
user {
|
||||
id
|
||||
displayName
|
||||
username
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"variables": map[string]interface{}{
|
||||
"email": a.email,
|
||||
"password": a.password,
|
||||
},
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(mutation)
|
||||
if err != nil {
|
||||
return &AuthError{
|
||||
Op: "login",
|
||||
Reason: "failed to marshal request",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", kosmiHTTPURL, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return &AuthError{
|
||||
Op: "login",
|
||||
Reason: "failed to create request",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Referer", "https://app.kosmi.io/")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := a.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return &AuthError{
|
||||
Op: "login",
|
||||
Reason: "request failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 401 {
|
||||
return &AuthError{
|
||||
Op: "login",
|
||||
Reason: "invalid credentials",
|
||||
Err: ErrAuthFailed,
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return &AuthError{
|
||||
Op: "login",
|
||||
Reason: fmt.Sprintf("HTTP %d", resp.StatusCode),
|
||||
Err: ErrAuthFailed,
|
||||
}
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return &AuthError{
|
||||
Op: "login",
|
||||
Reason: "failed to parse response",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Extract token and user info
|
||||
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||
if login, ok := data["login"].(map[string]interface{}); ok {
|
||||
if token, ok := login["token"].(string); ok {
|
||||
a.token = token
|
||||
|
||||
// Extract refresh token if present
|
||||
if refreshToken, ok := login["refreshToken"].(string); ok {
|
||||
a.refreshToken = refreshToken
|
||||
}
|
||||
|
||||
// Calculate token expiry
|
||||
if expiresIn, ok := login["expiresIn"].(float64); ok {
|
||||
a.tokenExpiry = time.Now().Add(time.Duration(expiresIn) * time.Second)
|
||||
} else {
|
||||
// Default to 24 hours if not specified
|
||||
a.tokenExpiry = time.Now().Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
// Extract user ID
|
||||
if user, ok := login["user"].(map[string]interface{}); ok {
|
||||
if userID, ok := user["id"].(string); ok {
|
||||
a.userID = userID
|
||||
}
|
||||
if displayName, ok := user["displayName"].(string); ok {
|
||||
a.log.Infof("Logged in as: %s", displayName)
|
||||
}
|
||||
}
|
||||
|
||||
a.log.Info("✅ Login successful")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for GraphQL errors
|
||||
if errors, ok := result["errors"].([]interface{}); ok && len(errors) > 0 {
|
||||
if errObj, ok := errors[0].(map[string]interface{}); ok {
|
||||
if message, ok := errObj["message"].(string); ok {
|
||||
return &AuthError{
|
||||
Op: "login",
|
||||
Reason: message,
|
||||
Err: ErrAuthFailed,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthError{
|
||||
Op: "login",
|
||||
Reason: "no token in response",
|
||||
Err: ErrAuthFailed,
|
||||
}
|
||||
}
|
||||
|
||||
// GetToken returns a valid token, refreshing if necessary
|
||||
func (a *AuthManager) GetToken() (string, error) {
|
||||
a.mu.RLock()
|
||||
|
||||
// Check if token is still valid
|
||||
if a.token != "" && time.Now().Before(a.tokenExpiry.Add(-tokenExpiryBuffer)) {
|
||||
token := a.token
|
||||
a.mu.RUnlock()
|
||||
return token, nil
|
||||
}
|
||||
|
||||
a.mu.RUnlock()
|
||||
|
||||
// Token is expired or about to expire, refresh it
|
||||
if a.refreshToken != "" {
|
||||
if err := a.RefreshToken(); err != nil {
|
||||
a.log.Warnf("Token refresh failed, attempting re-login: %v", err)
|
||||
if err := a.Login(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No refresh token, need to login again
|
||||
if err := a.Login(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.token, nil
|
||||
}
|
||||
|
||||
// RefreshToken renews the access token using the refresh token
|
||||
func (a *AuthManager) RefreshToken() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.refreshToken == "" {
|
||||
return &AuthError{
|
||||
Op: "token_refresh",
|
||||
Reason: "no refresh token available",
|
||||
Err: ErrTokenExpired,
|
||||
}
|
||||
}
|
||||
|
||||
a.log.Debug("Refreshing authentication token...")
|
||||
|
||||
// Prepare refresh mutation
|
||||
mutation := map[string]interface{}{
|
||||
"query": `mutation RefreshToken($refreshToken: String!) {
|
||||
refreshToken(refreshToken: $refreshToken) {
|
||||
token
|
||||
refreshToken
|
||||
expiresIn
|
||||
}
|
||||
}`,
|
||||
"variables": map[string]interface{}{
|
||||
"refreshToken": a.refreshToken,
|
||||
},
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(mutation)
|
||||
if err != nil {
|
||||
return &AuthError{
|
||||
Op: "token_refresh",
|
||||
Reason: "failed to marshal request",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", kosmiHTTPURL, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return &AuthError{
|
||||
Op: "token_refresh",
|
||||
Reason: "failed to create request",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Referer", "https://app.kosmi.io/")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.token))
|
||||
|
||||
resp, err := a.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return &AuthError{
|
||||
Op: "token_refresh",
|
||||
Reason: "request failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return &AuthError{
|
||||
Op: "token_refresh",
|
||||
Reason: fmt.Sprintf("HTTP %d", resp.StatusCode),
|
||||
Err: ErrTokenExpired,
|
||||
}
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return &AuthError{
|
||||
Op: "token_refresh",
|
||||
Reason: "failed to parse response",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Extract new token
|
||||
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||
if refresh, ok := data["refreshToken"].(map[string]interface{}); ok {
|
||||
if token, ok := refresh["token"].(string); ok {
|
||||
a.token = token
|
||||
|
||||
// Update refresh token if provided
|
||||
if newRefreshToken, ok := refresh["refreshToken"].(string); ok {
|
||||
a.refreshToken = newRefreshToken
|
||||
}
|
||||
|
||||
// Update expiry
|
||||
if expiresIn, ok := refresh["expiresIn"].(float64); ok {
|
||||
a.tokenExpiry = time.Now().Add(time.Duration(expiresIn) * time.Second)
|
||||
} else {
|
||||
a.tokenExpiry = time.Now().Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
a.log.Debug("✅ Token refreshed successfully")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthError{
|
||||
Op: "token_refresh",
|
||||
Reason: "no token in response",
|
||||
Err: ErrTokenExpired,
|
||||
}
|
||||
}
|
||||
|
||||
// Logout invalidates the current session
|
||||
func (a *AuthManager) Logout() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.token == "" {
|
||||
return nil // Already logged out
|
||||
}
|
||||
|
||||
a.log.Info("Logging out from Kosmi...")
|
||||
|
||||
// Prepare logout mutation
|
||||
mutation := map[string]interface{}{
|
||||
"query": `mutation Logout {
|
||||
logout {
|
||||
ok
|
||||
}
|
||||
}`,
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(mutation)
|
||||
if err != nil {
|
||||
a.log.Warnf("Failed to marshal logout request: %v", err)
|
||||
// Continue with local cleanup
|
||||
} else {
|
||||
req, err := http.NewRequest("POST", kosmiHTTPURL, bytes.NewReader(jsonBody))
|
||||
if err == nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Referer", "https://app.kosmi.io/")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.token))
|
||||
|
||||
resp, err := a.httpClient.Do(req)
|
||||
if err != nil {
|
||||
a.log.Warnf("Logout request failed: %v", err)
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
a.log.Warnf("Logout returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear local state regardless of server response
|
||||
a.token = ""
|
||||
a.refreshToken = ""
|
||||
a.tokenExpiry = time.Time{}
|
||||
a.userID = ""
|
||||
|
||||
a.log.Info("✅ Logged out")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAuthenticated checks if we have a valid token
|
||||
func (a *AuthManager) IsAuthenticated() bool {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.token != "" && time.Now().Before(a.tokenExpiry)
|
||||
}
|
||||
|
||||
// GetUserID returns the authenticated user's ID
|
||||
func (a *AuthManager) GetUserID() string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.userID
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user