mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
first test
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/trackeep/backend/config"
|
||||
"github.com/trackeep/backend/models"
|
||||
)
|
||||
|
||||
// GitHub OAuth configuration
|
||||
var githubOAuthConfig *oauth2.Config
|
||||
|
||||
func initGitHubOAuth() {
|
||||
githubOAuthConfig = &oauth2.Config{
|
||||
ClientID: os.Getenv("GITHUB_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
|
||||
RedirectURL: os.Getenv("GITHUB_REDIRECT_URL"),
|
||||
Scopes: []string{"user:email", "repo"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://github.com/login/oauth/authorize",
|
||||
TokenURL: "https://github.com/login/oauth/access_token",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GitHubUser represents the GitHub user profile
|
||||
type GitHubUser struct {
|
||||
ID int `json:"id"`
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
// GitHubRepo represents a GitHub repository
|
||||
type GitHubRepo struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Description string `json:"description"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Stargazers int `json:"stargazers_count"`
|
||||
Forks int `json:"forks_count"`
|
||||
Watchers int `json:"watchers_count"`
|
||||
Language string `json:"language"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Size int `json:"size"`
|
||||
OpenIssues int `json:"open_issues_count"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
}
|
||||
|
||||
// GitHubLogin initiates the GitHub OAuth flow
|
||||
func GitHubLogin(c *gin.Context) {
|
||||
if githubOAuthConfig == nil {
|
||||
initGitHubOAuth()
|
||||
}
|
||||
|
||||
// Generate state parameter to prevent CSRF
|
||||
state := generateRandomString(32)
|
||||
|
||||
// Store state in session or cookie (simplified here)
|
||||
c.SetCookie("oauth_state", state, 3600, "/", "", false, true)
|
||||
|
||||
// Redirect to GitHub for authorization
|
||||
authURL := githubOAuthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||
c.Redirect(http.StatusTemporaryRedirect, authURL)
|
||||
}
|
||||
|
||||
// GitHubCallback handles the GitHub OAuth callback
|
||||
func GitHubCallback(c *gin.Context) {
|
||||
if githubOAuthConfig == nil {
|
||||
initGitHubOAuth()
|
||||
}
|
||||
|
||||
// Verify state parameter
|
||||
storedState, err := c.Cookie("oauth_state")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "State not found"})
|
||||
return
|
||||
}
|
||||
|
||||
state := c.Query("state")
|
||||
if state != storedState {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid state"})
|
||||
return
|
||||
}
|
||||
|
||||
// Clear the state cookie
|
||||
c.SetCookie("oauth_state", "", -1, "/", "", false, true)
|
||||
|
||||
// Exchange authorization code for access token
|
||||
code := c.Query("code")
|
||||
token, err := githubOAuthConfig.Exchange(context.Background(), code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user info from GitHub
|
||||
user, err := getGitHubUser(token.AccessToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get or create user in database
|
||||
db := c.MustGet("db").(*gorm.DB)
|
||||
var existingUser models.User
|
||||
|
||||
// First try to find by GitHub ID
|
||||
err = db.Where("github_id = ?", user.ID).First(&existingUser).Error
|
||||
if err != nil {
|
||||
// If not found by GitHub ID, try by email
|
||||
err = db.Where("email = ?", user.Email).First(&existingUser).Error
|
||||
if err != nil {
|
||||
// Create new user
|
||||
newUser := models.User{
|
||||
Username: user.Login,
|
||||
Email: user.Email,
|
||||
FullName: user.Name,
|
||||
GitHubID: user.ID,
|
||||
AvatarURL: user.AvatarURL,
|
||||
Provider: "github",
|
||||
}
|
||||
|
||||
if err := db.Create(&newUser).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
|
||||
return
|
||||
}
|
||||
existingUser = newUser
|
||||
} else {
|
||||
// Update existing user with GitHub info
|
||||
existingUser.GitHubID = user.ID
|
||||
existingUser.AvatarURL = user.AvatarURL
|
||||
existingUser.Provider = "github"
|
||||
db.Save(&existingUser)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": existingUser.ID,
|
||||
"email": existingUser.Email,
|
||||
"username": existingUser.Username,
|
||||
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7 days
|
||||
})
|
||||
|
||||
tokenString, err := jwtToken.SignedString([]byte(config.JWTSecret))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to frontend with token
|
||||
redirectURL := fmt.Sprintf("%s/auth/callback?token=%s", os.Getenv("FRONTEND_URL"), tokenString)
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
}
|
||||
|
||||
// getGitHubUser fetches user information from GitHub API
|
||||
func getGitHubUser(accessToken string) (*GitHubUser, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user GitHubUser
|
||||
if err := json.Unmarshal(body, &user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If email is not public, fetch user emails
|
||||
if user.Email == "" {
|
||||
email, err := getPrimaryEmail(accessToken)
|
||||
if err == nil {
|
||||
user.Email = email
|
||||
}
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// getPrimaryEmail fetches the primary email for the user
|
||||
func getPrimaryEmail(accessToken string) (string, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var emails []struct {
|
||||
Email string `json:"email"`
|
||||
Primary bool `json:"primary"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &emails); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, email := range emails {
|
||||
if email.Primary && email.Verified {
|
||||
return email.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no primary verified email found")
|
||||
}
|
||||
|
||||
// HandleOAuthCallback handles the callback from the centralized OAuth service
|
||||
func HandleOAuthCallback(c *gin.Context) {
|
||||
// Get the token from the query parameters
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No token provided"})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the JWT from the OAuth service
|
||||
claims := jwt.MapClaims{}
|
||||
parsedToken, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
// Use the OAuth service's JWT secret (should be shared)
|
||||
return []byte(os.Getenv("OAUTH_JWT_SECRET")), nil
|
||||
})
|
||||
|
||||
if err != nil || !parsedToken.Valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid OAuth token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user information from OAuth service
|
||||
username, _ := claims["username"].(string)
|
||||
email, _ := claims["email"].(string)
|
||||
githubID, _ := claims["github_id"]
|
||||
accessToken, _ := claims["access_token"].(string)
|
||||
|
||||
// Get database
|
||||
db := c.MustGet("db").(*gorm.DB)
|
||||
|
||||
// Find or create user in local database
|
||||
var user models.User
|
||||
err = db.Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
// Create new user
|
||||
newUser := models.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
GitHubID: int(githubID.(float64)), // JWT numbers are float64
|
||||
Provider: "github",
|
||||
}
|
||||
|
||||
if err := db.Create(&newUser).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
|
||||
return
|
||||
}
|
||||
user = newUser
|
||||
} else {
|
||||
// Update existing user with GitHub info
|
||||
user.GitHubID = int(githubID.(float64))
|
||||
user.Provider = "github"
|
||||
db.Save(&user)
|
||||
}
|
||||
|
||||
// Generate Trackeep JWT token
|
||||
trackeepToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
"username": user.Username,
|
||||
"github_id": user.GitHubID,
|
||||
"access_token": accessToken, // Pass through the GitHub access token
|
||||
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7 days
|
||||
})
|
||||
|
||||
trackeepTokenString, err := trackeepToken.SignedString([]byte(os.Getenv("JWT_SECRET")))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to frontend with Trackeep token
|
||||
redirectURL := fmt.Sprintf("%s/auth/callback?token=%s", os.Getenv("FRONTEND_URL"), trackeepTokenString)
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
}
|
||||
|
||||
// GetCurrentUser returns the current authenticated user with GitHub info
|
||||
func GetCurrentUserWithGitHub(c *gin.Context) {
|
||||
user, exists := c.Get("user")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
currentUser := user.(models.User)
|
||||
|
||||
// Remove sensitive data
|
||||
currentUser.Password = ""
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"user": currentUser})
|
||||
}
|
||||
func GetGitHubRepos(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
db := c.MustGet("db").(*gorm.DB)
|
||||
var user models.User
|
||||
if err := db.First(&user, userID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if user.GitHubID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "GitHub not connected"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the JWT token from the request header
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "No authorization header"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract token from "Bearer <token>"
|
||||
tokenString := authHeader
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
tokenString = authHeader[7:]
|
||||
}
|
||||
|
||||
// Parse the JWT to get the GitHub access token from the centralized OAuth service
|
||||
claims := jwt.MapClaims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(os.Getenv("JWT_SECRET")), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract GitHub access token from the OAuth service JWT
|
||||
githubAccessToken, ok := claims["access_token"]
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "GitHub access token not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch repositories using the GitHub access token
|
||||
repos, err := fetchGitHubRepos(githubAccessToken.(string))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch repos: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"repos": repos})
|
||||
}
|
||||
|
||||
// fetchGitHubRepos fetches repositories from GitHub API
|
||||
func fetchGitHubRepos(accessToken string) ([]GitHubRepo, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/user/repos?type=owner&sort=updated&per_page=100", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var repos []GitHubRepo
|
||||
if err := json.Unmarshal(body, &repos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// generateRandomString generates a random string for state parameter
|
||||
func generateRandomString(length int) string {
|
||||
bytes := make([]byte, length)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
Reference in New Issue
Block a user