-
Notifications
You must be signed in to change notification settings - Fork 307
Development #370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Development #370
Changes from all commits
464b556
098b410
b1e3547
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,7 +90,7 @@ export default class ReportingOverviewController extends ReportingOverviewBase { | |
|
|
||
| const teamFilterClause = `p.team_id = $1`; | ||
|
|
||
| const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, ""); | ||
| const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField as string, sortOrder, "", "", "", archivedClause, teamFilterClause, ""); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find toPaginationOptions definition
find . -type f -name "*.ts" | xargs grep -l "toPaginationOptions" | head -20Repository: Worklenz/worklenz Length of output: 1389 🏁 Script executed: #!/bin/bash
# Locate and inspect toPaginationOptions implementation
rg -n --type=ts "toPaginationOptions" -A 15 worklenz-backend/src/controllers/Repository: Worklenz/worklenz Length of output: 50373 🏁 Script executed: #!/bin/bash
# Get the full toPaginationOptions function from worklenz-controller-base.ts
wc -l worklenz-backend/src/controllers/worklenz-controller-base.tsRepository: Worklenz/worklenz Length of output: 124 🏁 Script executed: #!/bin/bash
# Read the toPaginationOptions function implementation
sed -n '30,100p' worklenz-backend/src/controllers/worklenz-controller-base.tsRepository: Worklenz/worklenz Length of output: 2264 🏁 Script executed: #!/bin/bash
# Find the specific line 93 in reporting-overview-controller.ts and surrounding context
sed -n '80,100p' worklenz-backend/src/controllers/reporting/overview/reporting-overview-controller.tsRepository: Worklenz/worklenz Length of output: 1158 🏁 Script executed: #!/bin/bash
# Find getProjectsByTeam implementation and see how it uses sortField
rg -n "getProjectsByTeam" worklenz-backend/src/controllers/ -A 5 | head -40Repository: Worklenz/worklenz Length of output: 4508 🏁 Script executed: #!/bin/bash
# Check the ReportingControllerBase class
find . -type f -name "*reporting*controller*base*" -o -name "*base*controller*" | grep reportingRepository: Worklenz/worklenz Length of output: 222 🏁 Script executed: #!/bin/bash
# Read the getProjectsByTeam function in reporting-controller-base.ts
sed -n '230,280p' worklenz-backend/src/controllers/reporting/reporting-controller-base.tsRepository: Worklenz/worklenz Length of output: 2140 🏁 Script executed: #!/bin/bash
# Read more of the getProjectsByTeam function to find the ORDER BY clause
sed -n '230,330p' worklenz-backend/src/controllers/reporting/reporting-controller-base.tsRepository: Worklenz/worklenz Length of output: 5743 🏁 Script executed: #!/bin/bash
# Search for ORDER BY in the getProjectsByTeam function
sed -n '230,400p' worklenz-backend/src/controllers/reporting/reporting-controller-base.ts | grep -n "ORDER BY"Repository: Worklenz/worklenz Length of output: 245 🏁 Script executed: #!/bin/bash
# Get the complete file size and read the end portion
wc -l worklenz-backend/src/controllers/reporting/reporting-controller-base.ts
tail -200 worklenz-backend/src/controllers/reporting/reporting-controller-base.ts | head -100Repository: Worklenz/worklenz Length of output: 6041 🏁 Script executed: #!/bin/bash
# Read lines around 370-380 where ORDER BY ${sortField} is used
sed -n '360,390p' worklenz-backend/src/controllers/reporting/reporting-controller-base.tsRepository: Worklenz/worklenz Length of output: 2383 🏁 Script executed: #!/bin/bash
# Check the return type annotation of toPaginationOptions
sed -n '30,78p' worklenz-backend/src/controllers/worklenz-controller-base.ts | tail -5Repository: Worklenz/worklenz Length of output: 176 🏁 Script executed: #!/bin/bash
# Look for where sortField is used in other controllers after toPaginationOptions
rg "toPaginationOptions.*sortField" worklenz-backend/src/controllers/ -A 3 | head -50Repository: Worklenz/worklenz Length of output: 43 Verify that The Ensure 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| for (const project of result.projects) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,7 +24,7 @@ export default class ReportingOverviewExportController extends ReportingOverview | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const teamFilterClause = `p.team_id = $1`; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, ""); | ||||||||||||||||||||||||||
| const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField as string, sortOrder, "", "", "", archivedClause, teamFilterClause, ""); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for (const project of result.projects) { | ||||||||||||||||||||||||||
| project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA; | ||||||||||||||||||||||||||
|
|
@@ -416,7 +416,7 @@ export default class ReportingOverviewExportController extends ReportingOverview | |||||||||||||||||||||||||
| const teamMemberName = (req.query.team_member_name as string)?.trim() || null; | ||||||||||||||||||||||||||
| const teamName = (req.query.team_name as string)?.trim() || ""; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const { duration, date_range, only_single_member, archived} = req.query; | ||||||||||||||||||||||||||
| const { duration, date_range, only_single_member, archived } = req.query; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const includeArchived = req.query.archived === "true"; | ||||||||||||||||||||||||||
|
Comment on lines
+419
to
421
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Either remove 🔧 Proposed fix- const { duration, date_range, only_single_member, archived } = req.query;
+ const { duration, date_range, only_single_member } = req.query;
const includeArchived = req.query.archived === "true";Or, alternatively, use the destructured variable and remove the redundant const { duration, date_range, only_single_member, archived } = req.query;
- const includeArchived = req.query.archived === "true";
+ const includeArchived = archived === "true";📝 Committable suggestion
Suggested change
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -506,7 +506,7 @@ export default class ReportingOverviewExportController extends ReportingOverview | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const includeArchived = req.query.archived === "true"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, "false", "", [], includeArchived, req.user?.id as string); | ||||||||||||||||||||||||||
| const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, "false", "", [], includeArchived, req.user?.id as string); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // excel file | ||||||||||||||||||||||||||
| const exportDate = moment().format("MMM-DD-YYYY"); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,7 +111,7 @@ export default abstract class ReportingControllerBase extends WorklenzController | |
|
|
||
| protected static buildBillableQuery(selectedStatuses: { billable: boolean; nonBillable: boolean }): string { | ||
| const { billable, nonBillable } = selectedStatuses; | ||
|
|
||
| if (billable && nonBillable) { | ||
| // Both are enabled, no need to filter | ||
| return ""; | ||
|
|
@@ -121,7 +121,7 @@ export default abstract class ReportingControllerBase extends WorklenzController | |
| } else if (nonBillable) { | ||
| // Only non-billable is enabled | ||
| return " AND tasks.billable IS FALSE"; | ||
| } | ||
| } | ||
|
|
||
| return ""; | ||
| } | ||
|
|
@@ -166,6 +166,67 @@ export default abstract class ReportingControllerBase extends WorklenzController | |
| } | ||
|
|
||
|
|
||
| /** | ||
| * Build project filter clause for Team Leads | ||
| * Team Leads can only see projects they are assigned to as project managers | ||
| */ | ||
| public static async buildProjectFilterForTeamLead(req: IWorkLenzRequest): Promise<string> { | ||
| // Check if user is a Team Lead (not Admin or Owner) | ||
| const userId = req.user?.id; | ||
| const teamId = req.user?.team_id; | ||
|
|
||
| if (!userId || !teamId) return ""; | ||
|
|
||
| // Check user's role | ||
| const roleQuery = ` | ||
| SELECT r.key | ||
| FROM roles r | ||
| JOIN team_members tm ON tm.role_id = r.id | ||
| WHERE tm.user_id = $1 AND tm.team_id = $2 | ||
| `; | ||
| const roleResult = await db.query(roleQuery, [userId, teamId]); | ||
|
|
||
| if (roleResult.rows.length === 0) return ""; | ||
|
|
||
| const roleKey = roleResult.rows[0].key; | ||
|
|
||
| // Only apply filter for Team Leads | ||
| if (roleKey === 'TEAM_LEAD') { | ||
| // Team Leads can only see projects they manage | ||
| return `AND p.id IN ( | ||
| SELECT pm.project_id | ||
| FROM project_members pm | ||
| WHERE pm.team_member_id IN ( | ||
| SELECT id FROM team_members WHERE user_id = '${userId}' | ||
| ) | ||
| AND pm.project_access_level_id = ( | ||
| SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER' | ||
| ) | ||
| )`; | ||
| } | ||
|
|
||
| // Admins and Owners can see all projects | ||
| return ""; | ||
| } | ||
|
Comment on lines
+169
to
+210
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SQL injection: Line 200 embeds Return both a clause with placeholders and the corresponding params, similar to Proposed fix- public static async buildProjectFilterForTeamLead(req: IWorkLenzRequest): Promise<string> {
+ public static async buildProjectFilterForTeamLead(req: IWorkLenzRequest, paramOffset: number): Promise<{ clause: string; params: any[] }> {
const userId = req.user?.id;
const teamId = req.user?.team_id;
- if (!userId || !teamId) return "";
+ if (!userId || !teamId) return { clause: "", params: [] };
const roleQuery = `
SELECT r.key
FROM roles r
JOIN team_members tm ON tm.role_id = r.id
WHERE tm.user_id = $1 AND tm.team_id = $2
`;
const roleResult = await db.query(roleQuery, [userId, teamId]);
- if (roleResult.rows.length === 0) return "";
+ if (roleResult.rows.length === 0) return { clause: "", params: [] };
const roleKey = roleResult.rows[0].key;
if (roleKey === 'TEAM_LEAD') {
- return `AND p.id IN (
+ return {
+ clause: `AND p.id IN (
SELECT pm.project_id
FROM project_members pm
WHERE pm.team_member_id IN (
- SELECT id FROM team_members WHERE user_id = '${userId}'
+ SELECT id FROM team_members WHERE user_id = $${paramOffset}
)
AND pm.project_access_level_id = (
SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'
)
- )`;
+ )`,
+ params: [userId]
+ };
}
- return "";
+ return { clause: "", params: [] };
}Callers would then spread the returned params into their 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Get project IDs that a Team Lead is assigned to | ||
| */ | ||
| public static async getTeamLeadProjects(userId: string, teamId: string): Promise<string[]> { | ||
| const q = ` | ||
| SELECT DISTINCT pm.project_id | ||
| FROM project_members pm | ||
| JOIN team_members tm ON pm.team_member_id = tm.id | ||
| WHERE tm.user_id = $1 | ||
| AND tm.team_id = $2 | ||
| AND pm.project_access_level_id = ( | ||
| SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER' | ||
| ) | ||
| `; | ||
| const result = await db.query(q, [userId, teamId]); | ||
| return result.rows.map(row => row.project_id); | ||
| } | ||
|
|
||
| public static async getProjectsByTeam( | ||
| teamId: string, | ||
| size: string | number | null, | ||
|
|
@@ -178,7 +239,8 @@ export default abstract class ReportingControllerBase extends WorklenzController | |
| categoryClause: string, | ||
| archivedClause = "", | ||
| teamFilterClause: string, | ||
| projectManagersClause: string) { | ||
| projectManagersClause: string, | ||
| filterParams: any[] = []) { | ||
|
|
||
| const q = `SELECT COUNT(*) AS total, | ||
| (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON) | ||
|
|
@@ -315,7 +377,9 @@ export default abstract class ReportingControllerBase extends WorklenzController | |
| LEFT JOIN project_categories pc ON pc.id = p.category_id | ||
| LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id | ||
| WHERE ${teamFilterClause} ${searchQuery} ${healthClause} ${statusClause} ${categoryClause} ${projectManagersClause} ${archivedClause};`; | ||
| const result = await db.query(q, [teamId, size, offset]); | ||
| // Combine all parameters: teamId, size, offset, then filter params | ||
| const queryParams = [teamId, size, offset, ...filterParams]; | ||
| const result = await db.query(q, queryParams); | ||
| const [data] = result.rows; | ||
|
|
||
| for (const project of data.projects) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Worklenz/worklenz
Length of output: 518
🏁 Script executed:
Repository: Worklenz/worklenz
Length of output: 1356
🏁 Script executed:
Repository: Worklenz/worklenz
Length of output: 2047
🌐 Web query:
moment.js isAfter(undefined) behavior current version💡 Result:
In the current Moment.js latest release (2.30.1),
moment#isAfter(undefined)behaves the same as callingisAfter()with no argument: theundefinedgets parsed into a Moment representing “now”, and the comparison is done against that current time. [1][2]Concretely:
someMoment.isAfter(undefined)⇒ comparessomeMomentto now (at the timeisAfterruns). [1][2]moment().isAfter(undefined)⇒ typicallyfalse, because it compares “now” (created slightly earlier) to a new “now” created insideisAfter. [2]This happens because
isAfterinternally turns non-Moment inputs into a Moment viacreateLocal(input)(andcreateLocal(undefined)becomes “now”). [2]Sources:
[1] Moment.js site showing current version 2.30.1
[2] Moment.js docs +
isAfterimplementation (parsing input viacreateLocal(input);moment(undefined)treated asmoment())dateRange.endDateis a non-existent property — should bedateRange.end_date.Line 226 uses
dateRange.endDate(camelCase), which isundefinedat runtime. Every other access to this object uses the snake_case propertyend_date(lines 214–215, 227, and throughout the file). As a result,moment(logsRange.max_date).isAfter(undefined)never evaluates as intended: in moment.js,isAfter(undefined)treatsundefinedas the current time, so the condition becomes "is logsRange.max_date after now?" — which is false for typical past log dates. The log-range max date is silently never applied todateRange.end_date.🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents