🎉 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:
Tomas Dvorak
2026-01-26 12:36:49 +01:00
commit 18aa702174
79 changed files with 12885 additions and 0 deletions
+319
View File
@@ -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(&currentUser).Updates(updates).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to update profile"})
return
}
// Refresh user data
if err := db.First(&currentUser, 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(&currentUser).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"})
}