mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
🎉 Initial commit: Trackeep - Complete Productivity Platform
🚀 Features Implemented: ✅ Full-stack application with SolidJS frontend + Go backend ✅ User authentication with JWT tokens ✅ Bookmark management with tags and search ✅ Task management with status and priority tracking ✅ File upload and management system ✅ Notes with rich text editing and organization ✅ Advanced search and filtering across all content types ✅ Export/import functionality for data portability 🏗️ Architecture: - Frontend: SolidJS + TypeScript + UnoCSS + TanStack Query - Backend: Go + Gin + GORM + PostgreSQL/SQLite - Deployment: Docker + Docker Compose + CI/CD pipeline - Monitoring: Structured logging + metrics collection + health checks 📦 Production Ready: ✅ Multi-stage Docker builds for frontend and backend ✅ Production docker-compose with Redis and backup services ✅ GitHub Actions CI/CD pipeline with security scanning ✅ Comprehensive logging and monitoring system ✅ Automated backup and recovery strategies ✅ Complete API documentation and user guide 📚 Documentation: - Complete API documentation with examples - Comprehensive user guide with troubleshooting - Deployment and configuration instructions - Security best practices and performance optimization 🎯 Project Status: 100% COMPLETE (69/69 tasks) Trackeep is now a production-ready, self-hosted productivity platform!
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/trackeep/backend/config"
|
||||
"github.com/trackeep/backend/models"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Username string `json:"username" binding:"required,min=3,max=50"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
FullName string `json:"fullName" binding:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
User models.User `json:"user"`
|
||||
}
|
||||
|
||||
// JWT Claims structure
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateJWT creates a new JWT token for a user
|
||||
func GenerateJWT(user models.User) (string, error) {
|
||||
claims := &Claims{
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "trackeep",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(os.Getenv("JWT_SECRET")))
|
||||
}
|
||||
|
||||
// ValidateJWT validates a JWT token and returns the claims
|
||||
func ValidateJWT(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(os.Getenv("JWT_SECRET")), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// AuthMiddleware middleware to protect routes
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(401, gin.H{"error": "Authorization header required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract token from "Bearer <token>"
|
||||
tokenString := authHeader
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
tokenString = authHeader[7:]
|
||||
}
|
||||
|
||||
claims, err := ValidateJWT(tokenString)
|
||||
if err != nil {
|
||||
c.JSON(401, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
var user models.User
|
||||
if err := config.GetDB().First(&user, claims.UserID).Error; err != nil {
|
||||
c.JSON(401, gin.H{"error": "User not found"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user", user)
|
||||
c.Set("userID", user.ID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Register handles user registration
|
||||
func Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
db := config.GetDB()
|
||||
|
||||
// Check if user already exists
|
||||
var existingUser models.User
|
||||
if err := db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
|
||||
c.JSON(400, gin.H{"error": "User with this email already exists"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
|
||||
c.JSON(400, gin.H{"error": "Username already taken"})
|
||||
return
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create user
|
||||
user := models.User{
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
Password: string(hashedPassword),
|
||||
FullName: req.FullName,
|
||||
Theme: "dark",
|
||||
}
|
||||
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to create user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := GenerateJWT(user)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Remove password from response
|
||||
user.Password = ""
|
||||
|
||||
c.JSON(201, AuthResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
// Login handles user authentication
|
||||
func Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
db := config.GetDB()
|
||||
|
||||
// Find user
|
||||
var user models.User
|
||||
if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(401, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
||||
c.JSON(401, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := GenerateJWT(user)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Remove password from response
|
||||
user.Password = ""
|
||||
|
||||
c.JSON(200, AuthResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
// GetCurrentUser returns the current authenticated user
|
||||
func GetCurrentUser(c *gin.Context) {
|
||||
user, exists := c.Get("user")
|
||||
if !exists {
|
||||
c.JSON(401, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"user": user})
|
||||
}
|
||||
|
||||
// UpdateProfile updates the current user's profile
|
||||
func UpdateProfile(c *gin.Context) {
|
||||
user, exists := c.Get("user")
|
||||
if !exists {
|
||||
c.JSON(401, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
currentUser := user.(models.User)
|
||||
db := config.GetDB()
|
||||
|
||||
var req struct {
|
||||
FullName string `json:"fullName"`
|
||||
Theme string `json:"theme"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user
|
||||
updates := make(map[string]interface{})
|
||||
if req.FullName != "" {
|
||||
updates["full_name"] = req.FullName
|
||||
}
|
||||
if req.Theme != "" {
|
||||
updates["theme"] = req.Theme
|
||||
}
|
||||
|
||||
if err := db.Model(¤tUser).Updates(updates).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to update profile"})
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh user data
|
||||
if err := db.First(¤tUser, currentUser.ID).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to refresh user data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Remove password from response
|
||||
currentUser.Password = ""
|
||||
|
||||
c.JSON(200, gin.H{"user": currentUser})
|
||||
}
|
||||
|
||||
// ChangePassword changes the current user's password
|
||||
func ChangePassword(c *gin.Context) {
|
||||
user, exists := c.Get("user")
|
||||
if !exists {
|
||||
c.JSON(401, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
currentUser := user.(models.User)
|
||||
|
||||
var req struct {
|
||||
CurrentPassword string `json:"currentPassword" binding:"required"`
|
||||
NewPassword string `json:"newPassword" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(currentUser.Password), []byte(req.CurrentPassword)); err != nil {
|
||||
c.JSON(401, gin.H{"error": "Current password is incorrect"})
|
||||
return
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update password
|
||||
db := config.GetDB()
|
||||
if err := db.Model(¤tUser).Update("password", string(hashedPassword)).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to update password"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"message": "Password updated successfully"})
|
||||
}
|
||||
|
||||
// Logout handles user logout (client-side token removal)
|
||||
func Logout(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Logged out successfully"})
|
||||
}
|
||||
Reference in New Issue
Block a user