mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
Configure Docker publishing with correct GitHub username
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/trackeep/backend/config"
|
||||
"github.com/trackeep/backend/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// AdminMiddleware checks if user is admin
|
||||
@@ -212,6 +213,71 @@ func AdminGetUsers(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// AdminCreateUser handles POST /api/v1/admin/users
|
||||
func AdminCreateUser(c *gin.Context) {
|
||||
db := config.GetDB()
|
||||
|
||||
var req 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"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
role := req.Role
|
||||
if role == "" {
|
||||
role = "user"
|
||||
}
|
||||
if role != "user" && role != "admin" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role. Must be 'user' or 'admin'"})
|
||||
return
|
||||
}
|
||||
|
||||
var existing models.User
|
||||
if err := db.Where("email = ?", req.Email).First(&existing).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "User with this email already exists"})
|
||||
return
|
||||
}
|
||||
if err := db.Where("username = ?", req.Username).First(&existing).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username already taken"})
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
Password: string(hashedPassword),
|
||||
FullName: req.FullName,
|
||||
Role: role,
|
||||
Theme: "dark",
|
||||
}
|
||||
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
|
||||
return
|
||||
}
|
||||
|
||||
_ = ensureMessagingDefaults(db, user.ID)
|
||||
|
||||
user.Password = ""
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "User created successfully",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminUpdateUserRole handles PUT /api/v1/admin/users/:id/role
|
||||
func AdminUpdateUserRole(c *gin.Context) {
|
||||
db := config.GetDB()
|
||||
|
||||
@@ -588,7 +588,7 @@ Provide a JSON array of task objects with:
|
||||
- context_data: Additional context
|
||||
- deadline: Suggested deadline (ISO date or null)
|
||||
- estimated_time: Estimated time in minutes
|
||||
- confidence: Confidence score 0-1`, contextData, limit)
|
||||
- confidence: Confidence score 0-1`, limit, contextData)
|
||||
|
||||
messages := []services.Message{
|
||||
{Role: "system", Content: "You are a productivity assistant. Always respond with valid JSON array."},
|
||||
|
||||
@@ -95,6 +95,33 @@ func ValidateJWT(tokenString string) (*Claims, error) {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
func getAuthenticatedUserFromHeader(c *gin.Context, db *gorm.DB) (*models.User, error) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
return nil, errors.New("authorization header required")
|
||||
}
|
||||
|
||||
tokenString := authHeader
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
tokenString = strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||
}
|
||||
if tokenString == "" {
|
||||
return nil, errors.New("invalid authorization header")
|
||||
}
|
||||
|
||||
claims, err := ValidateJWT(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.First(&user, claims.UserID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// AuthMiddleware validates JWT tokens
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
@@ -202,6 +229,24 @@ func Register(c *gin.Context) {
|
||||
|
||||
db := config.GetDB()
|
||||
|
||||
// Registration rules:
|
||||
// - First user can self-register and becomes admin.
|
||||
// - After that, only authenticated admins can create users.
|
||||
var userCount int64
|
||||
if err := db.Model(&models.User{}).Count(&userCount).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "Failed to check existing users"})
|
||||
return
|
||||
}
|
||||
|
||||
isFirstUser := userCount == 0
|
||||
if !isFirstUser {
|
||||
requester, err := getAuthenticatedUserFromHeader(c, db)
|
||||
if err != nil || requester.Role != "admin" {
|
||||
c.JSON(403, gin.H{"error": "Registration is disabled. Only an administrator can create users."})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
var existingUser models.User
|
||||
if err := db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
|
||||
@@ -222,11 +267,17 @@ func Register(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create user
|
||||
role := "user"
|
||||
if isFirstUser {
|
||||
role = "admin"
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
Password: string(hashedPassword),
|
||||
FullName: req.FullName,
|
||||
Role: role,
|
||||
Theme: "dark",
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,10 +19,40 @@ import (
|
||||
func GetFiles(c *gin.Context) {
|
||||
var files []models.File
|
||||
|
||||
// TODO: Get user ID from authentication context
|
||||
userID := uint(1) // Placeholder
|
||||
userID := c.GetUint("user_id")
|
||||
if userID == 0 {
|
||||
userID = c.GetUint("userID")
|
||||
}
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DB.Where("user_id = ?", userID).Find(&files).Error; err != nil {
|
||||
query := models.DB.Where("user_id = ?", userID)
|
||||
|
||||
if rawQuery := strings.TrimSpace(c.Query("q")); rawQuery != "" {
|
||||
needle := "%" + strings.ToLower(rawQuery) + "%"
|
||||
query = query.Where("LOWER(original_name) LIKE ? OR LOWER(description) LIKE ?", needle, needle)
|
||||
}
|
||||
|
||||
limitApplied := false
|
||||
if limitRaw := strings.TrimSpace(c.Query("limit")); limitRaw != "" {
|
||||
limit, err := strconv.Atoi(limitRaw)
|
||||
if err != nil || limit <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid limit"})
|
||||
return
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
query = query.Limit(limit)
|
||||
limitApplied = true
|
||||
}
|
||||
if !limitApplied && strings.TrimSpace(c.Query("q")) != "" {
|
||||
query = query.Limit(20)
|
||||
}
|
||||
|
||||
if err := query.Order("created_at DESC").Find(&files).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve files"})
|
||||
return
|
||||
}
|
||||
|
||||
+151
-28
@@ -640,41 +640,23 @@ func CreateConversationMessage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Body) == "" && len(req.Attachments) == 0 {
|
||||
trimmedBody := strings.TrimSpace(req.Body)
|
||||
if trimmedBody == "" && len(req.Attachments) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Message body or attachments are required"})
|
||||
return
|
||||
}
|
||||
|
||||
metadataJSON := "{}"
|
||||
if req.Metadata != nil {
|
||||
if raw, err := json.Marshal(req.Metadata); err == nil {
|
||||
metadataJSON = string(raw)
|
||||
}
|
||||
}
|
||||
|
||||
message := models.Message{
|
||||
ConversationID: conversationID,
|
||||
SenderID: userID,
|
||||
Body: strings.TrimSpace(req.Body),
|
||||
MetadataJSON: metadataJSON,
|
||||
}
|
||||
if err := models.DB.Create(&message).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create message"})
|
||||
return
|
||||
}
|
||||
|
||||
attachmentRows := make([]models.MessageAttachment, 0, len(req.Attachments))
|
||||
for _, a := range req.Attachments {
|
||||
attachmentRows = append(attachmentRows, models.MessageAttachment{
|
||||
MessageID: message.ID,
|
||||
Kind: normalizeAttachmentKind(a.Kind),
|
||||
FileID: a.FileID,
|
||||
URL: a.URL,
|
||||
Title: a.Title,
|
||||
Kind: normalizeAttachmentKind(a.Kind),
|
||||
FileID: a.FileID,
|
||||
URL: a.URL,
|
||||
Title: a.Title,
|
||||
})
|
||||
}
|
||||
|
||||
suggestions, inferredAttachments, isSensitive := services.DetectMessageContent(message.Body)
|
||||
suggestions, inferredAttachments, isSensitive := services.DetectMessageContent(trimmedBody)
|
||||
for _, inferred := range inferredAttachments {
|
||||
if hasAttachment(attachmentRows, inferred.Kind, inferred.URL) {
|
||||
continue
|
||||
@@ -684,13 +666,55 @@ func CreateConversationMessage(c *gin.Context) {
|
||||
previewJSON = string(raw)
|
||||
}
|
||||
attachmentRows = append(attachmentRows, models.MessageAttachment{
|
||||
MessageID: message.ID,
|
||||
Kind: normalizeAttachmentKind(inferred.Kind),
|
||||
URL: inferred.URL,
|
||||
Title: inferred.Title,
|
||||
PreviewJSON: previewJSON,
|
||||
})
|
||||
}
|
||||
|
||||
metadataMap := map[string]interface{}{}
|
||||
for k, v := range req.Metadata {
|
||||
metadataMap[k] = v
|
||||
}
|
||||
|
||||
storedBody := trimmedBody
|
||||
if isSensitive && (conv.Type == models.ConversationTypeDM || conv.Type == models.ConversationTypeSelf) && trimmedBody != "" {
|
||||
ciphertext, err := utils.Encrypt(trimmedBody)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encrypt sensitive message"})
|
||||
return
|
||||
}
|
||||
storedBody = maskSensitiveBody(trimmedBody)
|
||||
metadataMap["sensitive_payload"] = map[string]interface{}{
|
||||
"version": "v1",
|
||||
"ciphertext": ciphertext,
|
||||
"masked_body": storedBody,
|
||||
"scope": string(conv.Type),
|
||||
}
|
||||
}
|
||||
|
||||
metadataJSON := "{}"
|
||||
if len(metadataMap) > 0 {
|
||||
if raw, err := json.Marshal(metadataMap); err == nil {
|
||||
metadataJSON = string(raw)
|
||||
}
|
||||
}
|
||||
|
||||
message := models.Message{
|
||||
ConversationID: conversationID,
|
||||
SenderID: userID,
|
||||
Body: storedBody,
|
||||
MetadataJSON: metadataJSON,
|
||||
}
|
||||
if err := models.DB.Create(&message).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create message"})
|
||||
return
|
||||
}
|
||||
|
||||
for i := range attachmentRows {
|
||||
attachmentRows[i].MessageID = message.ID
|
||||
}
|
||||
if len(attachmentRows) > 0 {
|
||||
models.DB.Create(&attachmentRows)
|
||||
}
|
||||
@@ -1187,6 +1211,37 @@ func DismissMessageSuggestion(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"suggestion": suggestion})
|
||||
}
|
||||
|
||||
// RevealSensitiveMessage decrypts and returns sensitive message plaintext for authorized members.
|
||||
func RevealSensitiveMessage(c *gin.Context) {
|
||||
userID := getAuthUserID(c)
|
||||
messageID, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid message id"})
|
||||
return
|
||||
}
|
||||
|
||||
var msg models.Message
|
||||
if err := models.DB.First(&msg, messageID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"})
|
||||
return
|
||||
}
|
||||
if _, _, err := getConversationWithMembership(models.DB, msg.ConversationID, userID); err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
plaintext, ok := extractSensitivePlaintext(msg.MetadataJSON)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Sensitive payload not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message_id": msg.ID,
|
||||
"plaintext": plaintext,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPasswordVaultItems returns owned and explicitly shared vault items.
|
||||
func GetPasswordVaultItems(c *gin.Context) {
|
||||
userID := getAuthUserID(c)
|
||||
@@ -1760,11 +1815,15 @@ func applySuggestionAction(db *gorm.DB, userID uint, message *models.Message, su
|
||||
return gin.H{"deep_link": ref.DeepLink}, nil
|
||||
|
||||
case "move_to_password_vault":
|
||||
secretSource := message.Body
|
||||
if sensitivePlaintext, ok := extractSensitivePlaintext(message.MetadataJSON); ok {
|
||||
secretSource = sensitivePlaintext
|
||||
}
|
||||
label := "Imported from chat"
|
||||
if compact := compactMessageTitle(message.Body, 50); compact != "" {
|
||||
if compact := compactMessageTitle(secretSource, 50); compact != "" {
|
||||
label = compact
|
||||
}
|
||||
encryptedSecret, err := utils.Encrypt(message.Body)
|
||||
encryptedSecret, err := utils.Encrypt(secretSource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2026,6 +2085,70 @@ func hasAttachment(rows []models.MessageAttachment, kind, url string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func maskSensitiveBody(text string) string {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if trimmed == "" {
|
||||
return "[sensitive content hidden]"
|
||||
}
|
||||
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) == 0 {
|
||||
return "[sensitive content hidden]"
|
||||
}
|
||||
|
||||
maskedParts := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
runes := []rune(part)
|
||||
if len(runes) <= 2 {
|
||||
maskedParts = append(maskedParts, "**")
|
||||
continue
|
||||
}
|
||||
maskedParts = append(maskedParts, strings.Repeat("*", len(runes)))
|
||||
}
|
||||
return strings.Join(maskedParts, " ")
|
||||
}
|
||||
|
||||
func extractSensitivePlaintext(metadataJSON string) (string, bool) {
|
||||
payload := extractSensitivePayload(metadataJSON)
|
||||
if payload == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
ciphertext := asString(payload["ciphertext"])
|
||||
if ciphertext == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
plaintext, err := utils.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return plaintext, true
|
||||
}
|
||||
|
||||
func extractSensitivePayload(metadataJSON string) map[string]interface{} {
|
||||
trimmed := strings.TrimSpace(metadataJSON)
|
||||
if trimmed == "" || trimmed == "{}" {
|
||||
return nil
|
||||
}
|
||||
|
||||
metadata := map[string]interface{}{}
|
||||
if err := json.Unmarshal([]byte(trimmed), &metadata); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rawPayload, ok := metadata["sensitive_payload"]
|
||||
if !ok || rawPayload == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, ok := rawPayload.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func normalizeAttachmentKind(kind string) string {
|
||||
k := strings.ToLower(strings.TrimSpace(kind))
|
||||
switch k {
|
||||
|
||||
Reference in New Issue
Block a user