Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions frontend/src/components/app/dialogs/CreateChannelDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ export const CreateChannelDialog: React.FC<{
);

if (!response.ok) {
showNotification("Failed to add channel", "error", "Unable to create new channel");
return false;
const errorData = await response.json();
const errorMessage = errorData.error || errorData.message || "Unable to create new channel";
throw new Error(errorMessage);
}

showNotification(`Channel ${name} added successfully!`, "success", "Channel created");
setChannelOrProjectUpdateToggle(!channelsOrProjectsUpdateToggle);
return true;
} catch (err) {
showNotification("Failed to add channel", "error", "Unable to create new channel");
const errorMessage = err instanceof Error ? err.message : "Unable to create new channel";
showNotification("Failed to add channel", "error", errorMessage);
return false;
}
};
Expand All @@ -47,18 +49,22 @@ export const CreateChannelDialog: React.FC<{
return;
}

// Validate channel name: only allow alphanumeric, hyphens, and underscores (no spaces)
const validNamePattern = /^[a-zA-Z0-9_-]+$/;
if (!validNamePattern.test(channelName.trim())) {
setError("Channel name can only contain letters, numbers, hyphens (-) and underscores (_)");
return;
}

setIsLoading(true);
setError("");

try {
await addChannel(channelName.trim());
const success = await addChannel(channelName.trim());
if (success) {
setChannelName("");
setOpen(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create channel");
} finally {
setIsLoading(false);
}
setIsLoading(false);
};

return (
Expand Down
25 changes: 16 additions & 9 deletions frontend/src/components/app/dialogs/CreateProjectDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,17 @@ export const CreateProjectDialog: React.FC<{
});

if (!response.ok) {
showNotification("Failed to add project", "error", "Unable to create new project");
return false;
const errorData = await response.json();
const errorMessage = errorData.error || errorData.message || "Unable to create new project";
throw new Error(errorMessage);
}

showNotification(`${name} added successfully!`, "success", "Project created");
setChannelOrProjectUpdateToggle(!channelsOrProjectsUpdateToggle);
return true;
} catch (err) {
showNotification("Failed to add project", "error", "Unable to create new project");
const errorMessage = err instanceof Error ? err.message : "Unable to create new project";
showNotification("Failed to add project", "error", errorMessage);
return false;
}
};
Expand All @@ -70,18 +72,23 @@ export const CreateProjectDialog: React.FC<{
return;
}

// Validate project name: only allow alphanumeric, hyphens, and underscores (no spaces)
const validNamePattern = /^[a-zA-Z0-9_-]+$/;
if (!validNamePattern.test(projectName.trim())) {
setError("Project name can only contain letters, numbers, hyphens (-) and underscores (_)");
return;
}

setIsLoading(true);
setError("");

try {
await addProject(projectName.trim());
const success = await addProject(projectName.trim());
if (success) {
setProjectName("");
setProjectLogoBase64(null);
setOpen(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create project");
} finally {
setIsLoading(false);
}
setIsLoading(false);
};

return (
Expand Down
12 changes: 12 additions & 0 deletions internal/handler/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"net/http"
"regexp"

"trakrlog/internal/middleware"
"trakrlog/internal/model"
Expand Down Expand Up @@ -54,6 +55,17 @@ func (h *ChannelHandler) CreateChannel(ctx *gin.Context) {
return
}

// Validate channel name: only allow alphanumeric, hyphens, and underscores
validNamePattern := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
if !validNamePattern.MatchString(req.Name) {
ctx.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid channel name",
"error": "Channel name can only contain letters, numbers, hyphens (-) and underscores (_)",
})
return
}

channel, err := h.channelService.CreateChannel(ctx.Request.Context(), userID, projectID, req.Name)
if err != nil {
statusCode := http.StatusInternalServerError
Expand Down
12 changes: 4 additions & 8 deletions internal/handler/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ func (h *EventHandler) CreateEvent(ctx *gin.Context) {
}

var req struct {
ProjectID string `json:"project_id" binding:"required"`
ChannelID string `json:"channel_id" binding:"required"`
Project string `json:"project" binding:"required"`
Channel string `json:"channel" binding:"required"`
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Icon string `json:"icon"`
Expand All @@ -51,15 +51,11 @@ func (h *EventHandler) CreateEvent(ctx *gin.Context) {
return
}

event, err := h.eventService.CreateEvent(ctx.Request.Context(), userID, req.ProjectID, req.ChannelID, req.Title, req.Description, req.Icon, req.Tags)
event, err := h.eventService.CreateEvent(ctx.Request.Context(), userID, req.Project, req.Channel, req.Title, req.Description, req.Icon, req.Tags)
if err != nil {
statusCode := http.StatusInternalServerError
if err.Error() == "unauthorized: project does not belong to user" {
statusCode = http.StatusForbidden
} else if err.Error() == "channel not found" || err.Error() == "project not found" {
if err.Error() == "channel not found" || err.Error() == "project not found" {
statusCode = http.StatusNotFound
} else if err.Error() == "channel does not belong to the specified project" {
statusCode = http.StatusBadRequest
}

ctx.JSON(statusCode, gin.H{
Expand Down
12 changes: 12 additions & 0 deletions internal/handler/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"net/http"
"regexp"

"trakrlog/internal/middleware"
"trakrlog/internal/model"
Expand Down Expand Up @@ -46,6 +47,17 @@ func (h *ProjectHandler) CreateProject(ctx *gin.Context) {
return
}

// Validate project name: only allow alphanumeric, hyphens, and underscores
validNamePattern := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
if !validNamePattern.MatchString(req.Name) {
ctx.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid project name",
"error": "Project name can only contain letters, numbers, hyphens (-) and underscores (_)",
})
return
}

project, err := h.projectService.CreateProject(ctx.Request.Context(), userID, req.Name, req.LogoBase64)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
Expand Down
18 changes: 18 additions & 0 deletions internal/repository/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ func (r *channelRepository) FindByProjectID(ctx context.Context, projectID strin
return channels, nil
}

func (r *channelRepository) FindByProjectIDAndName(ctx context.Context, projectID, name string) (*model.Channel, error) {
projectObjectID, err := primitive.ObjectIDFromHex(projectID)
if err != nil {
return nil, err
}

var channel model.Channel
err = r.collection.FindOne(ctx, bson.M{
"project_id": projectObjectID,
"name": name,
}).Decode(&channel)
if err != nil {
return nil, err
}

return &channel, nil
}

func (r *channelRepository) Update(ctx context.Context, channel *model.Channel) error {
channel.UpdatedAt = time.Now()

Expand Down
18 changes: 18 additions & 0 deletions internal/repository/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ func (r *projectRepository) FindByUserID(ctx context.Context, userID string) ([]
return projects, nil
}

func (r *projectRepository) FindByUserIDAndName(ctx context.Context, userID, name string) (*model.Project, error) {
userObjectID, err := primitive.ObjectIDFromHex(userID)
if err != nil {
return nil, err
}

var project model.Project
err = r.collection.FindOne(ctx, bson.M{
"user_id": userObjectID,
"name": name,
}).Decode(&project)
if err != nil {
return nil, err
}

return &project, nil
}

func (r *projectRepository) Update(ctx context.Context, project *model.Project) error {
project.UpdatedAt = time.Now()

Expand Down
2 changes: 2 additions & 0 deletions internal/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type ProjectRepository interface {
Create(ctx context.Context, project *model.Project) error
FindByID(ctx context.Context, id string) (*model.Project, error)
FindByUserID(ctx context.Context, userID string) ([]*model.Project, error)
FindByUserIDAndName(ctx context.Context, userID, name string) (*model.Project, error)
Update(ctx context.Context, project *model.Project) error
Delete(ctx context.Context, id string) error
}
Expand All @@ -33,6 +34,7 @@ type ChannelRepository interface {
Create(ctx context.Context, channel *model.Channel) error
FindByID(ctx context.Context, id string) (*model.Channel, error)
FindByProjectID(ctx context.Context, projectID string) ([]*model.Channel, error)
FindByProjectIDAndName(ctx context.Context, projectID, name string) (*model.Channel, error)
Update(ctx context.Context, channel *model.Channel) error
Delete(ctx context.Context, id string) error
}
Expand Down
25 changes: 7 additions & 18 deletions internal/service/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (

"trakrlog/internal/model"
"trakrlog/internal/repository"

"go.mongodb.org/mongo-driver/bson/primitive"
)

type EventService struct {
Expand All @@ -25,37 +23,28 @@ func NewEventService(eventRepo repository.EventRepository, channelRepo repositor
}

// CreateEvent creates a new event in a channel
func (s *EventService) CreateEvent(ctx context.Context, userID, projectID, channelID, title, description, icon string, tags map[string]string) (*model.Event, error) {
func (s *EventService) CreateEvent(ctx context.Context, userID, projectName, channelName, title, description, icon string, tags map[string]string) (*model.Event, error) {
// Validation
if title == "" {
return nil, errors.New("event title required")
}

// Verify project exists and belongs to user
project, err := s.projectRepo.FindByID(ctx, projectID)
// Find project by user ID and name
project, err := s.projectRepo.FindByUserIDAndName(ctx, userID, projectName)
if err != nil {
return nil, errors.New("project not found")
}

if project.UserID.Hex() != userID {
return nil, errors.New("unauthorized: project does not belong to user")
}

// Verify channel exists and belongs to the project
channel, err := s.channelRepo.FindByID(ctx, channelID)
// Find channel by project ID and name
channel, err := s.channelRepo.FindByProjectIDAndName(ctx, project.ID.Hex(), channelName)
if err != nil {
return nil, errors.New("channel not found")
}

if channel.ProjectID.Hex() != projectID {
return nil, errors.New("channel does not belong to the specified project")
}

// Create event
channelOID, _ := primitive.ObjectIDFromHex(channelID)
event := &model.Event{
ChannelID: channelOID,
ProjectID: channel.ProjectID,
ChannelID: channel.ID,
ProjectID: project.ID,
Title: title,
Description: description,
Icon: icon,
Expand Down