@@ -9,19 +9,26 @@ import (
99
1010 "windshift/internal/database"
1111 "windshift/internal/models"
12+ "windshift/internal/services"
1213 "windshift/internal/utils"
1314)
1415
1516type 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
2426func (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
105125func (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
159185func (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