mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
first test
This commit is contained in:
+397
-2
@@ -1,8 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -31,6 +35,24 @@ type AuthResponse struct {
|
||||
User models.User `json:"user"`
|
||||
}
|
||||
|
||||
type PasswordResetRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
type PasswordResetConfirm struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
type PasswordResetCode struct {
|
||||
ID uint `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Used bool `json:"used"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// JWT Claims structure
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
@@ -73,9 +95,47 @@ func ValidateJWT(tokenString string) (*Claims, error) {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// AuthMiddleware middleware to protect routes
|
||||
// AuthMiddleware validates JWT tokens
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Check if demo mode is enabled
|
||||
if os.Getenv("VITE_DEMO_MODE") == "true" {
|
||||
path := c.Request.URL.Path
|
||||
// Set a demo user for specific routes in demo mode
|
||||
if strings.Contains(path, "/youtube") ||
|
||||
strings.Contains(path, "/learning-paths") ||
|
||||
strings.Contains(path, "/bookmarks") ||
|
||||
strings.Contains(path, "/tasks") ||
|
||||
strings.Contains(path, "/notes") ||
|
||||
strings.Contains(path, "/files") ||
|
||||
strings.Contains(path, "/time-entries") ||
|
||||
strings.Contains(path, "/calendar") ||
|
||||
strings.Contains(path, "/ai/settings") ||
|
||||
strings.Contains(path, "/ai/providers") ||
|
||||
strings.Contains(path, "/ai/test-connection") ||
|
||||
strings.Contains(path, "/search") ||
|
||||
strings.Contains(path, "/dashboard/stats") {
|
||||
// Set a demo user for these routes in demo mode
|
||||
c.Set("user", models.User{
|
||||
ID: 1,
|
||||
Username: "demo",
|
||||
Email: "demo@trackeep.com",
|
||||
})
|
||||
c.Set("user_id", uint(1))
|
||||
c.Set("userID", uint(1)) // Add this for compatibility with handlers
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Skip auth for AI settings in demo mode for testing
|
||||
if os.Getenv("VITE_DEMO_MODE") == "true" && strings.Contains(c.Request.URL.Path, "/ai/settings") {
|
||||
c.Set("user_id", uint(1))
|
||||
c.Set("userID", uint(1))
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(401, gin.H{"error": "Authorization header required"})
|
||||
@@ -105,11 +165,28 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
c.Set("user", user)
|
||||
c.Set("userID", user.ID)
|
||||
c.Set("user_id", user.ID)
|
||||
c.Set("userID", user.ID) // Add this for compatibility with handlers
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// CheckUsers checks if any users exist in the system
|
||||
func CheckUsers(c *gin.Context) {
|
||||
db := config.GetDB()
|
||||
|
||||
var count int64
|
||||
if err := db.Model(&models.User{}).Count(&count).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to check users"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"hasUsers": count > 0,
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
|
||||
// Register handles user registration
|
||||
func Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
@@ -317,3 +394,321 @@ func ChangePassword(c *gin.Context) {
|
||||
func Logout(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Logged out successfully"})
|
||||
}
|
||||
|
||||
// generateResetCode generates a cryptographically secure 8-character reset code
|
||||
func generateResetCode() (string, error) {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
bytes := make([]byte, 8)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, b := range bytes {
|
||||
bytes[i] = charset[b%byte(len(charset))]
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// sendResetEmail sends a password reset email
|
||||
func sendResetEmail(email, code string) error {
|
||||
smtpHost := os.Getenv("SMTP_HOST")
|
||||
smtpPort := os.Getenv("SMTP_PORT")
|
||||
smtpUsername := os.Getenv("SMTP_USERNAME")
|
||||
smtpPassword := os.Getenv("SMTP_PASSWORD")
|
||||
fromEmail := os.Getenv("SMTP_FROM_EMAIL")
|
||||
fromName := os.Getenv("SMTP_FROM_NAME")
|
||||
|
||||
if smtpHost == "" || smtpUsername == "" || smtpPassword == "" || fromEmail == "" {
|
||||
return errors.New("SMTP configuration not complete")
|
||||
}
|
||||
|
||||
// Create auth
|
||||
auth := smtp.PlainAuth("", smtpUsername, smtpPassword, smtpHost)
|
||||
|
||||
// Compose message
|
||||
subject := "Password Reset - Trackeep"
|
||||
body := fmt.Sprintf(`
|
||||
Hello,
|
||||
|
||||
You requested a password reset for your Trackeep account.
|
||||
|
||||
Your reset code is: %s
|
||||
|
||||
This code will expire in 15 minutes.
|
||||
|
||||
If you didn't request this, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
%s
|
||||
`, code, fromName)
|
||||
|
||||
msg := fmt.Sprintf("From: %s <%s>\r\nTo: %s\r\nSubject: %s\r\n\r\n%s",
|
||||
fromName, fromEmail, email, subject, body)
|
||||
|
||||
// Send email
|
||||
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
|
||||
return smtp.SendMail(addr, auth, fromEmail, []string{email}, []byte(msg))
|
||||
}
|
||||
|
||||
// RequestPasswordReset handles password reset requests
|
||||
func RequestPasswordReset(c *gin.Context) {
|
||||
var req PasswordResetRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
db := config.GetDB()
|
||||
|
||||
// Check if user exists
|
||||
var user models.User
|
||||
if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
// Don't reveal if user exists or not
|
||||
c.JSON(200, gin.H{"message": "If an account with this email exists, a reset code has been sent"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate reset code
|
||||
code, err := generateResetCode()
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to generate reset code"})
|
||||
return
|
||||
}
|
||||
|
||||
// Store reset code in database (you might want to create a separate table for this)
|
||||
resetCode := PasswordResetCode{
|
||||
Email: req.Email,
|
||||
Code: code,
|
||||
ExpiresAt: time.Now().Add(15 * time.Minute),
|
||||
Used: false,
|
||||
}
|
||||
|
||||
// For now, we'll use a simple approach - in production, you'd want a proper table
|
||||
// Create the reset_codes table if it doesn't exist
|
||||
db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS password_reset_codes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
used BOOLEAN DEFAULT FALSE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
|
||||
if err := db.Create(&resetCode).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to store reset code"})
|
||||
return
|
||||
}
|
||||
|
||||
// Send email
|
||||
if err := sendResetEmail(req.Email, code); err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to send reset email: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"message": "Reset code sent to your email"})
|
||||
}
|
||||
|
||||
// ConfirmPasswordReset handles password reset confirmation
|
||||
func ConfirmPasswordReset(c *gin.Context) {
|
||||
var req PasswordResetConfirm
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
db := config.GetDB()
|
||||
|
||||
// Find valid reset code
|
||||
var resetCode PasswordResetCode
|
||||
if err := db.Where("code = ? AND used = ? AND expires_at > ?", req.Code, false, time.Now()).First(&resetCode).Error; err != nil {
|
||||
c.JSON(400, gin.H{"error": "Invalid or expired reset code"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find user
|
||||
var user models.User
|
||||
if err := db.Where("email = ?", resetCode.Email).First(&user).Error; err != nil {
|
||||
c.JSON(400, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update password
|
||||
if err := db.Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to update password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Mark reset code as used
|
||||
db.Model(&resetCode).Update("used", true)
|
||||
|
||||
c.JSON(200, gin.H{"message": "Password reset successfully"})
|
||||
}
|
||||
|
||||
// GetDashboardStats returns dashboard statistics for the current user
|
||||
func GetDashboardStats(c *gin.Context) {
|
||||
// Check if demo mode is enabled
|
||||
if os.Getenv("VITE_DEMO_MODE") == "true" {
|
||||
// Return mock dashboard stats for demo mode
|
||||
stats := gin.H{
|
||||
"totalBookmarks": 156,
|
||||
"totalTasks": 42,
|
||||
"totalFiles": 234,
|
||||
"totalNotes": 89,
|
||||
"recentActivity": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "task",
|
||||
"title": "Complete project documentation",
|
||||
"timestamp": "1 hour ago",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "bookmark",
|
||||
"title": "SolidJS Documentation",
|
||||
"timestamp": "2 hours ago",
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "note",
|
||||
"title": "Meeting notes - Q1 planning",
|
||||
"timestamp": "3 hours ago",
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "file",
|
||||
"title": "project-roadmap.pdf",
|
||||
"timestamp": "4 hours ago",
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "task",
|
||||
"title": "Review pull requests",
|
||||
"timestamp": "5 hours ago",
|
||||
},
|
||||
},
|
||||
}
|
||||
c.JSON(200, stats)
|
||||
return
|
||||
}
|
||||
|
||||
user, exists := c.Get("user")
|
||||
if !exists {
|
||||
c.JSON(401, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
currentUser := user.(models.User)
|
||||
db := config.GetDB()
|
||||
|
||||
// Get counts for each entity type
|
||||
var bookmarkCount, taskCount, fileCount, noteCount int64
|
||||
|
||||
// Count bookmarks
|
||||
db.Model(&models.Bookmark{}).Where("user_id = ?", currentUser.ID).Count(&bookmarkCount)
|
||||
|
||||
// Count tasks
|
||||
db.Model(&models.Task{}).Where("user_id = ?", currentUser.ID).Count(&taskCount)
|
||||
|
||||
// Count files
|
||||
db.Model(&models.File{}).Where("user_id = ?", currentUser.ID).Count(&fileCount)
|
||||
|
||||
// Count notes
|
||||
db.Model(&models.Note{}).Where("user_id = ?", currentUser.ID).Count(¬eCount)
|
||||
|
||||
// Get recent activity
|
||||
type RecentActivity struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
var activities []RecentActivity
|
||||
|
||||
// Get recent bookmarks
|
||||
var bookmarks []models.Bookmark
|
||||
db.Where("user_id = ?", currentUser.ID).Order("created_at DESC").Limit(3).Find(&bookmarks)
|
||||
for _, bookmark := range bookmarks {
|
||||
activities = append(activities, RecentActivity{
|
||||
ID: bookmark.ID,
|
||||
Type: "bookmark",
|
||||
Title: bookmark.Title,
|
||||
Timestamp: formatTimeAgo(bookmark.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
// Get recent tasks
|
||||
var tasks []models.Task
|
||||
db.Where("user_id = ?", currentUser.ID).Order("created_at DESC").Limit(3).Find(&tasks)
|
||||
for _, task := range tasks {
|
||||
activities = append(activities, RecentActivity{
|
||||
ID: task.ID,
|
||||
Type: "task",
|
||||
Title: task.Title,
|
||||
Timestamp: formatTimeAgo(task.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
// Get recent notes
|
||||
var notes []models.Note
|
||||
db.Where("user_id = ?", currentUser.ID).Order("created_at DESC").Limit(3).Find(¬es)
|
||||
for _, note := range notes {
|
||||
activities = append(activities, RecentActivity{
|
||||
ID: note.ID,
|
||||
Type: "note",
|
||||
Title: note.Title,
|
||||
Timestamp: formatTimeAgo(note.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
// Sort activities by timestamp (most recent first)
|
||||
// For simplicity, we'll just take the first 5
|
||||
if len(activities) > 5 {
|
||||
activities = activities[:5]
|
||||
}
|
||||
|
||||
stats := gin.H{
|
||||
"totalBookmarks": bookmarkCount,
|
||||
"totalTasks": taskCount,
|
||||
"totalFiles": fileCount,
|
||||
"totalNotes": noteCount,
|
||||
"recentActivity": activities,
|
||||
}
|
||||
|
||||
c.JSON(200, stats)
|
||||
}
|
||||
|
||||
// formatTimeAgo formats a time as a relative "time ago" string
|
||||
func formatTimeAgo(t time.Time) string {
|
||||
duration := time.Since(t)
|
||||
|
||||
if duration < time.Hour {
|
||||
minutes := int(duration.Minutes())
|
||||
if minutes == 1 {
|
||||
return "1 minute ago"
|
||||
}
|
||||
return fmt.Sprintf("%d minutes ago", minutes)
|
||||
} else if duration < 24*time.Hour {
|
||||
hours := int(duration.Hours())
|
||||
if hours == 1 {
|
||||
return "1 hour ago"
|
||||
}
|
||||
return fmt.Sprintf("%d hours ago", hours)
|
||||
} else if duration < 7*24*time.Hour {
|
||||
days := int(duration.Hours() / 24)
|
||||
if days == 1 {
|
||||
return "1 day ago"
|
||||
}
|
||||
return fmt.Sprintf("%d days ago", days)
|
||||
} else {
|
||||
return t.Format("Jan 2, 2006")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user