Skip to content

Commit 751ab33

Browse files
Stefan ErnstStefan Ernst
authored andcommitted
Fixed timer assignment to users
1 parent a2a5f45 commit 751ab33

3 files changed

Lines changed: 61 additions & 18 deletions

File tree

internal/database/database.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ func (db *DB) Initialize() error {
234234
check: "SELECT COUNT(*) FROM pragma_table_info('workspaces') WHERE name='display_mode'",
235235
alter: "ALTER TABLE workspaces ADD COLUMN display_mode TEXT DEFAULT 'default'",
236236
},
237+
{
238+
check: "SELECT COUNT(*) FROM pragma_table_info('active_timers') WHERE name='user_id'",
239+
alter: "ALTER TABLE active_timers ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE",
240+
},
237241
}
238242

239243
for _, m := range migrations {

internal/database/schema/system.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ CREATE TABLE active_timers (
122122
workspace_id INTEGER NOT NULL,
123123
item_id INTEGER,
124124
project_id INTEGER NOT NULL,
125+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
125126
description TEXT NOT NULL,
126127
start_time_utc INTEGER NOT NULL,
127128
created_at INTEGER NOT NULL,
@@ -133,6 +134,7 @@ CREATE TABLE active_timers (
133134
CREATE INDEX IF NOT EXISTS idx_active_timers_workspace_id ON active_timers(workspace_id);
134135
CREATE INDEX IF NOT EXISTS idx_active_timers_item_id ON active_timers(item_id);
135136
CREATE INDEX IF NOT EXISTS idx_active_timers_project_id ON active_timers(project_id);
137+
CREATE INDEX IF NOT EXISTS idx_active_timers_user_id ON active_timers(user_id);
136138

137139
-- Themes table
138140
CREATE TABLE IF NOT EXISTS themes (

internal/handlers/active_timers.go

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,26 @@ import (
99

1010
"windshift/internal/database"
1111
"windshift/internal/models"
12+
"windshift/internal/services"
1213
"windshift/internal/utils"
1314
)
1415

1516
type ActiveTimerHandler struct {
16-
db database.Database
17+
db database.Database
18+
timePermissionService *services.TimePermissionService
1719
}
1820

19-
func NewActiveTimerHandler(db database.Database) *ActiveTimerHandler {
20-
return &ActiveTimerHandler{db: db}
21+
func NewActiveTimerHandler(db database.Database, timePermissionService *services.TimePermissionService) *ActiveTimerHandler {
22+
return &ActiveTimerHandler{db: db, timePermissionService: timePermissionService}
2123
}
2224

2325
// StartTimer starts a new active timer
2426
func (h *ActiveTimerHandler) StartTimer(w http.ResponseWriter, r *http.Request) {
27+
user, ok := RequireAuth(w, r)
28+
if !ok {
29+
return
30+
}
31+
2532
var req struct {
2633
WorkspaceID int `json:"workspace_id"`
2734
ItemID *int `json:"item_id,omitempty"`
@@ -48,6 +55,19 @@ func (h *ActiveTimerHandler) StartTimer(w http.ResponseWriter, r *http.Request)
4855
return
4956
}
5057

58+
// Check booking permission on project
59+
if h.timePermissionService != nil {
60+
canBook, err := h.timePermissionService.CanBookTimeOnProject(user.ID, req.ProjectID)
61+
if err != nil {
62+
respondInternalError(w, r, err)
63+
return
64+
}
65+
if !canBook {
66+
respondForbidden(w, r)
67+
return
68+
}
69+
}
70+
5171
// Validate project exists and is Active
5272
var projectStatus string
5373
err := h.db.QueryRow("SELECT status FROM time_projects WHERE id = ?", req.ProjectID).Scan(&projectStatus)
@@ -64,9 +84,9 @@ func (h *ActiveTimerHandler) StartTimer(w http.ResponseWriter, r *http.Request)
6484
return
6585
}
6686

67-
// Check if there's already an active timer (only one timer allowed at a time)
87+
// Check if there's already an active timer for this user (only one timer per user)
6888
var existingID int
69-
err = h.db.QueryRow("SELECT id FROM active_timers LIMIT 1").Scan(&existingID)
89+
err = h.db.QueryRow("SELECT id FROM active_timers WHERE user_id = ? LIMIT 1", user.ID).Scan(&existingID)
7090
if err != sql.ErrNoRows {
7191
if err != nil {
7292
respondInternalError(w, r, err)
@@ -82,9 +102,9 @@ func (h *ActiveTimerHandler) StartTimer(w http.ResponseWriter, r *http.Request)
82102

83103
var id int64
84104
err = h.db.QueryRow(`
85-
INSERT INTO active_timers (workspace_id, item_id, project_id, description, start_time_utc, created_at)
86-
VALUES (?, ?, ?, ?, ?, ?) RETURNING id
87-
`, req.WorkspaceID, req.ItemID, req.ProjectID, req.Description, now, now).Scan(&id)
105+
INSERT INTO active_timers (workspace_id, item_id, project_id, user_id, description, start_time_utc, created_at)
106+
VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id
107+
`, req.WorkspaceID, req.ItemID, req.ProjectID, user.ID, req.Description, now, now).Scan(&id)
88108
if err != nil {
89109
respondInternalError(w, r, err)
90110
return
@@ -101,14 +121,19 @@ func (h *ActiveTimerHandler) StartTimer(w http.ResponseWriter, r *http.Request)
101121
_ = json.NewEncoder(w).Encode(timer)
102122
}
103123

104-
// GetActiveTimer gets the currently active timer
124+
// GetActiveTimer gets the currently active timer for the authenticated user
105125
func (h *ActiveTimerHandler) GetActiveTimer(w http.ResponseWriter, r *http.Request) {
126+
user, ok := RequireAuth(w, r)
127+
if !ok {
128+
return
129+
}
130+
106131
var timer *models.ActiveTimer
107132

108133
//nolint:misspell // customer_organisations is a database table name
109134
query := `
110-
SELECT
111-
at.id, at.workspace_id, at.item_id, at.project_id, at.description,
135+
SELECT
136+
at.id, at.workspace_id, at.item_id, at.project_id, at.user_id, at.description,
112137
at.start_time_utc, at.created_at,
113138
tp.name as project_name,
114139
tc.name as customer_name,
@@ -120,17 +145,18 @@ func (h *ActiveTimerHandler) GetActiveTimer(w http.ResponseWriter, r *http.Reque
120145
LEFT JOIN customer_organisations tc ON tp.customer_id = tc.id
121146
LEFT JOIN items i ON at.item_id = i.id
122147
LEFT JOIN workspaces ws ON at.workspace_id = ws.id
148+
WHERE at.user_id = ?
123149
LIMIT 1
124150
`
125151

126-
row := h.db.QueryRow(query)
152+
row := h.db.QueryRow(query, user.ID)
127153
timer = &models.ActiveTimer{}
128154

129155
// Use sql.NullString for nullable joined fields
130156
var projectName, customerName, itemTitle, workspaceName, workspaceKey sql.NullString
131157

132158
err := row.Scan(
133-
&timer.ID, &timer.WorkspaceID, &timer.ItemID, &timer.ProjectID, &timer.Description,
159+
&timer.ID, &timer.WorkspaceID, &timer.ItemID, &timer.ProjectID, &timer.UserID, &timer.Description,
134160
&timer.StartTimeUTC, &timer.CreatedAt,
135161
&projectName, &customerName, &itemTitle, &workspaceName, &workspaceKey,
136162
)
@@ -157,6 +183,11 @@ func (h *ActiveTimerHandler) GetActiveTimer(w http.ResponseWriter, r *http.Reque
157183

158184
// StopTimer stops the active timer and creates a worklog entry
159185
func (h *ActiveTimerHandler) StopTimer(w http.ResponseWriter, r *http.Request) {
186+
user, ok := RequireAuth(w, r)
187+
if !ok {
188+
return
189+
}
190+
160191
timerIDStr := r.PathValue("id")
161192
timerID, err := strconv.Atoi(timerIDStr)
162193
if err != nil {
@@ -175,6 +206,12 @@ func (h *ActiveTimerHandler) StopTimer(w http.ResponseWriter, r *http.Request) {
175206
return
176207
}
177208

209+
// Verify ownership
210+
if timer.UserID != user.ID {
211+
respondForbidden(w, r)
212+
return
213+
}
214+
178215
// Calculate duration
179216
endTimeUTC := time.Now().UTC().Unix()
180217
durationSeconds := endTimeUTC - timer.StartTimeUTC
@@ -189,8 +226,8 @@ func (h *ActiveTimerHandler) StopTimer(w http.ResponseWriter, r *http.Request) {
189226

190227
// Create worklog entry
191228
worklogQuery := `
192-
INSERT INTO time_worklogs (project_id, customer_id, item_id, description, date, start_time, end_time, duration_minutes, created_at, updated_at)
193-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
229+
INSERT INTO time_worklogs (project_id, customer_id, user_id, item_id, description, date, start_time, end_time, duration_minutes, created_at, updated_at)
230+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
194231
`
195232

196233
// Convert timestamps to integers for the database
@@ -200,7 +237,7 @@ func (h *ActiveTimerHandler) StopTimer(w http.ResponseWriter, r *http.Request) {
200237

201238
nowUnix := time.Now().UTC().Unix()
202239
_, err = h.db.ExecWrite(worklogQuery,
203-
timer.ProjectID, customerID, timer.ItemID, timer.Description,
240+
timer.ProjectID, customerID, user.ID, timer.ItemID, timer.Description,
204241
dateInt, int(timer.StartTimeUTC), int(endTimeUTC),
205242
durationMinutes, nowUnix, nowUnix)
206243
if err != nil {
@@ -245,7 +282,7 @@ func (h *ActiveTimerHandler) getActiveTimerByID(id int) (*models.ActiveTimer, er
245282
//nolint:misspell // database uses British spelling (customer_organisations)
246283
query := `
247284
SELECT
248-
at.id, at.workspace_id, at.item_id, at.project_id, at.description,
285+
at.id, at.workspace_id, at.item_id, at.project_id, at.user_id, at.description,
249286
at.start_time_utc, at.created_at,
250287
tp.name as project_name,
251288
tc.name as customer_name,
@@ -266,7 +303,7 @@ func (h *ActiveTimerHandler) getActiveTimerByID(id int) (*models.ActiveTimer, er
266303
var projectName, customerName, itemTitle, workspaceName, workspaceKey sql.NullString
267304

268305
err := h.db.QueryRow(query, id).Scan(
269-
&timer.ID, &timer.WorkspaceID, &timer.ItemID, &timer.ProjectID, &timer.Description,
306+
&timer.ID, &timer.WorkspaceID, &timer.ItemID, &timer.ProjectID, &timer.UserID, &timer.Description,
270307
&timer.StartTimeUTC, &timer.CreatedAt,
271308
&projectName, &customerName, &itemTitle, &workspaceName, &workspaceKey,
272309
)

0 commit comments

Comments
 (0)