package handlers import ( "encoding/json" "log" "net/http" "time" "omixlab.com/mosis-portal/internal/api/middleware" "omixlab.com/mosis-portal/internal/auth" "omixlab.com/mosis-portal/internal/database" ) // AuthHandler handles authentication endpoints type AuthHandler struct { oauthManager *auth.OAuthManager jwtManager *auth.JWTManager db *database.DB } // NewAuthHandler creates a new auth handler func NewAuthHandler(oauthManager *auth.OAuthManager, jwtManager *auth.JWTManager, db *database.DB) *AuthHandler { return &AuthHandler{ oauthManager: oauthManager, jwtManager: jwtManager, db: db, } } // OAuthStart initiates OAuth flow func (h *AuthHandler) OAuthStart(provider auth.OAuthProvider) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := auth.GenerateState() if err != nil { http.Error(w, "Failed to generate state", http.StatusInternalServerError) return } // Store state in cookie for verification http.SetCookie(w, &http.Cookie{ Name: "oauth_state", Value: state, Path: "/", MaxAge: 300, // 5 minutes HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, }) authURL, err := h.oauthManager.GetAuthURL(provider, state) if err != nil { http.Error(w, "OAuth not configured: "+err.Error(), http.StatusNotImplemented) return } http.Redirect(w, r, authURL, http.StatusTemporaryRedirect) } } // OAuthCallback handles OAuth callback func (h *AuthHandler) OAuthCallback(provider auth.OAuthProvider) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Verify state stateCookie, err := r.Cookie("oauth_state") if err != nil || stateCookie.Value != r.URL.Query().Get("state") { http.Error(w, "Invalid state", http.StatusBadRequest) return } // Clear state cookie http.SetCookie(w, &http.Cookie{ Name: "oauth_state", Value: "", Path: "/", MaxAge: -1, }) // Check for error from provider if errMsg := r.URL.Query().Get("error"); errMsg != "" { http.Error(w, "OAuth error: "+errMsg, http.StatusBadRequest) return } code := r.URL.Query().Get("code") if code == "" { http.Error(w, "No code provided", http.StatusBadRequest) return } // Exchange code for user info oauthUser, err := h.oauthManager.Exchange(r.Context(), provider, code) if err != nil { log.Printf("OAuth exchange failed: %v", err) http.Error(w, "Authentication failed", http.StatusInternalServerError) return } if oauthUser.Email == "" { http.Error(w, "Email not available from OAuth provider", http.StatusBadRequest) return } // Find or create developer developer, err := h.db.FindOrCreateDeveloper(r.Context(), &database.Developer{ Email: oauthUser.Email, Name: oauthUser.Name, OAuthProvider: string(oauthUser.Provider), OAuthID: oauthUser.ID, AvatarURL: oauthUser.Avatar, }) if err != nil { log.Printf("Failed to find/create developer: %v", err) http.Error(w, "Failed to create account", http.StatusInternalServerError) return } // Generate tokens tokenPair, err := h.jwtManager.GenerateTokenPair(developer.ID, developer.Email) if err != nil { log.Printf("Failed to generate tokens: %v", err) http.Error(w, "Failed to generate tokens", http.StatusInternalServerError) return } // Set refresh token in HttpOnly cookie http.SetCookie(w, &http.Cookie{ Name: "refresh_token", Value: tokenPair.RefreshToken, Path: "/", MaxAge: 30 * 24 * 60 * 60, // 30 days HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteStrictMode, }) // Log the authentication h.db.LogAudit(r.Context(), developer.ID, "oauth_login", r.RemoteAddr, r.UserAgent(), true, "") // Return access token w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "access_token": tokenPair.AccessToken, "token_type": tokenPair.TokenType, "expires_in": tokenPair.ExpiresIn, "developer": map[string]interface{}{ "id": developer.ID, "email": developer.Email, "name": developer.Name, "avatar_url": developer.AvatarURL, }, }) } } // Refresh refreshes the access token using refresh token func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) { // Get refresh token from cookie or body var refreshToken string cookie, err := r.Cookie("refresh_token") if err == nil { refreshToken = cookie.Value } // If not in cookie, try request body if refreshToken == "" { var body struct { RefreshToken string `json:"refresh_token"` } if err := json.NewDecoder(r.Body).Decode(&body); err == nil { refreshToken = body.RefreshToken } } if refreshToken == "" { http.Error(w, "Refresh token required", http.StatusBadRequest) return } // Validate refresh token claims, err := h.jwtManager.ValidateRefreshToken(refreshToken) if err != nil { // Clear invalid cookie http.SetCookie(w, &http.Cookie{ Name: "refresh_token", Value: "", Path: "/", MaxAge: -1, }) http.Error(w, "Invalid refresh token", http.StatusUnauthorized) return } // Generate new token pair tokenPair, err := h.jwtManager.GenerateTokenPair(claims.Subject, claims.Email) if err != nil { http.Error(w, "Failed to generate tokens", http.StatusInternalServerError) return } // Set new refresh token in cookie http.SetCookie(w, &http.Cookie{ Name: "refresh_token", Value: tokenPair.RefreshToken, Path: "/", MaxAge: 30 * 24 * 60 * 60, HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteStrictMode, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "access_token": tokenPair.AccessToken, "token_type": tokenPair.TokenType, "expires_in": tokenPair.ExpiresIn, }) } // Logout invalidates the current session func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { // Clear refresh token cookie http.SetCookie(w, &http.Cookie{ Name: "refresh_token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteStrictMode, }) // Log the logout if authenticated developerID := middleware.GetDeveloperID(r.Context()) if developerID != "" { h.db.LogAudit(r.Context(), developerID, "logout", r.RemoteAddr, r.UserAgent(), true, "") } w.WriteHeader(http.StatusNoContent) } // Me returns the current user's information func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) { developerID := middleware.GetDeveloperID(r.Context()) if developerID == "" { http.Error(w, "Not authenticated", http.StatusUnauthorized) return } developer, err := h.db.GetDeveloper(r.Context(), developerID) if err != nil { http.Error(w, "Developer not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "id": developer.ID, "email": developer.Email, "name": developer.Name, "avatar_url": developer.AvatarURL, "created_at": developer.CreatedAt.Format(time.RFC3339), }) }