Skip to content

SS1214#104

Merged
cloudysatrn merged 5 commits into
mainfrom
saturnsupdate
Dec 14, 2025
Merged

SS1214#104
cloudysatrn merged 5 commits into
mainfrom
saturnsupdate

Conversation

@cloudysatrn

@cloudysatrn cloudysatrn commented Dec 14, 2025

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Birthday webhook notifications for Discord
    • Idle time tracking toggle for activity monitoring
    • Activity quotas system with session type filtering
    • Alliance visit counting and tracking
    • Leaderboard display modes (list and podium styles)
    • Role color customization
    • Participant management for alliance visits
    • Editable saved views
  • Enhancements

    • Profile information tab with timezone, department, and line manager fields
    • Session pattern editing with single/future/all update scopes
    • Activity dashboard with tabbed interface
    • Automatic timezone detection and sync
    • Day/night themed profile header
  • Bug Fixes

    • Host display now shows "No Host" instead of "Unknown"

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 14, 2025

Copy link
Copy Markdown
Contributor

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Introduces comprehensive activity tracking enhancements: timezone auto-detection and storage, birthday webhook notifications with cron execution, idle-time configuration, activity dashboard refactoring with tabbed UI components, profile information editing with timezone and member linking, session pattern editing workflow, leaderboard style selection (list/podium), quotas page redesign, and supporting API endpoints and database schema extensions.

Changes

Cohort / File(s) Summary
Birthday Webhook System
pages/api/cron/birthdays.ts, pages/api/workspace/[id]/settings/general/birthdays/hook.ts, pages/api/workspace/[id]/settings/general/birthdays/test.ts, components/settings/general/bhooks.tsx, components/settings/instance/index.ts
New birthday notification webhook management: stores enabled flag and Discord webhook URL, tests webhook connectivity, and cron job processes daily birthdays and posts embeds to configured webhooks.
Timezone Detection & Storage
pages/api/workspace/[id]/timezone.ts, pages/api/workspace/[id]/birthday.ts, components/bdayprompt.tsx, pages/workspace/[id]/index.tsx
Detects client timezone on workspace home load, persists via new timezone API, propagates to birthday payloads, and displays in profile with day/night indicator.
Activity Dashboard Refactoring
components/profile/activity.tsx, components/profile/activityoverview.tsx, components/profile/sessions.tsx, components/profile/quotas.tsx, components/profile/activitysubsection.tsx
Refactors activity display from single-column to tabbed UI (Activity, Sessions, Quotas); introduces dedicated components for each section with timeline, summary metrics, and progress bars.
Profile Information Tab
components/profile/information.tsx, pages/api/workspace/[id]/profile/[uid]/member-info.ts
New editable profile information panel supporting department, line manager selection, timezone, Discord ID, and birthday fields; stores via new member-info API.
Session Pattern Editing
components/sessionpatterns.tsx, pages/api/workspace/[id]/sessions/[sid]/update-pattern.ts, pages/workspace/[id]/sessions.tsx, pages/workspace/[id]/sessions/edit/[sid].tsx
Adds dialog-driven pattern-aware session updates: single session, future recurrences, or entire pattern via scoped update API.
Leaderboard & Quotas UI
components/settings/general/leaderboard.tsx, components/sidebar.tsx, pages/workspace/[id]/activity/index.tsx, pages/workspace/[id]/quotas.tsx
Introduces leaderboard style selection (list/podium), replaces leaderboard sidebar toggle with quotas link, adds new quotas management page with create/delete and progress tracking.
Activity Configuration
pages/api/workspace/[id]/settings/activity/setIdleTime.ts, pages/api/workspace/[id]/settings/activity/getConfig.ts, components/settings/general/activity.tsx
Adds idleTimeEnabled toggle and leaderboard style settings; extends config fetch to return idle time state.
Activity Metrics & Quotas APIs
pages/api/workspace/[id]/activity/stats.ts, pages/api/workspace/[id]/activity/users.ts, pages/api/workspace/[id]/activity/reset.ts, pages/api/workspace/[id]/activity/quotas/new.ts, pages/api/activity/session.ts
Extends activity data collection with idle time, alliance visits, per-type session logging, and new quota types (sessions_logged, alliance_visits); refactors metrics aggregation logic.
Profile & Views Data
pages/workspace/[id]/profile/[uid].tsx, pages/workspace/[id]/views.tsx, pages/api/workspace/[id]/profile/[uid].ts
Expands profile server props with allianceVisits, workspaceMember metadata, line manager, and all members; adds alliance visits column and idle-time configuration to activity views.
Session & Permission Updates
pages/api/workspace/[id]/sessions/[sid]/claim-role.ts, pages/api/workspace/[id]/settings/permissions/roles.tsx, pages/api/workspace/[id]/settings/roles/[roleid]/update.ts, pages/api/workspace/[id]/settings/general/session-colors.ts, pages/workspace/[id]/policies.tsx, pages/workspace/[id]/wall.tsx
Refactors permission checks to use admin-or-permission model; adds role color editing; extends role filtering and wall post permissions.
Alliance & Home Updates
pages/api/workspace/[id]/allies/[aid]/visits/index.ts, pages/api/workspace/[id]/allies/[aid]/visits/[vid].ts, pages/api/workspace/[id]/home/upcoming.ts, pages/workspace/[id]/alliances/manage/[aid].tsx
Adds participant tracking to alliance visits; refactors birthday lookup to query nested user fields; extends UI for participant display and management.
Database & Build
prisma/schema.prisma, prisma/migrations/20251011203302_hw_25/migration.sql, prisma/migrations/20251210214146_107/migration.sql, package.json
Adds schema fields for timezone, department, line manager, role color, quota description/sessionType, ally visit participants; removes joinDate default; updates dependencies (Next.js, moment-timezone).
Settings & Utilities
components/settings/instance/external.ts, components/sticky-note-announcement.tsx, utils/permissionsManager.ts, utils/randomText.ts, pages/api/admin/instance-config.ts, pages/api/createws.ts, components/home/sessions.tsx, components/settings/general/color.tsx, pages/workspace/[id]/settings.tsx
Minor text/doc updates, removes console.log, fixes external services link, updates announcement version, refines color picker dependency, and initializes workspaceMember on creation.

Sequence Diagrams

sequenceDiagram
    actor User
    participant Frontend as Client/UI
    participant API as API Handler
    participant Discord as Discord Webhook
    participant Database as Database
    
    User->>Frontend: Test Birthday Webhook
    Frontend->>API: POST /settings/general/birthdays/test<br/>(webhookUrl)
    activate API
    API->>API: Validate webhook URL format
    API->>Discord: POST embed payload
    activate Discord
    Discord-->>API: 200 OK / Error
    deactivate Discord
    alt Success
        API-->>Frontend: { success: true }
        Frontend-->>User: Toast: "Test sent successfully"
    else Failed
        API-->>Frontend: { error: "..." }
        Frontend-->>User: Toast: "Test failed"
    end
    deactivate API
Loading
sequenceDiagram
    actor User
    participant Frontend as Edit Session UI
    participant Dialog as PatternEditDialog
    participant API as API Handler
    participant Database as Database
    
    User->>Frontend: Click Edit on Recurring Session
    alt Session has scheduleId (pattern)
        Frontend->>Dialog: Open PatternEditDialog
        activate Dialog
        User->>Dialog: Select Scope<br/>(single/future/all)
        Dialog-->>Frontend: onConfirm(scope)
        deactivate Dialog
        Frontend->>API: PUT /sessions/[sid]/update-pattern<br/>(scope, changes)
        activate API
        alt Scope: single
            API->>Database: Update this session only
        else Scope: future
            API->>Database: Update this + future sessions<br/>in same pattern
        else Scope: all
            API->>Database: Update all sessions<br/>in pattern
        end
        Database-->>API: Updated
        API-->>Frontend: { updatedCount, success }
        deactivate API
        Frontend-->>User: Toast: "Updated (scope)"
    else No pattern
        Frontend->>API: Standard session edit
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • pages/api/workspace/[id]/activity/reset.ts — Dense quota progress calculation with multiple quota types (sessions_hosted, sessions_logged, alliance_visits) and per-type session filtering logic; sessionType matching and percentage clamping.
  • pages/api/workspace/[id]/activity/users.ts — Major refactor of activity aggregation; new idle-time handling toggle, topStaff construction with deduplication and deterministic sorting, combined minutes calculation across idle time and adjustments.
  • pages/api/workspace/[id]/sessions/[sid]/update-pattern.ts — New API for pattern-scoped session updates with three distinct branches (single/future/all) and complex recurrence logic; session log generation.
  • components/profile/activity.tsx — Significant refactoring from monolithic layout to tabbed UI with new props interface; removed/simplified internal state for charts and modals; propagates data to new child components.
  • pages/workspace/[id]/views.tsx — Extended data aggregation with session logging by type, alliance visits, and per-type session counts; new saved-view edit workflow with unsaved-change detection.
  • pages/workspace/[id]/activity/index.tsx — Leaderboard rendering with dual display modes (list/podium), new podium visual helpers, and expanded quota type handling; large conditional blocks for UI state.
  • pages/workspace/[id]/sessions/edit/[sid].tsx — New scope-aware update branching; deletion logic now respects pattern scope; pattern edit mode UI indicator.

Possibly related PRs

  • PlanetaryOrbit/orbit#95 — Related sidebar feature-flag and navigation item refactoring; both modify leaderboard/quotas menu item display and feature-toggle logic in sidebar.tsx.

Suggested labels

enhancement

Suggested reviewers

  • gml-pz
  • brennanpeters

Poem

🐰 Hop along, the features bloom,
Timezones tick in every room,
Birthdays chime and patterns bend,
Quotas rise to meet the end. ✨🎂

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'SS1214' is vague and does not convey meaningful information about the changeset, using a non-descriptive identifier instead of describing the actual changes. Replace with a descriptive title that summarizes the main change, such as 'Add birthday notifications, quotas refactor, and profile enhancements' or identify the primary objective of this substantial changeset.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread components/bdayprompt.tsx
try {
if (skip) {
await axios.post(`/api/workspace/${workspaceId}/birthday`, { day: 0, month: 0 });
await axios.post(`/api/workspace/${workspaceId}/birthday`, { day: 0, month: 0, timezone });

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix this vulnerability, we should validate the workspaceId before using it to construct API request URLs. Specifically, we should:

  • Accept only workspaceId values that conform to an expected pattern and are valid for the use-case. If workspace IDs are numeric, ensure they are numbers. If they are UUIDs or have a fixed format, strictly check that the input matches.
  • If workspaceId is expected to be a numeric ID, coerce to Number and ensure it is a finite positive integer (not NaN, Infinity, or negative), otherwise bail out.
  • If alphanumeric or UUID, use a regex to ensure the value matches the expected format.
  • Reject (do not make the request, or show an error/return null) if the value does not pass validation, preventing attempts to forge paths, perform path traversal, or inject unexpected values. That way, even if the user provides a malicious id, no sensitive or unexpected backend route is accessed via the frontend.

The changes are to be implemented in components/bdayprompt.tsx:

  • Before using workspaceId in any backend API call (in the useEffect and save methods), validate its value per above.
  • Only proceed with fetch/post requests if the value is valid; otherwise, skip with a comment or log.

We do not modify backend code, nor do we change the way props are set from layouts/workspace.tsx—we simply validate at the point of use.


Suggested changeset 1
components/bdayprompt.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/bdayprompt.tsx b/components/bdayprompt.tsx
--- a/components/bdayprompt.tsx
+++ b/components/bdayprompt.tsx
@@ -15,13 +15,27 @@
   const [loading, setLoading] = useState(false);
   const [initialLoaded, setInitialLoaded] = useState(false);
 
+  // Helper for validating workspaceId: allow only numbers or UUIDs. Adjust pattern as needed.
+  const isValidWorkspaceId = (id: string | number): boolean => {
+    if (typeof id === "number") {
+      return Number.isInteger(id) && id > 0;
+    }
+    if (typeof id === "string") {
+      // Accept only positive integer string or canonical UUID; adjust as needed for your system
+      const intPattern = /^[1-9][0-9]*$/;
+      const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+      return intPattern.test(id) || uuidPattern.test(id);
+    }
+    return false;
+  };
+
   useEffect(() => {
     const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
     setTimezone(detectedTimezone);
   }, []);
 
   useEffect(() => {
-    if (!workspaceId) return;
+    if (!workspaceId || !isValidWorkspaceId(workspaceId)) return;
     let cancelled = false;
     (async () => {
       try {
@@ -58,6 +66,11 @@
   const save = async (skip = false) => {
     setLoading(true);
     try {
+      if (!isValidWorkspaceId(workspaceId)) {
+        // Silently ignore or notify user; at minimum, avoid possibly malicious request
+        setOpen(false);
+        return;
+      }
       if (skip) {
         await axios.post(`/api/workspace/${workspaceId}/birthday`, { day: 0, month: 0, timezone });
       } else {
EOF
@@ -15,13 +15,27 @@
const [loading, setLoading] = useState(false);
const [initialLoaded, setInitialLoaded] = useState(false);

// Helper for validating workspaceId: allow only numbers or UUIDs. Adjust pattern as needed.
const isValidWorkspaceId = (id: string | number): boolean => {
if (typeof id === "number") {
return Number.isInteger(id) && id > 0;
}
if (typeof id === "string") {
// Accept only positive integer string or canonical UUID; adjust as needed for your system
const intPattern = /^[1-9][0-9]*$/;
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return intPattern.test(id) || uuidPattern.test(id);
}
return false;
};

useEffect(() => {
const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
setTimezone(detectedTimezone);
}, []);

useEffect(() => {
if (!workspaceId) return;
if (!workspaceId || !isValidWorkspaceId(workspaceId)) return;
let cancelled = false;
(async () => {
try {
@@ -58,6 +66,11 @@
const save = async (skip = false) => {
setLoading(true);
try {
if (!isValidWorkspaceId(workspaceId)) {
// Silently ignore or notify user; at minimum, avoid possibly malicious request
setOpen(false);
return;
}
if (skip) {
await axios.post(`/api/workspace/${workspaceId}/birthday`, { day: 0, month: 0, timezone });
} else {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread components/bdayprompt.tsx
await axios.post(`/api/workspace/${workspaceId}/birthday`, { day: 0, month: 0, timezone });
} else {
await axios.post(`/api/workspace/${workspaceId}/birthday`, { day: Number(day), month: Number(month) });
await axios.post(`/api/workspace/${workspaceId}/birthday`, { day: Number(day), month: Number(month), timezone });

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the SSRF/path traversal risk, we should validate (or sanitize) workspaceId before including it in the URL. Since the allowed values are likely to be either numeric or slug/UUID identifiers for existing workspaces, we should constrain the allowed set as strictly as possible. For example, if IDs are always numbers, allow only numbers; if they're UUIDs, validate the UUID pattern; if they're slugs, allow only alphanumerics (and possibly -/_). This can be achieved with simple regex checks or type coercion. If the identifier fails validation, do not make the API call (or raise an error/UI notification).

The ideal place for this validation is just before making the API request—namely, in components/bdayprompt.tsx. We'll add a function to check that workspaceId is valid, and ensure the API call is only made if so. We'll apply validation in both the useEffect (GET) and in the save (POST).
If not valid, the component should not interact with the API.


Suggested changeset 1
components/bdayprompt.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/bdayprompt.tsx b/components/bdayprompt.tsx
--- a/components/bdayprompt.tsx
+++ b/components/bdayprompt.tsx
@@ -7,6 +7,23 @@
   visible?: boolean;
 }
 
+// Only allow numeric IDs or UUID format; adjust regex as needed for your ID convention
+function isValidWorkspaceId(id: unknown): boolean {
+  // Numeric ID
+  if (typeof id === "number") {
+    return Number.isInteger(id) && id > 0;
+  }
+  if (typeof id === "string") {
+    // Allow integer strings
+    if (/^\d+$/.test(id)) return true;
+    // Allow simple slug (alphanumeric, dash, underscore, at least 3 chars)
+    if (/^[a-zA-Z0-9-_]{3,}$/.test(id)) return true;
+    // Allow UUIDs (version 1-5, optional dashes)
+    if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)) return true;
+  }
+  return false;
+}
+
 export const WorkspaceBirthdayPrompt: React.FC<WorkspaceBirthdayPromptProps> = ({ workspaceId, visible }) => {
   const [open, setOpen] = useState(false);
   const [month, setMonth] = useState('');
@@ -21,7 +38,7 @@
   }, []);
 
   useEffect(() => {
-    if (!workspaceId) return;
+    if (!isValidWorkspaceId(workspaceId)) return;
     let cancelled = false;
     (async () => {
       try {
@@ -56,6 +73,10 @@
   const days = month ? Array.from({ length: daysInMonth(Number(month)) }, (_, i) => i + 1) : [];
 
   const save = async (skip = false) => {
+    if (!isValidWorkspaceId(workspaceId)) {
+      setLoading(false);
+      return;
+    }
     setLoading(true);
     try {
       if (skip) {
@@ -69,7 +90,7 @@
     }
   };
 
-  if (!open || !initialLoaded) return null;
+  if (!isValidWorkspaceId(workspaceId) || !open || !initialLoaded) return null;
 
   return (
     <div className="fixed inset-0 flex items-center justify-center bg-black/50 z-50 p-4">
EOF
@@ -7,6 +7,23 @@
visible?: boolean;
}

// Only allow numeric IDs or UUID format; adjust regex as needed for your ID convention
function isValidWorkspaceId(id: unknown): boolean {
// Numeric ID
if (typeof id === "number") {
return Number.isInteger(id) && id > 0;
}
if (typeof id === "string") {
// Allow integer strings
if (/^\d+$/.test(id)) return true;
// Allow simple slug (alphanumeric, dash, underscore, at least 3 chars)
if (/^[a-zA-Z0-9-_]{3,}$/.test(id)) return true;
// Allow UUIDs (version 1-5, optional dashes)
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)) return true;
}
return false;
}

export const WorkspaceBirthdayPrompt: React.FC<WorkspaceBirthdayPromptProps> = ({ workspaceId, visible }) => {
const [open, setOpen] = useState(false);
const [month, setMonth] = useState('');
@@ -21,7 +38,7 @@
}, []);

useEffect(() => {
if (!workspaceId) return;
if (!isValidWorkspaceId(workspaceId)) return;
let cancelled = false;
(async () => {
try {
@@ -56,6 +73,10 @@
const days = month ? Array.from({ length: daysInMonth(Number(month)) }, (_, i) => i + 1) : [];

const save = async (skip = false) => {
if (!isValidWorkspaceId(workspaceId)) {
setLoading(false);
return;
}
setLoading(true);
try {
if (skip) {
@@ -69,7 +90,7 @@
}
};

if (!open || !initialLoaded) return null;
if (!isValidWorkspaceId(workspaceId) || !open || !initialLoaded) return null;

return (
<div className="fixed inset-0 flex items-center justify-center bg-black/50 z-50 p-4">
Copilot is powered by AI and may make mistakes. Always verify output.
const fetchConfig = async () => {
if (id) {
try {
const res = await axios.get(`/api/workspace/${id}/settings/activity/getConfig`);

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix this problem while preserving existing frontend functionality, we should ensure that the user-provided id parameter cannot be abused to create unexpected or unauthorized backend API calls. The best fix, given only the shown code, is to strictly validate id before inserting it into the URL. Typically, workspace IDs have a fixed expected format, such as an alphanumeric string, UUID, or database object ID. We should ensure that id matches the allowed pattern (e.g., /^[a-zA-Z0-9_-]+$/ for alphanumeric + dashes/underscores) before making the API call. If it does not, we should avoid making the request, show an error, and possibly log the event.

The change should be applied within the file components/profile/activity.tsx, specifically within the fetchConfig function inside the useEffect on line 128–140. This involves validating id before calling axios, and handling the error scenario. No third-party validation package is required; a simple regex suffices.


Suggested changeset 1
components/profile/activity.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/profile/activity.tsx b/components/profile/activity.tsx
--- a/components/profile/activity.tsx
+++ b/components/profile/activity.tsx
@@ -127,13 +127,16 @@
 
   useEffect(() => {
     const fetchConfig = async () => {
-      if (id) {
+      if (id && typeof id === "string" && /^[a-zA-Z0-9_-]+$/.test(id)) {
         try {
           const res = await axios.get(`/api/workspace/${id}/settings/activity/getConfig`);
           setIdleTimeEnabled(res.data.idleTimeEnabled ?? true);
         } catch (error) {
           console.error("Failed to fetch activity config:", error);
         }
+      } else if (id) {
+        // Invalid id, do not send request
+        console.error("Invalid workspace id provided:", id);
       }
     };
     fetchConfig();
EOF
@@ -127,13 +127,16 @@

useEffect(() => {
const fetchConfig = async () => {
if (id) {
if (id && typeof id === "string" && /^[a-zA-Z0-9_-]+$/.test(id)) {
try {
const res = await axios.get(`/api/workspace/${id}/settings/activity/getConfig`);
setIdleTimeEnabled(res.data.idleTimeEnabled ?? true);
} catch (error) {
console.error("Failed to fetch activity config:", error);
}
} else if (id) {
// Invalid id, do not send request
console.error("Invalid workspace id provided:", id);
}
};
fetchConfig();
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +183 to +185
const { data, status } = await axios.get(
`/api/workspace/${id}/activity/${sessionId}`
);

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix this SSRF (and potential path traversal) risk, the user-supplied id value should be validated before being used to construct API endpoint paths. The recommended approach is to explicitly validate id against an allow-list of acceptable values (if feasible), or at minimum sanitize the value to ensure it only contains permitted characters (e.g., a UUID, numeric string, or alphanumeric slug). For this component, assuming id should be a string consisting of standard URL-safe characters (such as a UUID, alphanumerics, or slug), we should validate it before it is used.

A safe and straightforward patch, within the shown code, is to check that id matches an expected pattern (such as a UUID, or whitelist of workspace IDs if available). If invalid, do not make the API request and show an error. This validation can be done at the start of fetchSession.

Changes needed:

  • Before using id when forming the API path, validate id matches an expected pattern (e.g., /^[a-zA-Z0-9_-]+$/ for slugs, or a stricter UUID regex if appropriate).
  • If id is invalid, do not proceed with the API request, and show a user-friendly error.
  • No new packages are needed; regex validation is standard.
  • Only code inside the components/profile/activityoverview.tsx file (as shown) needs to be edited.
Suggested changeset 1
components/profile/activityoverview.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/profile/activityoverview.tsx b/components/profile/activityoverview.tsx
--- a/components/profile/activityoverview.tsx
+++ b/components/profile/activityoverview.tsx
@@ -175,6 +175,12 @@
   }, [liveSessionTimer]);
 
   const fetchSession = async (sessionId: string) => {
+    // Validate the 'id' to allow only workspace slugs (alphanumeric, -, _), adjust regex as needed
+    const idPattern = /^[a-zA-Z0-9_-]+$/;
+    if (!id || typeof id !== "string" || !idPattern.test(id)) {
+      toast.error("Invalid workspace ID.");
+      return;
+    }
     setLoading(true);
     setIsOpen(true);
     setConcurrentUsers([]);
EOF
@@ -175,6 +175,12 @@
}, [liveSessionTimer]);

const fetchSession = async (sessionId: string) => {
// Validate the 'id' to allow only workspace slugs (alphanumeric, -, _), adjust regex as needed
const idPattern = /^[a-zA-Z0-9_-]+$/;
if (!id || typeof id !== "string" || !idPattern.test(id)) {
toast.error("Invalid workspace ID.");
return;
}
setLoading(true);
setIsOpen(true);
setConcurrentUsers([]);
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +204 to +206
const concurrentResponse = await axios.get(
`/api/workspace/${id}/activity/concurrent?sessionId=${sessionId}&startTime=${data.message.startTime}&endTime=${data.message.endTime}`
);

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the problem, user-supplied data from router.query.id must be validated before being interpolated into the API endpoint URL. The best-practice way is to define a whitelist or strict validation for workspace IDs, ensuring it follows expected format—for example, a UUID, alphanumeric string, or similar—before using it in any API call. This can be implemented in the React component with a simple validation check, for example:

  • Accept only alphanumeric/known-safe characters for workspace IDs.
  • If invalid, refuse to make the request or replace with a safe fallback.

The safest edit is to define a function to check that id is a string and matches the expected pattern before interpolating it in any outgoing API request. If not, show an error and return early. This logic belongs directly before the lines where the risky call is made—within the code shown, in the relevant async function.

We also need to ensure that validation is DRY and clear, so a helper like isValidWorkspaceId(id) can be introduced (above/beside the component definition, or in a relevant spot within the file). The validation can be simple (e.g., /^[a-zA-Z0-9_-]+$/ for slugs/IDs, or a UUID regex for UUIDs).

Edits needed:

  • Add a function to validate id.
  • Before using /api/workspace/${id}/activity/concurrent?sessionId=..., check that id is valid.
  • If not valid, do not send request and notify the user.

Suggested changeset 1
components/profile/activityoverview.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/profile/activityoverview.tsx b/components/profile/activityoverview.tsx
--- a/components/profile/activityoverview.tsx
+++ b/components/profile/activityoverview.tsx
@@ -93,6 +93,11 @@
   avatar: string;
 };
 
+// Validate workspace IDs: Only allow alphanumeric, hyphens, or underscores (adjust as needed)
+function isValidWorkspaceId(id: unknown): id is string {
+  return typeof id === "string" && /^[a-zA-Z0-9_-]+$/.test(id);
+}
+
 export function ActivityOverview({
   data,
   displayMinutes,
@@ -200,6 +205,11 @@
       });
 
       if (data.message?.startTime && data.message?.endTime) {
+        if (!isValidWorkspaceId(id)) {
+          toast.error("Invalid workspace ID.");
+          setLoading(false);
+          return;
+        }
         try {
           const concurrentResponse = await axios.get(
             `/api/workspace/${id}/activity/concurrent?sessionId=${sessionId}&startTime=${data.message.startTime}&endTime=${data.message.endTime}`
EOF
@@ -93,6 +93,11 @@
avatar: string;
};

// Validate workspace IDs: Only allow alphanumeric, hyphens, or underscores (adjust as needed)
function isValidWorkspaceId(id: unknown): id is string {
return typeof id === "string" && /^[a-zA-Z0-9_-]+$/.test(id);
}

export function ActivityOverview({
data,
displayMinutes,
@@ -200,6 +205,11 @@
});

if (data.message?.startTime && data.message?.endTime) {
if (!isValidWorkspaceId(id)) {
toast.error("Invalid workspace ID.");
setLoading(false);
return;
}
try {
const concurrentResponse = await axios.get(
`/api/workspace/${id}/activity/concurrent?sessionId=${sessionId}&startTime=${data.message.startTime}&endTime=${data.message.endTime}`
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +163 to +173
await axios.patch(
`/api/workspace/${workspaceId}/profile/${user.userid}/member-info`,
{
department,
lineManagerId: selectedManager?.userid || null,
timezone: selectedTimezone || null,
birthdayDay: birthdayDay ? parseInt(birthdayDay as string) : null,
birthdayMonth: birthdayMonth ? parseInt(birthdayMonth as string) : null,
discordId: discordId || null,
}
);

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix this SSRF concern, the value of workspaceId (taken from router.query.id) should be validated or sanitized before it's interpolated into the API endpoint URL. The best practice is to:

  • Whitelist or validate the format of workspaceId, ensuring it only contains allowed characters (e.g., alphanumerics, hyphens, or whatever your workspace IDs are guaranteed to be).
  • Reject or sanitize (return or handle gracefully) if the value does not conform.
  • The validation should be performed just after assigning workspaceId and before using it in requests.
  • For safety, avoid any /, \, or other path traversal characters.

To implement this:

  • Add a function to validate workspace IDs (e.g., only allow slugs like /^[a-zA-Z0-9-_]+$/).
  • In handleSave, before proceeding, check workspaceId against this function; if invalid, don't proceed (show error and return).
  • If using TypeScript, optionally cast/parse to the expected type.
  • No external dependencies are needed; use a simple regex.

Edit to make:

  • Insert a validateWorkspaceId helper (if not already present).
  • Check and return early in handleSave if invalid.
Suggested changeset 1
components/profile/information.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/profile/information.tsx b/components/profile/information.tsx
--- a/components/profile/information.tsx
+++ b/components/profile/information.tsx
@@ -139,6 +139,12 @@
   const [localTime, setLocalTime] = useState("");
   const [isNight, setIsNight] = useState(false);
 
+
+  // Only allow alphanumerics, dashes, underscores (adjust as needed)
+  function validateWorkspaceId(id: string): boolean {
+    return typeof id === "string" && /^[a-zA-Z0-9_-]+$/.test(id);
+  }
+
   const workspaceId = router.query.id as string;
   const canEdit = canEditMembers && (isUser || isAdmin);
 
@@ -157,7 +163,10 @@
   }, [workspaceMember?.timezone]);
 
   const handleSave = async () => {
-    if (!workspaceId) return;
+    if (!workspaceId || !validateWorkspaceId(workspaceId)) {
+      toast.error("Invalid workspace ID");
+      return;
+    }
     setLoading(true);
     try {
       await axios.patch(
EOF
@@ -139,6 +139,12 @@
const [localTime, setLocalTime] = useState("");
const [isNight, setIsNight] = useState(false);


// Only allow alphanumerics, dashes, underscores (adjust as needed)
function validateWorkspaceId(id: string): boolean {
return typeof id === "string" && /^[a-zA-Z0-9_-]+$/.test(id);
}

const workspaceId = router.query.id as string;
const canEdit = canEditMembers && (isUser || isAdmin);

@@ -157,7 +163,10 @@
}, [workspaceMember?.timezone]);

const handleSave = async () => {
if (!workspaceId) return;
if (!workspaceId || !validateWorkspaceId(workspaceId)) {
toast.error("Invalid workspace ID");
return;
}
setLoading(true);
try {
await axios.patch(
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +19 to +22
axios
.get(
`/api/workspace/${router.query.id}/settings/general/birthdays/hook`
)

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To address potential SSRF or routing manipulation, we should strictly validate or sanitize router.query.id before including it in a constructed URL. If the workspace id should be a UUID, restrict it to valid UUIDs; if numeric, restrict to valid integer IDs. In this code, before using router.query.id in the axios calls, we can perform basic validation to ensure it conforms to the expected id format. If it doesn’t, we should refuse to issue the backend API request and optionally show an error. The minimal change is to add a validation function (e.g., isValidId(id)) and only proceed with data fetching and requests if id passes validation.

The changes will go in the file components/settings/general/bhooks.tsx. We need to:

  • Define a validation function for workspace IDs.
  • Use this function to guard outgoing axios requests (on .get as well as other places using router.query.id).

No external dependencies are needed unless the ID format is complex (for simple UUID or numeric, no new packages are required).


Suggested changeset 1
components/settings/general/bhooks.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/settings/general/bhooks.tsx b/components/settings/general/bhooks.tsx
--- a/components/settings/general/bhooks.tsx
+++ b/components/settings/general/bhooks.tsx
@@ -14,8 +14,15 @@
   const [loading, setLoading] = useState(false);
   const [testing, setTesting] = useState(false);
 
+  // Accept only UUIDs as workspace IDs
+  function isValidWorkspaceId(id: string | string[] | undefined): boolean {
+    // If your IDs are not UUIDs, adjust this regex accordingly
+    if (typeof id !== 'string') return false;
+    return /^[0-9a-fA-F\-]{36}$/.test(id);
+  }
+
   useEffect(() => {
-    if (router.query.id) {
+    if (router.query.id && isValidWorkspaceId(router.query.id)) {
       axios
         .get(
           `/api/workspace/${router.query.id}/settings/general/birthdays/hook`
@@ -29,10 +35,17 @@
         .catch((err) => {
           console.error("Error fetching birthday webhook config:", err);
         });
+    } else if (router.query.id) {
+      toast.error("Invalid workspace ID");
     }
   }, [router.query.id]);
 
   const handleSave = async () => {
+    // Validate workspace id before saving
+    if (!isValidWorkspaceId(router.query.id)) {
+      toast.error("Invalid workspace ID");
+      return;
+    }
     setLoading(true);
     try {
       await axios.patch(
@@ -56,6 +65,11 @@
       toast.error("Please enter a webhook URL first");
       return;
     }
+    // Validate workspace id before testing
+    if (!isValidWorkspaceId(router.query.id)) {
+      toast.error("Invalid workspace ID");
+      return;
+    }
 
     setTesting(true);
     try {
EOF
@@ -14,8 +14,15 @@
const [loading, setLoading] = useState(false);
const [testing, setTesting] = useState(false);

// Accept only UUIDs as workspace IDs
function isValidWorkspaceId(id: string | string[] | undefined): boolean {
// If your IDs are not UUIDs, adjust this regex accordingly
if (typeof id !== 'string') return false;
return /^[0-9a-fA-F\-]{36}$/.test(id);
}

useEffect(() => {
if (router.query.id) {
if (router.query.id && isValidWorkspaceId(router.query.id)) {
axios
.get(
`/api/workspace/${router.query.id}/settings/general/birthdays/hook`
@@ -29,10 +35,17 @@
.catch((err) => {
console.error("Error fetching birthday webhook config:", err);
});
} else if (router.query.id) {
toast.error("Invalid workspace ID");
}
}, [router.query.id]);

const handleSave = async () => {
// Validate workspace id before saving
if (!isValidWorkspaceId(router.query.id)) {
toast.error("Invalid workspace ID");
return;
}
setLoading(true);
try {
await axios.patch(
@@ -56,6 +65,11 @@
toast.error("Please enter a webhook URL first");
return;
}
// Validate workspace id before testing
if (!isValidWorkspaceId(router.query.id)) {
toast.error("Invalid workspace ID");
return;
}

setTesting(true);
try {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +38 to +44
await axios.patch(
`/api/workspace/${router.query.id}/settings/general/birthdays/hook`,
{
enabled,
url: webhookUrl,
}
);

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix this problem, constrain router.query.id to only allow safe values before using it in the URL for the API call. Typically, this involves validating that id matches the expected format (such as a UUID, numeric ID, or a string of specific permitted characters), thus preventing arbitrary path injection, path traversal, or other abuses. The best single fix is to extract id from router.query, validate it against a strict pattern (for example, a RegExp that matches only allowed formats), and only use it if validation passes. Otherwise, display an error or abort the operation. This can be done inline before using the value in string interpolation for the URL.

What is needed:

  • Import or define a function or regex to validate the allowed format for id (e.g., only alphanumeric, or UUID).
  • Add a check before making the axios.patch call to ensure that router.query.id is valid.
  • Do the same validation in the useEffect that makes an axios.get call to the same dynamic endpoint.
  • Display an error or otherwise abort if the value is invalid.

Suggested changeset 1
components/settings/general/bhooks.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/settings/general/bhooks.tsx b/components/settings/general/bhooks.tsx
--- a/components/settings/general/bhooks.tsx
+++ b/components/settings/general/bhooks.tsx
@@ -15,10 +15,14 @@
   const [testing, setTesting] = useState(false);
 
   useEffect(() => {
-    if (router.query.id) {
+    const id = router.query.id;
+    // Restrict id to a valid pattern (e.g., UUID v4, or only alphanumerics/hyphen/underscore)
+    // Adjust the regex as appropriate for your IDs
+    const validIdPattern = /^[a-zA-Z0-9-_]{1,64}$/;
+    if (typeof id === "string" && validIdPattern.test(id)) {
       axios
         .get(
-          `/api/workspace/${router.query.id}/settings/general/birthdays/hook`
+          `/api/workspace/${id}/settings/general/birthdays/hook`
         )
         .then((res) => {
           if (res.data.value) {
@@ -29,14 +31,23 @@
         .catch((err) => {
           console.error("Error fetching birthday webhook config:", err);
         });
+    } else if (id) {
+      console.error("Invalid workspace ID format.");
     }
   }, [router.query.id]);
 
   const handleSave = async () => {
     setLoading(true);
+    const id = router.query.id;
+    const validIdPattern = /^[a-zA-Z0-9-_]{1,64}$/;
+    if (typeof id !== "string" || !validIdPattern.test(id)) {
+      toast.error("Invalid workspace ID.");
+      setLoading(false);
+      return;
+    }
     try {
       await axios.patch(
-        `/api/workspace/${router.query.id}/settings/general/birthdays/hook`,
+        `/api/workspace/${id}/settings/general/birthdays/hook`,
         {
           enabled,
           url: webhookUrl,
EOF
@@ -15,10 +15,14 @@
const [testing, setTesting] = useState(false);

useEffect(() => {
if (router.query.id) {
const id = router.query.id;
// Restrict id to a valid pattern (e.g., UUID v4, or only alphanumerics/hyphen/underscore)
// Adjust the regex as appropriate for your IDs
const validIdPattern = /^[a-zA-Z0-9-_]{1,64}$/;
if (typeof id === "string" && validIdPattern.test(id)) {
axios
.get(
`/api/workspace/${router.query.id}/settings/general/birthdays/hook`
`/api/workspace/${id}/settings/general/birthdays/hook`
)
.then((res) => {
if (res.data.value) {
@@ -29,14 +31,23 @@
.catch((err) => {
console.error("Error fetching birthday webhook config:", err);
});
} else if (id) {
console.error("Invalid workspace ID format.");
}
}, [router.query.id]);

const handleSave = async () => {
setLoading(true);
const id = router.query.id;
const validIdPattern = /^[a-zA-Z0-9-_]{1,64}$/;
if (typeof id !== "string" || !validIdPattern.test(id)) {
toast.error("Invalid workspace ID.");
setLoading(false);
return;
}
try {
await axios.patch(
`/api/workspace/${router.query.id}/settings/general/birthdays/hook`,
`/api/workspace/${id}/settings/general/birthdays/hook`,
{
enabled,
url: webhookUrl,
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +62 to +65
const response = await axios.post(
`/api/workspace/${router.query.id}/settings/general/birthdays/test`,
{ url: webhookUrl }
);

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

The solution is to sanitize and validate the router.query.id value before incorporating it into the API request URL. If the backend expects a specific format or set of workspace IDs (UUIDs, numerical IDs, etc.), the frontend should confirm the value matches the allowed pattern before making the request. Ideally, an allow-list or regex match can enforce this. If invalid, the action should be halted and the user notified. This fix concerns the usages of router.query.id in /api/workspace/${router.query.id}/settings/general/birthdays/test (in handleTest), as well as /api/workspace/${router.query.id}/settings/general/birthdays/hook (in useEffect and handleSave), to consistently prevent any malformed or malicious input from being sent to the backend API route.

Specifically, add a validation helper (e.g., isValidId) above or within the component that only allows workspace IDs matching an expected pattern (such as /^[a-zA-Z0-9_-]+$/). Before every API request that uses router.query.id, check that router.query.id is valid; if not, do not proceed (perhaps show an error toast). This requires a new function definition and checks at all API-call points where router.query.id is interpolated.


Suggested changeset 1
components/settings/general/bhooks.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/components/settings/general/bhooks.tsx b/components/settings/general/bhooks.tsx
--- a/components/settings/general/bhooks.tsx
+++ b/components/settings/general/bhooks.tsx
@@ -6,6 +6,11 @@
 import { useRouter } from "next/router";
 import { IconGift, IconCheck, IconX } from "@tabler/icons-react";
 
+// Helper to validate workspace id (allow only alphanumerics, '-', '_' for example)
+function isValidId(id: any) {
+  return typeof id === 'string' && /^[a-zA-Z0-9_-]+$/.test(id);
+}
+
 function BirthdayWebhook() {
   const [workspace, setWorkspace] = useRecoilState(workspacestate);
   const router = useRouter();
@@ -15,7 +20,7 @@
   const [testing, setTesting] = useState(false);
 
   useEffect(() => {
-    if (router.query.id) {
+    if (router.query.id && isValidId(router.query.id)) {
       axios
         .get(
           `/api/workspace/${router.query.id}/settings/general/birthdays/hook`
@@ -29,10 +34,16 @@
         .catch((err) => {
           console.error("Error fetching birthday webhook config:", err);
         });
+    } else if (router.query.id) {
+      toast.error("Invalid workspace ID");
     }
   }, [router.query.id]);
 
   const handleSave = async () => {
+    if (!isValidId(router.query.id)) {
+      toast.error("Invalid workspace ID");
+      return;
+    }
     setLoading(true);
     try {
       await axios.patch(
@@ -56,6 +63,10 @@
       toast.error("Please enter a webhook URL first");
       return;
     }
+    if (!isValidId(router.query.id)) {
+      toast.error("Invalid workspace ID");
+      return;
+    }
 
     setTesting(true);
     try {
EOF
@@ -6,6 +6,11 @@
import { useRouter } from "next/router";
import { IconGift, IconCheck, IconX } from "@tabler/icons-react";

// Helper to validate workspace id (allow only alphanumerics, '-', '_' for example)
function isValidId(id: any) {
return typeof id === 'string' && /^[a-zA-Z0-9_-]+$/.test(id);
}

function BirthdayWebhook() {
const [workspace, setWorkspace] = useRecoilState(workspacestate);
const router = useRouter();
@@ -15,7 +20,7 @@
const [testing, setTesting] = useState(false);

useEffect(() => {
if (router.query.id) {
if (router.query.id && isValidId(router.query.id)) {
axios
.get(
`/api/workspace/${router.query.id}/settings/general/birthdays/hook`
@@ -29,10 +34,16 @@
.catch((err) => {
console.error("Error fetching birthday webhook config:", err);
});
} else if (router.query.id) {
toast.error("Invalid workspace ID");
}
}, [router.query.id]);

const handleSave = async () => {
if (!isValidId(router.query.id)) {
toast.error("Invalid workspace ID");
return;
}
setLoading(true);
try {
await axios.patch(
@@ -56,6 +63,10 @@
toast.error("Please enter a webhook URL first");
return;
}
if (!isValidId(router.query.id)) {
toast.error("Invalid workspace ID");
return;
}

setTesting(true);
try {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +98 to +104
const response = await fetch(webhookConfig.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(webhookBody),
});

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a user-provided value.

Copilot Autofix

AI 7 months ago

General approach:
Validate all webhook URLs strictly against an allow-list of domains and URL structure before making outgoing HTTP requests. Do not rely solely on validation at the configuration-input endpoints. Specifically, before making the fetch request in pages/api/cron/birthdays.ts, ensure the loaded webhook URL is a valid HTTPS Discord webhook endpoint (using a strict regex).

Detailed description:

  • In pages/api/cron/birthdays.ts, before calling fetch(webhookConfig.url, ...), check that webhookConfig.url is a string and matches a strict regular expression for Discord webhook URLs (e.g., ^https:\/\/discord\.com\/api\/webhooks\/\d+\/[\w\-]+).
  • If the check fails, skip sending the request for that workspace (just as the code skips if the URL isn't present).
  • No changes are needed to input or config-writing endpoints, since the key fix is to prevent SSRF via runtime validation at the request sink.
  • No additional dependencies are required to use regex matching.
  • Add a helper function, e.g., isValidDiscordWebhookURL(url) above the handler, and use this to guard the request.

What to change:

  • In pages/api/cron/birthdays.ts:
    • Add the validator function.
    • Change the webhook sending section to check for URL validity before sending.

Suggested changeset 1
pages/api/cron/birthdays.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pages/api/cron/birthdays.ts b/pages/api/cron/birthdays.ts
--- a/pages/api/cron/birthdays.ts
+++ b/pages/api/cron/birthdays.ts
@@ -2,6 +2,12 @@
 import prisma from "@/utils/database";
 import { getConfig } from "@/utils/configEngine";
 
+// Only allow real Discord webhook URLs
+function isValidDiscordWebhookURL(url: any): boolean {
+  if (typeof url !== 'string') return false;
+  return /^https:\/\/discord\.com\/api\/webhooks\/\d+\/[\w\-]+/i.test(url);
+}
+
 export default async function handler(
   req: NextApiRequest,
   res: NextApiResponse
@@ -29,7 +35,12 @@
       try {
         const webhookConfig = await getConfig("birthday_webhook", workspace.groupId);
         
-        if (!webhookConfig || !webhookConfig.enabled || !webhookConfig.url) {
+        if (
+          !webhookConfig ||
+          !webhookConfig.enabled ||
+          !webhookConfig.url ||
+          !isValidDiscordWebhookURL(webhookConfig.url)
+        ) {
           continue;
         }
 
EOF
@@ -2,6 +2,12 @@
import prisma from "@/utils/database";
import { getConfig } from "@/utils/configEngine";

// Only allow real Discord webhook URLs
function isValidDiscordWebhookURL(url: any): boolean {
if (typeof url !== 'string') return false;
return /^https:\/\/discord\.com\/api\/webhooks\/\d+\/[\w\-]+/i.test(url);
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
@@ -29,7 +35,12 @@
try {
const webhookConfig = await getConfig("birthday_webhook", workspace.groupId);

if (!webhookConfig || !webhookConfig.enabled || !webhookConfig.url) {
if (
!webhookConfig ||
!webhookConfig.enabled ||
!webhookConfig.url ||
!isValidDiscordWebhookURL(webhookConfig.url)
) {
continue;
}

Copilot is powered by AI and may make mistakes. Always verify output.
@cloudysatrn
cloudysatrn merged commit 2b85796 into main Dec 14, 2025
2 checks passed
@cloudysatrn
cloudysatrn deleted the saturnsupdate branch December 14, 2025 19:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
pages/api/admin/instance-config.ts (1)

21-40: robloxClientSecret is stored and returned in plaintext — tighten secret handling before shipping.

Even with owner-only access, returning the secret in GET (Line 38) and persisting it raw increases accidental exposure risk (UI/devtools, logs, backups). Consider: don’t return it after initial save (return masked **** + “isSet” flag), and encrypt-at-rest (KMS/app-level) if it must be stored.

Also applies to: 47-55

components/settings/general/color.tsx (1)

39-43: Sync effect won’t handle “clear theme” cases (empty string).
Because Line 40 checks truthiness, a valid “reset to no theme” ("") won’t update selectedColor. If clearing is supported, set unconditionally (or check for undefined instead).

-  useEffect(() => {
-    if (workspace?.groupTheme) {
-      setSelectedColor(workspace.groupTheme);
-    }
-  }, [workspace?.groupTheme]);
+  useEffect(() => {
+    // Sync even when theme is intentionally cleared to ""
+    setSelectedColor(workspace?.groupTheme ?? "");
+  }, [workspace?.groupTheme]);
pages/workspace/[id]/policies.tsx (1)

210-221: Update the pageProps type to include isOwnerRole.

The roles type definition doesn't include isOwnerRole, but it's used for filtering at lines 1580 and 2231. This causes a type mismatch.

 type pageProps = {
   isUserView?: boolean;
   documents?: (document & {
     owner: { username: string; picture: string };
     roles: Array<{ id: string; name: string }>;
     acknowledgments: Array<{
       user: { userid: string; username: string; picture: string };
       acknowledgedAt: string;
     }>;
   })[];
-  roles?: Array<{ id: string; name: string }>;
+  roles?: Array<{ id: string; name: string; isOwnerRole?: boolean }>;
 };
prisma/migrations/20251011203302_hw_25/migration.sql (1)

49-57: Some code paths do not set joinDate, creating members with NULL values.

The joinDate column now has no default, but pages/api/workspace/[id]/home/upcoming.ts and pages/api/workspace/[id]/profile/[uid]/member-info.ts create workspace members without explicitly setting joinDate. This results in NULL values instead of the current timestamp.

Fix these operations to include joinDate: new Date() in their create blocks, or document whether NULL joinDate is intentional.

pages/api/workspace/[id]/profile/[uid].ts (1)

167-234: Fix sessionsLogged.byType: you're reading session.type but didn't select it

allSessionParticipations selects { id, date, sessionType { ... } }, so (p.session as any).type will be undefined and everything buckets into "other".

       include: {
         session: {
           select: {
             id: true,
             date: true,
+            type: true,
             sessionType: {
               select: {
                 slots: true,
                 name: true,
               },
             },
           },
         },
       },
🟠 Major comments (17)
components/settings/general/color.tsx-45-48 (1)

45-48: Guard workspace.groupId before PATCHes (avoid calling /api/workspace/undefined/...).
With settings pages, it’s common for workspace to load async; updateColor / updateSessionColor assume workspace.groupId exists. Consider disabling the controls until workspace?.groupId is truthy, or early-return with a toast.

Also applies to: 67-82, 95-107

pages/workspace/[id]/wall.tsx-55-66 (1)

55-66: Merge permissions from all user roles.

The system supports users having multiple roles in a single workspace, but this code extracts permissions from only the first role. Users with additional roles will lose access to permissions granted by those other roles, causing authorization failures (e.g., a user with manage_wall in a secondary role won't be able to delete posts).

Change:

-    const userPermissions = user?.roles?.[0]?.permissions || [];
+    const userPermissions = user?.roles?.flatMap(role => role.permissions) || [];
components/profile/activityoverview.tsx-177-220 (1)

177-220: Potential Server-Side Request Forgery (SSRF) vulnerability.

The id parameter from router.query is used directly in API URLs without validation. While this is a client-side component, the pattern could be exploited if an attacker can control the id query parameter to make requests to unintended endpoints.

Consider validating the id parameter before using it in API calls:

 const fetchSession = async (sessionId: string) => {
+  // Validate workspace ID format
+  if (!id || typeof id !== 'string' || !/^\d+$/.test(id)) {
+    toast.error("Invalid workspace ID");
+    return;
+  }
+
   setLoading(true);
   setIsOpen(true);
   setConcurrentUsers([]);
components/profile/activityoverview.tsx-147-167 (1)

147-167: Potential infinite loop in useEffect due to liveSessionTimer in dependency array.

Including liveSessionTimer in the dependency array while also setting it inside the effect can cause re-renders. Additionally, on Line 163, accessing liveSessionTimer after it might have changed can lead to clearing the wrong timer.

Consider using a ref to store the timer instead:

-const [liveSessionTimer, setLiveSessionTimer] = useState<NodeJS.Timeout | null>(null);
+const liveSessionTimerRef = useRef<NodeJS.Timeout | null>(null);

 useEffect(() => {
   const hasLiveSessions = timeline.some(
     (item) => item.__type === "session" && item.active && !item.endTime
   );

   if (hasLiveSessions) {
     const timer = setInterval(() => {
       setTimeline((prev) => [...prev]);
     }, 60000);

-    setLiveSessionTimer(timer);
+    liveSessionTimerRef.current = timer;

     return () => {
       clearInterval(timer);
-      setLiveSessionTimer(null);
+      liveSessionTimerRef.current = null;
     };
-  } else if (liveSessionTimer) {
-    clearInterval(liveSessionTimer);
-    setLiveSessionTimer(null);
+  } else if (liveSessionTimerRef.current) {
+    clearInterval(liveSessionTimerRef.current);
+    liveSessionTimerRef.current = null;
   }
-}, [timeline, liveSessionTimer]);
+}, [timeline]);

Committable suggestion skipped: line range outside the PR's diff.

pages/api/workspace/[id]/allies/[aid]/visits/[vid].ts-97-105 (1)

97-105: Add the same time/participants validation on PATCH to prevent 500s.
Current code will throw on invalid participants shapes and can write an Invalid Date.

  if(!req.body.name || !req.body.time) return res.status(400).json({ success: false, error: 'Missing data' })

+ const parsedTime = new Date(req.body.time)
+ if (Number.isNaN(parsedTime.getTime())) {
+   return res.status(400).json({ success: false, error: 'Invalid time' })
+ }
+ if (req.body.participants !== undefined && !Array.isArray(req.body.participants)) {
+   return res.status(400).json({ success: false, error: 'Invalid participants' })
+ }

  const updateData: any = {
    name: req.body.name,
-   time: new Date(req.body.time)
+   time: parsedTime
  };

  if (req.body.participants !== undefined) {
-   updateData.participants = req.body.participants.map((p: number) => BigInt(p));
+   updateData.participants = req.body.participants.map((p: unknown) => {
+     if (typeof p !== 'number' || !Number.isSafeInteger(p)) throw new Error('Invalid participant id')
+     return BigInt(p)
+   });
  }

Also applies to: 112-113

pages/api/workspace/[id]/activity/stats.ts-7-8 (1)

7-8: Guard workspaceId parsing; don’t pass NaN into Prisma/getConfig.
This endpoint now uses req.query.id earlier, so a bad/missing id becomes a 500 risk.

- const workspaceId = parseInt(req.query.id as string);
+ const rawId = req.query.id
+ if (typeof rawId !== 'string' || !/^\d+$/.test(rawId)) {
+   return res.status(400).json({ success: false, error: 'Invalid workspace id' })
+ }
+ const workspaceId = parseInt(rawId, 10);

Also applies to: 24-31, 38-50

pages/api/workspace/[id]/allies/[aid]/visits/index.ts-75-76 (1)

75-76: Validate time and participants before coercion to avoid 500s / bad writes.
Right now participants.map(BigInt) will throw if body shape is wrong (or values aren’t integers), and new Date(time) can silently become Invalid Date.

- const { name, time, participants } = req.body
+ const { name, time, participants } = req.body ?? {}

  if(!name || !time) return res.status(400).json({ success: false, error: 'Missing content' })
+ const parsedTime = new Date(time)
+ if (Number.isNaN(parsedTime.getTime())) {
+   return res.status(400).json({ success: false, error: 'Invalid time' })
+ }
+ if (participants !== undefined && !Array.isArray(participants)) {
+   return res.status(400).json({ success: false, error: 'Invalid participants' })
+ }

  const visit = await prisma.allyVisit.create({
    data: {
      hostId: req.session.userid,
      allyId: req.query.aid,
      name: name,
-     time: new Date(time),
-     participants: participants ? participants.map((p: number) => BigInt(p)) : []
+     time: parsedTime,
+     participants: Array.isArray(participants)
+       ? participants.map((p: unknown) => {
+           if (typeof p !== 'number' || !Number.isSafeInteger(p)) throw new Error('Invalid participant id')
+           return BigInt(p)
+         })
+       : []
    }
  })

Also applies to: 85-87

pages/api/workspace/[id]/birthday.ts-20-43 (1)

20-43: Don’t overwrite joinDate on birthday updates; validate timezone and ensure userId type matches Prisma (BigInt vs number).
Setting joinDate: now in the update path will mutate historical join data on every birthday edit. Also, storing arbitrary timezone strings will create downstream parsing bugs, and userId: userid should match your Prisma scalar type (notably createws writes BigInt(req.session.userid)).

- const now = new Date();
+ const now = new Date();

  await prisma.workspaceMember.upsert({
    where: { workspaceGroupId_userId: { workspaceGroupId, userId: userid } },
    update: {
      birthdayDay: day,
      birthdayMonth: month,
      ...(timezone && { timezone }),
-     joinDate: now
    },
    create: {
      workspaceGroupId,
      userId: userid,
      birthdayDay: day,
      birthdayMonth: month,
      timezone: timezone || 'UTC',
      joinDate: now
    },
  });
pages/api/workspace/[id]/settings/activity/setIdleTime.ts-27-31 (1)

27-31: Handle null config to prevent TypeError.

If no existing activity config exists, getConfig returns null (per configEngine.ts). Spreading null with ...activityconfig will throw TypeError: Cannot convert undefined or null to object.

 const activityconfig = await getConfig('activity', parseInt(req.query.id as string));
 const newconfig = {
-  ...activityconfig,
+  ...(activityconfig || {}),
   idleTimeEnabled: req.body.enabled
 };
pages/api/workspace/[id]/activity/reset.ts-184-201 (1)

184-201: Bug: session.type is not selected in the participation query.

The allSessionParticipations query (lines 131-155) selects only id and sessionType from session, but line 194 accesses (p.session as any).type. This will always be undefined, causing all participation sessions to be categorized as 'other' in sessionsByType.

Add type to the session select:

 const allSessionParticipations = await prisma.sessionUser.findMany({
   where: {
     userid: userId,
     session: {
       sessionType: { workspaceGroupId },
       date: {
         gte: periodStart,
         lte: periodEnd,
       },
     },
   },
   include: {
     session: {
       select: {
         id: true,
+        type: true,
         sessionType: {
           select: {
             slots: true,
             name: true,
           },
         },
       },
     },
   },
 });

Committable suggestion skipped: line range outside the PR's diff.

pages/api/cron/birthdays.ts-38-41 (1)

38-41: Birthday “today” should be timezone-aware (otherwise you’ll fire on the wrong day)

Using server new Date() means “today” depends on where the server runs, not the workspace/member timezone. Since this PR adds timezone storage, consider computing “today” per workspace (or per member via workspaceMember.timezone) and filtering accordingly. A low-effort improvement is: store a workspace timezone config and use it here; the higher-correctness approach is per-member timezone.

Also applies to: 42-49

pages/api/workspace/[id]/settings/general/birthdays/test.ts-12-33 (1)

12-33: Align userid typing with the rest of the API (likely BigInt)

Other endpoints treat session userid as string and convert via BigInt(...). Here it's passed directly to Prisma; if your schema uses BigInt, this can silently fail auth/permission checks.

-  const userId = req.session.userid;
+  const userIdRaw = req.session.userid;
...
-  if (!userId || isNaN(workspaceId)) {
+  if (!userIdRaw || isNaN(workspaceId)) {
     return res.status(400).json({ success: false, error: "Invalid request" });
   }
+  const userId = typeof userIdRaw === "bigint" ? userIdRaw : BigInt(userIdRaw);
...
-  const user = await prisma.user.findFirst({
-    where: { userid: userId },
+  const user = await prisma.user.findFirst({
+    where: { userid: userId },
pages/api/workspace/[id]/settings/general/session-colors.ts-21-23 (1)

21-23: Missing session middleware wrapper - authentication may fail.

Unlike birthdays/hook.ts which uses withSessionRoute, this handler directly accesses (req as any).session without ensuring the session is initialized. This could result in session being undefined and authentication silently failing.

Wrap the handler with withSessionRoute for consistent session handling:

+import { withSessionRoute } from "@/lib/withSession";

-export default handler;
+export default withSessionRoute(handler);

 async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
   // ...
-    const userId = (req as any).session?.userid;
+    const userId = req.session.userid;

Also applies to: 54-57

pages/workspace/[id]/quotas.tsx-55-66 (1)

55-66: getRandomColor uses Math.random() which causes hydration mismatches.

This function is called during render for role badges (lines 635, 725), but Math.random() produces different values on server vs client, causing React hydration warnings and visual flicker.

Use the deterministic getRandomBg function (which uses a hash) instead, or pass a stable identifier.

-const getRandomColor = () => {
-  const colors = [
-    "bg-blue-500",
-    "bg-green-500",
-    "bg-yellow-500",
-    "bg-red-500",
-    "bg-purple-500",
-    "bg-pink-500",
-    "bg-indigo-500",
-  ];
-  return colors[Math.floor(Math.random() * colors.length)];
-};
+function getRoleColor(roleId: string) {
+  const colors = [
+    "bg-blue-500",
+    "bg-green-500",
+    "bg-yellow-500",
+    "bg-red-500",
+    "bg-purple-500",
+    "bg-pink-500",
+    "bg-indigo-500",
+  ];
+  let hash = 5381;
+  for (let i = 0; i < roleId.length; i++) {
+    hash = ((hash << 5) - hash) ^ roleId.charCodeAt(i);
+  }
+  return colors[(hash >>> 0) % colors.length];
+}

Then update usages:

-className={`${qr.role.color || getRandomColor()} text-white ...`}
+className={`${qr.role.color || getRoleColor(qr.role.id)} text-white ...`}
pages/api/workspace/[id]/activity/users.ts-189-212 (1)

189-212: Clamp idle-time math to prevent negative “active time” + avoid O(n²) aggregation.
If idleTimeMs > sessionDuration, effectiveTime goes negative (Line 201) and can invert rankings. Also combinedMinutes.find(...) per session is O(n²) as sessions grow.

-  const combinedMinutes: CombinedObj[] = [];
-  sessions.forEach((session) => {
+  const combinedMinutesByUserId = new Map<number, number[]>();
+  sessions.forEach((session) => {
     if (!session.endTime) return;
-    const found = combinedMinutes.find(
-      (x) => x.userId == Number(session.userId)
-    );
+    const userId = Number(session.userId);
+    const bucket = combinedMinutesByUserId.get(userId) ?? [];
     const sessionDuration =
       session.endTime.getTime() - session.startTime.getTime();
     const idleTimeMs =
       idleTimeEnabled && session.idleTime
         ? Number(session.idleTime) * 60000
         : 0;
-    const effectiveTime = sessionDuration - idleTimeMs;
-
-    if (found) {
-      found.ms.push(effectiveTime);
-    } else {
-      combinedMinutes.push({
-        userId: Number(session.userId),
-        ms: [effectiveTime],
-      });
-    }
+    const effectiveTime = Math.max(0, sessionDuration - Math.max(0, idleTimeMs));
+    bucket.push(effectiveTime);
+    combinedMinutesByUserId.set(userId, bucket);
   });
+
+  const combinedMinutes: CombinedObj[] = Array.from(
+    combinedMinutesByUserId,
+    ([userId, ms]) => ({ userId, ms })
+  );
pages/api/workspace/[id]/activity/users.ts-161-188 (1)

161-188: Dedup/cross-filter is currently broken (object identity); dedupe by userId.
indexOf(v) and x == y compare object references, so duplicates won’t be removed and inactive users won’t be removed when also active (Line 181-187).

-  activeUsers = activeUsers.filter((v, i, a) => a.indexOf(v) == i);
-  inactiveUsers = inactiveUsers.filter((v, i, a) => a.indexOf(v) == i);
-
-  inactiveUsers = inactiveUsers.filter((x) => {
-    if (activeUsers.find((y) => x == y)) return false;
-    return true;
-  });
+  const activeUserIdSet = new Set<number>();
+  activeUsers = activeUsers.filter((u) => {
+    if (activeUserIdSet.has(u.userId)) return false;
+    activeUserIdSet.add(u.userId);
+    return true;
+  });
+
+  const inactiveUserIdSet = new Set<number>();
+  inactiveUsers = inactiveUsers.filter((u) => {
+    if (activeUserIdSet.has(u.userId)) return false;
+    if (inactiveUserIdSet.has(u.userId)) return false;
+    inactiveUserIdSet.add(u.userId);
+    return true;
+  });
pages/api/workspace/[id]/activity/users.ts-59-65 (1)

59-65: Fix property name mismatch and harden config typing (leaderboardRole vs lRole).

The code reads leaderboardRole from config, but setLRole.ts stores it as lRole. This means leaderboardRank will always be undefined, preventing the rank filter from ever working. Additionally, since the config.value column is untyped JSON, values could be stored as strings (e.g., "0" or "false"), and the truthy checks on lines 120, 243, and 264 would incorrectly filter out rank 0 or treat string-valued booleans as true.

Fix by either:

  1. Aligning property names (lRoleleaderboardRole everywhere), or
  2. Reading the correct property (activityConfig?.lRole instead of leaderboardRole)

Then apply defensive coercion:

-  const leaderboardRank = activityConfig?.leaderboardRole;
+  const leaderboardRankRaw = activityConfig?.leaderboardRole;
+  const leaderboardRank =
+    leaderboardRankRaw == null ? null : Number(leaderboardRankRaw);
-  const idleTimeEnabled = activityConfig?.idleTimeEnabled ?? true;
+  const idleTimeEnabledRaw = activityConfig?.idleTimeEnabled;
+  const idleTimeEnabled =
+    typeof idleTimeEnabledRaw === "boolean"
+      ? idleTimeEnabledRaw
+      : typeof idleTimeEnabledRaw === "string"
+        ? idleTimeEnabledRaw.toLowerCase() === "true"
+        : true;

Committable suggestion skipped: line range outside the PR's diff.

🟡 Minor comments (15)
components/sticky-note-announcement.tsx-61-61 (1)

61-61: Fix grammatical error in birthday webhook description.

The verb tense is incorrect. Since this describes a current capability, it should use present tense.

Apply this diff:

-              We have added the ability for you to add a webhook which announced
+              We have added the ability for you to add a webhook which announces
               the birthdays of people on your workspace.
utils/randomText.ts-18-18 (1)

18-18: Inconsistent spacing around hyphens used as em dash replacements.

The replacement of em dashes with hyphens shows inconsistent spacing:

  • Lines 18, 21, 22, 24, 54, 60, 64, 66, 75, 77: No spaces around hyphens (e.g., "time-deck", "tasks-let's")
  • Lines 31, 44: Spaces around hyphens (e.g., "through - keep")

When using hyphens to replace em dashes (for separating clauses), standard practice is to include spaces on both sides. Without spaces, they create compound words that may be confusing.

Consider standardizing the punctuation:

Option 1 (Preferred): Use consistent spacing with hyphens:

-		`It's go time-deck those halls, ${name} 🎄`,
+		`It's go time - deck those halls, ${name} 🎄`,
-		`New festive tasks-let's sleigh them, ${name} 🛷`,
+		`New festive tasks - let's sleigh them, ${name} 🛷`,
-		`Winter sun & Christmas lights-glow on, ${name} 🎄`,
+		`Winter sun & Christmas lights - glow on, ${name} 🎄`,
-		`Don't just wake-make spirits bright, ${name} 🔔`
+		`Don't just wake - make spirits bright, ${name} 🔔`

Option 2: Use proper em dashes (—) if the platform supports Unicode:

-		`It's go time-deck those halls, ${name} 🎄`,
+		`It's go time—deck those halls, ${name} 🎄`,

Apply similar changes to the other affected lines (54, 60, 64, 66, 75, 77) to maintain consistency throughout the file.

Also applies to: 21-22, 24-24, 31-31, 41-41, 44-44, 54-54, 60-60, 64-64, 66-66, 75-75, 77-77

components/sessionpatterns.tsx-9-9 (1)

9-9: Missing null check for session.date could cause runtime error.

If session.date is undefined or null, new Date(session.date) will create an "Invalid Date" object, causing getDay() to return NaN and subsequent operations to fail silently.

Add a defensive check:

+if (!session?.date) {
+  return null;
+}
+
 const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
 const sessionDate = new Date(session.date);
+if (isNaN(sessionDate.getTime())) {
+  return null;
+}

Also applies to: 26-26

pages/workspace/[id]/sessions/edit/[sid].tsx-378-385 (1)

378-385: Delete without confirmation when scope is pre-set could be unexpected.

When coming from the pattern dialog with a scope already set, clicking Delete immediately executes the deletion without showing the confirmation modal. This could lead to accidental deletions.

Consider always showing a confirmation for destructive actions:

 onPress={() => {
-  if (scope) {
-    deleteSession();
-  } else {
-    setShowDeleteModal(true);
-  }
+  setShowDeleteModal(true);
 }}

Alternatively, show a simpler confirmation that mentions the pre-selected scope.

pages/workspace/[id]/sessions/edit/[sid].tsx-208-208 (1)

208-208: Validate scope query parameter.

The scope parameter from router.query is used directly without validation. A malicious or malformed value could cause unexpected behavior.

-const { scope } = router.query;
+const rawScope = router.query.scope;
+const scope = typeof rawScope === 'string' && ['single', 'future', 'all'].includes(rawScope) 
+  ? rawScope as 'single' | 'future' | 'all'
+  : undefined;
components/profile/activityoverview.tsx-86-87 (1)

86-87: notices prop is declared but never used.

The notices prop is defined in the Props type but is not destructured or used in the component.

Either use the notices prop or remove it from the type definition:

 type Props = {
   data: any;
   displayMinutes: number;
   messages: number;
   idleTime: number;
   sessionsHosted: number;
   sessionsAttended: number;
   idleTimeEnabled: boolean;
-  notices: inactivityNotice[];
   adjustments: any[];
   sessions: (ActivitySession & {
components/profile/activityoverview.tsx-186-186 (1)

186-186: Non-2xx status codes won't reach the conditional check.

Axios throws an error for non-2xx responses by default, so status !== 200 will never be true here. The error will be caught in the catch block instead.

Remove the redundant check or use validateStatus in axios config:

-if (status !== 200) return toast.error("Could not fetch session.");
components/settings/permissions/roles.tsx-72-78 (1)

72-78: Bug: findIndex returns -1 when not found, not null.

The guard if (index === null) return will never trigger because findIndex returns -1 when the element is not found. This mirrors a pre-existing bug in updateRole, togglePermission, and toggleGroupRole. Note that saveRole correctly checks index === -1.

 const updateRoleColor = async (color: string, id: string) => {
   const index = roles.findIndex((role: any) => role.id === id);
-  if (index === null) return;
+  if (index === -1) return;
   const rroles = Object.assign(([] as typeof roles), roles);
   rroles[index].color = color;
   setRoles(rroles);
 };
pages/api/cron/birthdays.ts-68-119 (1)

68-119: Fix 1-second delay to avoid sleeping after the last send

Current logic sleeps even after the final birthday in the workspace. Use an index.

-        for (const member of membersWithBirthdays) {
+        for (let i = 0; i < membersWithBirthdays.length; i++) {
+          const member = membersWithBirthdays[i];
           ...
-          if (membersWithBirthdays.length > 1) {
+          if (i < membersWithBirthdays.length - 1) {
             await new Promise((resolve) => setTimeout(resolve, 1000));
           }
         }
components/settings/general/leaderboard.tsx-39-65 (1)

39-65: Missing error handling for API call.

The updateLeaderboard function doesn't wrap the axios.patch call in a try/catch block, unlike handleStyleChange in LeaderboardStyleSelector. If the request fails, it will throw an unhandled promise rejection.

   const updateLeaderboard = async (enabled?: boolean, style?: string) => {
+    try {
       const res = await axios.patch(
         `/api/workspace/${workspace.groupId}/settings/general/leaderboard`,
         {
           enabled:
             enabled !== undefined
               ? enabled
               : workspace.settings.leaderboardEnabled,
           style: style || leaderboardStyle,
         }
       );
       if (res.status === 200) {
         // ... existing success handling
       } else {
         triggerToast.error("Failed to update leaderboard.");
       }
+    } catch (error) {
+      triggerToast.error("Failed to update leaderboard.");
+    }
   };
pages/workspace/[id]/activity/index.tsx-125-156 (1)

125-156: Switch case declarations without block scope.

Variable declarations (const hostedCount, let loggedCount) inside switch cases without block scoping can be accessed by fall-through cases, leading to potential bugs.

Wrap each case body in a block:

             case "sessions_hosted":
+              {
                 const hostedCount =
                   quota.sessionType && quota.sessionType !== "all"
                     ? profileData.sessionsLogged?.byType[quota.sessionType] || 0
                     : sessionsHosted;
                 currentValue = hostedCount;
                 percentage = (hostedCount / quota.value) * 100;
-                break;
+                break;
+              }
             case "sessions_logged":
+              {
                 let loggedCount = 0;
                 // ... rest of logic
                 currentValue = loggedCount;
                 percentage = (loggedCount / quota.value) * 100;
-                break;
+                break;
+              }
pages/workspace/[id]/views.tsx-279-293 (1)

279-293: Potential issue with session type access.

The code accesses (p.session as any).type on line 284, but the session included from allSessionParticipations only selects id and sessionType (lines 233-240). The type field may not be available on this object.

Verify that p.session.type exists. Based on the include query, it appears only sessionType (the relation) is selected, not the type field. You may need to update the include:

         include: {
           session: {
             select: {
               id: true,
+              type: true,
               sessionType: {
                 select: {
                   slots: true,
                   name: true,
                 },
               },
             },
           },
         },

Committable suggestion skipped: line range outside the PR's diff.

pages/workspace/[id]/quotas.tsx-398-398 (1)

398-398: Missing dependency in useMemo hook.

The useMemo has an empty dependency array but uses login.displayname. This means the value won't update if the display name changes.

-  const text = useMemo(() => randomText(login.displayname), []);
+  const text = useMemo(() => randomText(login.displayname), [login.displayname]);
pages/workspace/[id]/quotas.tsx-285-316 (1)

285-316: Wrap switch case declarations in blocks to prevent accidental access.

The static analysis tool correctly identifies that hostedCount (line 291) and loggedCount (line 306) are declared without block scoping, making them technically accessible from other switch clauses.

       case "sessions_hosted":
-        const hostedCount = quota.sessionType && quota.sessionType !== "all"
+      {
+        const hostedCount = quota.sessionType && quota.sessionType !== "all"
           ? sessionsLogged.filter(
               (s) =>
                 s.ownerId?.toString() === userId &&
                 s.type === quota.sessionType
             ).length
           : sessionsHosted;
         currentValue = hostedCount;
         percentage = (hostedCount / quota.value) * 100;
         break;
+      }
       case "sessions_attended":
         currentValue = sessionsAttended;
         percentage = (sessionsAttended / quota.value) * 100;
         break;
       case "sessions_logged":
-        const loggedCount = quota.sessionType && quota.sessionType !== "all"
+      {
+        const loggedCount = quota.sessionType && quota.sessionType !== "all"
           ? sessionsLogged.filter((s) => s.type === quota.sessionType).length
           : totalSessionsLogged;
         currentValue = loggedCount;
         percentage = (loggedCount / quota.value) * 100;
         break;
+      }

Committable suggestion skipped: line range outside the PR's diff.

pages/workspace/[id]/profile/[uid].tsx-407-426 (1)

407-426: Add pagination to the allMembers query to handle workspaces with many members.

The workspaceMember.findMany() query at lines 407-426 fetches all workspace members without pagination. For workspaces with large member counts, this causes unnecessary data transfer and renders many DOM elements in the line manager selection dropdown. Add take and skip parameters, similar to pagination patterns used elsewhere in the codebase (e.g., /api/workspace/[id]/activity/history.ts).

🧹 Nitpick comments (38)
pages/api/admin/instance-config.ts (1)

47-55: Validate / coerce request body types before writing to DB.

req.body values may be non-strings; add minimal validation (e.g., ensure typeof value === 'string', trim, enforce max length; reject invalid URL for redirect URI).

Also applies to: 57-65

components/settings/general/color.tsx (1)

45-48: Prefer useCallback + depend on the callback (avoids stale closure / exhaustive-deps noise).
Right now the effect depends on workspace?.groupId but calls a recreated loadSessionColors that closes over workspace. Making the callback explicit tightens correctness and intent.

-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";

-  useEffect(() => {
-    loadSessionColors();
-  }, [workspace?.groupId]);
-
-  const loadSessionColors = async () => {
-    if (!workspace?.groupId) return;
+  const loadSessionColors = useCallback(async () => {
+    if (!workspace?.groupId) return;

     try {
       setIsLoadingSessionColors(true);
       const response = await axios.get(
         `/api/workspace/${workspace.groupId}/settings/general/session-colors`
       );
       if (response.data.success && response.data.colors) {
         setSessionColors(response.data.colors);
       }
     } catch (error) {
       console.error("Failed to load session colors:", error);
     } finally {
       setIsLoadingSessionColors(false);
     }
-  };
+  }, [workspace?.groupId]);
+
+  useEffect(() => {
+    loadSessionColors();
+  }, [loadSessionColors]);
pages/api/workspace/[id]/home/upcoming.ts (1)

34-34: Consider removing redundant filter.

The database query already filters for non-null birthdayDay and birthdayMonth values (lines 22-25), making this in-memory filter redundant. Removing it would improve code clarity.

Apply this diff:

 const filtered = memberBirthdays
-  .filter((m: typeof memberBirthdays[number]) => (m.user.birthdayDay ?? 0) > 0 && (m.user.birthdayMonth ?? 0) > 0)
   .map((m: typeof memberBirthdays[number]) => {
pages/workspace/[id]/wall.tsx (1)

413-429: The permission logic is correct.

The delete authorization correctly allows both post authors and users with manage_wall permission to delete posts. The String() conversion provides good type safety.

However, the IIFE pattern adds unnecessary complexity to the JSX. Consider this refactor for better readability:

-                    {(() => {
-                      const isAuthor = String(post.authorId) === String(login.userId);
-                      const hasManageWall = userPermissions.includes("manage_wall");
-                      const canDelete = isAuthor || hasManageWall;
-                      
-                      return canDelete ? (
-                        <button
-                          onClick={() => {
-                            setPostToDelete(post.id);
-                            setShowDeleteModal(true);
-                          }}
-                          className="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
-                        >
-                          <IconTrash size={18} />
-                        </button>
-                      ) : null;
-                    })()}
+                    {(String(post.authorId) === String(login.userId) || 
+                      userPermissions.includes("manage_wall")) && (
+                      <button
+                        onClick={() => {
+                          setPostToDelete(post.id);
+                          setShowDeleteModal(true);
+                        }}
+                        className="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
+                      >
+                        <IconTrash size={18} />
+                      </button>
+                    )}

Or extract to a helper function above the return statement:

const canDeletePost = (post: any) => {
  const isAuthor = String(post.authorId) === String(login.userId);
  const hasManageWall = userPermissions.includes("manage_wall");
  return isAuthor || hasManageWall;
};
pages/api/workspace/[id]/settings/roles/[roleid]/update.ts (1)

35-36: Prefer nullish coalescing operator (??) over logical OR (||).

Using || treats all falsy values (including empty string "" and 0) as triggers for the default. Use ?? to only default on null or undefined.

-			groupRoles: req.body.groupRoles || [],
-			color: req.body.color || null
+			groupRoles: req.body.groupRoles ?? [],
+			color: req.body.color ?? null
pages/api/workspace/[id]/settings/general/leaderboard.ts (1)

29-29: Prefer nullish coalescing operator (??) over logical OR (||).

Using || treats all falsy values (including empty string) as triggers for the default 'list'. Use ?? to only default on null or undefined.

-			const after = { enabled: req.body.enabled, style: req.body.style || 'list' };
+			const after = { enabled: req.body.enabled, style: req.body.style ?? 'list' };
pages/api/workspace/[id]/activity/quotas/new.ts (1)

34-48: Consider adding type safety and validation for sessionType.

Using any type for quotaData bypasses TypeScript's type checking. Additionally, sessionType is conditionally included without validation.

-	const quotaData: any = {
+	const quotaData = {
 		name,
 		type,
 		value: parseInt(value),
 		workspaceGroupId: parseInt(req.query.id as string),
 		description: description || null,
+		...(sessionType && { sessionType: String(sessionType) }),
 	};
-
-	if (sessionType) {
-		quotaData.sessionType = sessionType;
-	}

 	const quota = await prisma.quota.create({
 	  data: quotaData
 	});

If sessionType should only accept specific values, consider validating it against allowed session types before saving.

pages/api/workspace/[id]/sessions/create-scheduled.ts (1)

99-106: Consider wrapping schedule and session creation in a transaction.

If an error occurs after patternSchedule is created but before sessions are created (e.g., at line 167), an orphan schedule record will remain in the database. Using a Prisma transaction ensures atomicity.

+    const result = await prisma.$transaction(async (tx) => {
       const patternSchedule = await prisma.schedule.create({
         data: {
           Days: selectedDays,
           Hour: hours,
           Minute: minutes,
           sessionTypeId: sessionType.id,
         },
       });
+      // ... rest of session creation logic using tx instead of prisma
+      return { patternSchedule, createdSessions };
+    });

This prevents orphan schedule records if subsequent operations fail.

components/profile/activitysubsection.tsx (1)

1-1: Remove unused Fragment import.

Fragment is imported but not used in this component.

-import React, { Fragment } from "react";
+import React from "react";
components/settings/general/bhooks.tsx (3)

7-7: Remove unused import IconX.

-import { IconGift, IconCheck, IconX } from "@tabler/icons-react";
+import { IconGift, IconCheck } from "@tabler/icons-react";

10-10: Remove unused setWorkspace from destructuring.

-  const [workspace, setWorkspace] = useRecoilState(workspacestate);
+  const [workspace] = useRecoilState(workspacestate);

54-78: Add URL validation before sending test webhook.

While the server should validate the URL, adding client-side validation improves UX by catching invalid URLs early.

   const handleTest = async () => {
     if (!webhookUrl) {
       toast.error("Please enter a webhook URL first");
       return;
     }

+    if (!webhookUrl.startsWith("https://discord.com/api/webhooks/")) {
+      toast.error("Please enter a valid Discord webhook URL");
+      return;
+    }
+
     setTesting(true);
pages/api/workspace/[id]/sessions/[sid]/update-pattern.ts (1)

94-106: Consider using a transaction for bulk updates.

When updating multiple sessions with time changes, each session is updated individually in a loop. For large patterns, this could be slow and leaves the database in an inconsistent state if an error occurs mid-update.

+        await prisma.$transaction(async (tx) => {
           for (const session of filteredSessions) {
             const sessionDate = new Date(session.date);
             sessionDate.setHours(hours, minutes, 0, 0);
             
             const updateData: any = { date: sessionDate };
             if (newDuration !== undefined) updateData.duration = newDuration;
             if (newName !== undefined) updateData.name = newName;
             
-            await prisma.session.update({
+            await tx.session.update({
               where: { id: session.id },
               data: updateData,
             });
           }
+        });

The same pattern applies to lines 151-163 in the "all" scope.

components/profile/activityoverview.tsx (1)

76-76: Excessive use of any type reduces type safety.

Multiple instances of any type are used where more specific types would improve maintainability and catch errors at compile time.

Consider defining proper interfaces for:

  • TimelineItem adjustment type (Line 76)
  • Props.data (Line 79)
  • Props.adjustments (Line 87)
  • dialogData state (Line 125)

Also applies to: 79-79, 87-87, 125-125

components/sessionpatterns.tsx (1)

5-10: Consider typing session prop more strictly.

Using any for the session prop reduces type safety. At minimum, the expected shape should be documented.

+interface SessionData {
+  date: string | Date;
+  name: string;
+  scheduleId?: string | null;
+}
+
 interface PatternEditDialogProps {
   isOpen: boolean;
   onClose: () => void;
   onConfirm: (scope: "single" | "future" | "all") => void;
-  session: any;
+  session: SessionData;
 }
pages/workspace/[id]/sessions.tsx (1)

33-60: Duplicated BG_COLORS array and getRandomBg function.

This code is duplicated in activityoverview.tsx and edit/[sid].tsx. Consider extracting to a shared utility.

Create a shared utility file:

// utils/avatarColors.ts
export const BG_COLORS = [
  "bg-red-200",
  "bg-green-200",
  // ... rest of colors
];

export function getRandomBg(userid: string, username?: string): string {
  const key = `${userid ?? ""}:${username ?? ""}`;
  let hash = 5381;
  for (let i = 0; i < key.length; i++) {
    hash = ((hash << 5) - hash) ^ key.charCodeAt(i);
  }
  const index = (hash >>> 0) % BG_COLORS.length;
  return BG_COLORS[index];
}
pages/api/createws.ts (1)

92-110: Consider making the creator-member insert idempotent (retry/double-submit safe).
If the POST is retried after partial failure, tx.workspaceMember.create could throw on a unique constraint (if one exists).

pages/workspace/[id]/index.tsx (1)

148-159: Consider preventing redundant timezone POST requests.

This useEffect will fire on every render/remount when both workspace.groupId and login.userId are truthy. Without deduplication, multiple requests may be sent unnecessarily. Consider using a ref to track if the timezone was already submitted this session.

+const timezoneSubmittedRef = useRef(false);
+
 useEffect(() => {
-  if (workspace?.groupId && login?.userId) {
+  if (workspace?.groupId && login?.userId && !timezoneSubmittedRef.current) {
+    timezoneSubmittedRef.current = true;
     const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
     fetch(`/api/workspace/${workspace.groupId}/timezone`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({ timezone: detectedTimezone }),
     }).catch(() => {
       // no errors its gonan work amazing trust - famous last words
     });
   }
 }, [workspace?.groupId, login?.userId])
pages/api/workspace/[id]/profile/[uid]/member-info.ts (1)

39-47: Redundant conditional and missing input validation.

The ternary birthdayDay !== undefined ? birthdayDay : undefined is a no-op. Additionally, birthdayDay and birthdayMonth should be validated as integers within valid ranges (day: 1-31, month: 1-12) to prevent invalid data.

 await prisma.user.update({
   where: {
     userid: BigInt(userId),
   },
   data: {
-    birthdayDay: birthdayDay !== undefined ? birthdayDay : undefined,
-    birthdayMonth: birthdayMonth !== undefined ? birthdayMonth : undefined,
+    ...(birthdayDay !== undefined && { birthdayDay: Number(birthdayDay) }),
+    ...(birthdayMonth !== undefined && { birthdayMonth: Number(birthdayMonth) }),
   },
 });
pages/api/cron/birthdays.ts (1)

13-16: Harden cron auth header parsing + avoid timing leaks

req.headers["x-cron-secret"] can be string | string[] | undefined, so the current compare can fail unexpectedly. Also consider constant-time compare for a shared secret.

+import crypto from "crypto";
 ...
-  const cronSecret = req.headers["x-cron-secret"];
-  if (cronSecret !== process.env.CRON_SECRET) {
+  const cronSecretHeader = req.headers["x-cron-secret"];
+  const cronSecret =
+    typeof cronSecretHeader === "string"
+      ? cronSecretHeader
+      : Array.isArray(cronSecretHeader)
+        ? cronSecretHeader[0]
+        : undefined;
+
+  const expected = process.env.CRON_SECRET;
+  if (!cronSecret || !expected) {
     return res.status(401).json({ error: "Unauthorized" });
   }
+
+  const a = Buffer.from(cronSecret);
+  const b = Buffer.from(expected);
+  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
+    return res.status(401).json({ error: "Unauthorized" });
+  }
pages/api/workspace/[id]/profile/[uid].ts (1)

235-260: Use count() instead of findMany() when only returning allianceVisitsCount

This avoids loading rows + joins just to do .length.

-    const allianceVisits = await prisma.allyVisit.findMany({
-      where: {
-        ally: {
-          workspaceGroupId: workspaceGroupId,
-        },
-        time: {
-          gte: startDate,
-          lte: currentDate,
-        },
-        OR: [
-          { hostId: userId },
-          { participants: { has: userId } }
-        ]
-      },
-      include: {
-        ally: {
-          select: {
-            id: true,
-            name: true,
-          }
-        }
-      }
-    });
-
-    const allianceVisitsCount = allianceVisits.length;
+    const allianceVisitsCount = await prisma.allyVisit.count({
+      where: {
+        ally: { workspaceGroupId },
+        time: { gte: startDate, lte: currentDate },
+        OR: [{ hostId: userId }, { participants: { has: userId } }],
+      },
+    });
components/profile/sessions.tsx (1)

8-30: Consider responsive layout + avoid unused destructured props if linted

If you run no-unused-vars/eslint, destructuring sessions, notices, adjustments, avatar, idleTimeEnabled without using them may warn. Also, grid-cols-2 could be cramped on mobile.

-      <div className="grid grid-cols-2 gap-4 mb-6">
+      <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">

Also applies to: 34-78

pages/api/workspace/[id]/timezone.ts (1)

20-22: Validate timezone as an IANA time zone (not just a string)

A simple server-side check:

   if (!timezone || typeof timezone !== "string") {
     return res.status(400).json({ error: "Invalid timezone" });
   }
+  try {
+    Intl.DateTimeFormat("en-US", { timeZone: timezone }).format(new Date());
+  } catch {
+    return res.status(400).json({ error: "Invalid timezone" });
+  }
pages/api/workspace/[id]/settings/general/birthdays/test.ts (1)

47-54: Prefer URL parsing + host allowlist; also use HTTPS for avatar_url

Regex-only validation is easier to bypass than parsing+allowlist, and avatar_url should be https.

-    if (!url.match(/^https:\/\/discord\.com\/api\/webhooks\/\d+\/.+/)) {
+    let parsed: URL;
+    try { parsed = new URL(url); } catch { parsed = null as any; }
+    const isAllowed =
+      parsed &&
+      parsed.protocol === "https:" &&
+      (parsed.hostname === "discord.com" || parsed.hostname === "canary.discord.com") &&
+      /^\/api\/webhooks\/\d+\/.+/.test(parsed.pathname);
+    if (!isAllowed) {
       return res.status(400).json({
         success: false,
         error: "Invalid Discord webhook URL format",
       });
     }
...
-      avatar_url: `http://cdn.planetaryapp.us/brand/planetary.png`,
+      avatar_url: `https://cdn.planetaryapp.us/brand/planetary.png`,

Also applies to: 82-94

pages/api/workspace/[id]/settings/general/birthdays/hook.ts (2)

25-27: Permission check relies on array ordering assumption.

The code assumes the first role after ordering by isOwnerRole: "desc" is the most privileged. This works correctly, but if a user has multiple roles with different permissions, only the first role's permissions are checked.

Consider checking if any role has admin permission for more comprehensive authorization:

-  const userRole = user?.roles?.[0];
-  const hasAdminPermission =
-    userRole?.permissions?.includes("admin") || userRole?.isOwnerRole;
+  const hasAdminPermission = user?.roles?.some(
+    (role) => role.permissions?.includes("admin") || role.isOwnerRole
+  );

58-63: Webhook URL regex may not cover all valid Discord webhook formats.

The current regex /^https:\/\/discord\.com\/api\/webhooks\/\d+\/.+/ is reasonable but may miss webhooks using discordapp.com or ptb.discord.com/canary.discord.com subdomains.

Consider a more comprehensive pattern:

-      if (enabled && url && !url.match(/^https:\/\/discord\.com\/api\/webhooks\/\d+\/.+/)) {
+      if (enabled && url && !url.match(/^https:\/\/(discord\.com|discordapp\.com|ptb\.discord\.com|canary\.discord\.com)\/api\/webhooks\/\d+\/.+/)) {
components/settings/general/leaderboard.tsx (1)

21-37: Duplicate fetch logic between components.

Both Leaderboard and LeaderboardStyleSelector have nearly identical useEffect hooks for fetching leaderboard style. This violates DRY principles and can lead to inconsistent behavior if modified independently.

Consider extracting the shared logic into a custom hook:

function useLeaderboardStyle(workspaceId: number | undefined) {
  const [style, setStyle] = useState<"list" | "podium">("list");
  
  useEffect(() => {
    if (!workspaceId) return;
    axios.get(`/api/workspace/${workspaceId}/settings/general/leaderboard`)
      .then((res) => {
        if (res.data?.value?.style) setStyle(res.data.value.style);
      })
      .catch((error) => console.error("Failed to fetch leaderboard style:", error));
  }, [workspaceId]);
  
  return [style, setStyle] as const;
}

Also applies to: 107-123

pages/api/workspace/[id]/settings/general/session-colors.ts (2)

69-70: Permission check pattern identical to birthdays/hook.ts.

Same observation as in hook.ts - only the first role's permissions are checked. Consider checking if any role has admin permission.

-    const userRole = user?.roles?.[0];
-    const hasAdminPermission = userRole?.permissions?.includes('admin') || userRole?.isOwnerRole;
+    const hasAdminPermission = user?.roles?.some(
+      (role) => role.permissions?.includes('admin') || role.isOwnerRole
+    );

99-103: Audit logging errors are silently swallowed.

The empty catch block suppresses any audit logging failures without logging them. While audit failures shouldn't break the request, silent failures make debugging difficult.

       try {
         const actorId = (req as any).session?.userid ? Number((req as any).session.userid) : null;
         const actorUsername = actorId ? await getUsername(actorId).catch(() => null) : null;
         await logAudit(workspaceId, actorId, 'settings.general.sessionColors.update', 'sessionColors', { before, after: colors, actorUsername });
-      } catch (e) {}
+      } catch (e) {
+        console.error("Failed to log audit:", e);
+      }
pages/workspace/[id]/activity/index.tsx (1)

222-253: Dual polling intervals may increase server load.

User data polls every 30 seconds (line 215-218) while leaderboard data polls every 60 seconds (line 250). With many users, this could create significant load.

Consider:

  1. Aligning the intervals or combining the fetches
  2. Using a visibility-based approach (pause polling when tab is not visible)
  3. Using WebSockets or server-sent events for real-time updates
components/profile/information.tsx (1)

311-322: Consider validating day based on selected month.

The day selector always shows 31 days regardless of the selected month. While server-side validation can catch invalid dates like February 30th, consider adjusting the available days based on the selected month for better UX.

+                    const daysInMonth = birthdayMonth 
+                      ? new Date(2000, parseInt(birthdayMonth as string), 0).getDate() 
+                      : 31;
                     <select
                       value={birthdayDay}
                       onChange={(e) => setBirthdayDay(e.target.value)}
                       className="flex-1 px-2 py-1 text-sm rounded-lg bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-600 text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#ff0099]/50"
                     >
                       <option value="">Day</option>
-                      {Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
+                      {Array.from({ length: daysInMonth }, (_, i) => i + 1).map((day) => (
                         <option key={day} value={day}>
                           {day}
                         </option>
                       ))}
                     </select>
pages/workspace/[id]/views.tsx (1)

166-409: Consider extracting duplicated user computation logic.

The logic for computing user activity data (minutes, idle time, sessions, alliance visits, etc.) is largely duplicated between the main user loop (lines 166-409) and the fallback loop for users not in computedUsers (lines 417-586). This increases maintenance burden and risk of inconsistencies.

Extract the shared computation logic into a helper function to reduce duplication and ensure consistent behavior:

async function computeUserActivity(
  userId: bigint,
  allActivity: any[],
  workspaceGroupId: number,
  startDate: Date,
  currentDate: Date,
  idleTimeEnabled: boolean
) {
  // ... shared computation logic
  return { minutes, idleMinutes, sessionsHosted, sessionsAttended, allianceVisits, ... };
}

Also applies to: 417-586

components/profile/quotas.tsx (2)

21-43: getQuotaPercentage can return undefined for unknown quota types.

The switch statement lacks a default case, so if quota.type doesn't match any case and quota.percentage is undefined, the function returns undefined. This could cause the progress bar to display incorrectly (though the || 0 fallback on line 127 mitigates runtime issues).

Additionally, Quota | any defeats type safety—consider using a discriminated union or the augmented type from Props.

-  const getQuotaPercentage = (quota: Quota | any) => {
+  const getQuotaPercentage = (quota: Quota & { currentValue?: number; percentage?: number }) => {
     if (quota.percentage !== undefined) {
       return quota.percentage;
     }
     switch (quota.type) {
       case "mins": {
         return (displayMinutes / quota.value) * 100;
       }
       case "sessions_hosted": {
         return (sessionsHosted / quota.value) * 100;
       }
       case "sessions_attended": {
         return (sessionsAttended / quota.value) * 100;
       }
       case "sessions_logged": {
         const totalLogged = sessionsHosted + sessionsAttended;
         return (totalLogged / quota.value) * 100;
       }
       case "alliance_visits": {
         return (allianceVisits / quota.value) * 100;
       }
+      default:
+        return 0;
     }
   };

45-73: getQuotaProgress can also return undefined for unknown quota types.

Similar to getQuotaPercentage, this function lacks a default case. If the tooltip receives undefined, it may display poorly.

     case "alliance_visits": {
       return `${allianceVisits} / ${quota.value} alliance visits`;
     }
+    default:
+      return `${quota.currentValue ?? 0} / ${quota.value}`;
   }
 };
pages/workspace/[id]/quotas.tsx (2)

140-144: Hardcoded fallback date should be documented or made configurable.

The hardcoded date 2024-11-30T00:00:00Z appears in multiple places (here and in the profile page). Consider extracting this to a shared constant with documentation explaining its purpose.

-    // Use last reset date, or November 30th 2024, whichever is more recent
-    const nov30 = new Date("2024-11-30T00:00:00Z");
+    // Fallback date for activity tracking start - represents the launch date of the quota system
+    const QUOTA_SYSTEM_LAUNCH_DATE = new Date("2024-11-30T00:00:00Z");
+    const startDate = lastReset?.resetAt 
+      ? (lastReset.resetAt > QUOTA_SYSTEM_LAUNCH_DATE ? lastReset.resetAt : QUOTA_SYSTEM_LAUNCH_DATE)
+      : QUOTA_SYSTEM_LAUNCH_DATE;

482-493: Consider adding a confirmation dialog before quota deletion.

The delete action immediately calls the API without user confirmation. For destructive operations, a confirmation dialog prevents accidental data loss.

pages/workspace/[id]/profile/[uid].tsx (1)

795-849: Consider extracting the timezone clock to a separate component.

The IIFE pattern (() => { ... })() in JSX works but reduces readability. Extracting this to a dedicated component would improve maintainability and allow for memoization.

Also, parseInt(userHour) on line 801 could return NaN if the locale format differs unexpectedly. Consider adding a fallback.

+// Extract as a separate component or helper
+function TimezoneClock({ timezone }: { timezone: string }) {
+  const [currentTime, setCurrentTime] = useState(new Date());
+  
+  useEffect(() => {
+    const timer = setInterval(() => setCurrentTime(new Date()), 1000);
+    return () => clearInterval(timer);
+  }, []);
+  
+  const hour = parseInt(
+    currentTime.toLocaleString('en-US', { timeZone: timezone, hour: 'numeric', hour12: false }),
+    10
+  );
+  const isDay = !isNaN(hour) && hour >= 6 && hour < 18;
+  // ... rest of render
+}
pages/api/workspace/[id]/activity/users.ts (1)

66-104: Use a single “now” Date and reuse workspaceId to avoid tiny TOCTOU inconsistencies.
You call new Date() multiple times inside the inactivity query (Line 88-92) and re-parse req.query.id (Line 93, 135). This can produce small mismatches and is harder to reason about.

-  const currentDate = new Date();
+  const currentDate = new Date();
+  const nowDate = currentDate;
...
-      endTime: {
-        gt: new Date(),
-      },
-      startTime: {
-        lt: new Date(),
-      },
-      workspaceGroupId: parseInt(req.query.id as string),
+      endTime: { gt: nowDate },
+      startTime: { lt: nowDate },
+      workspaceGroupId: workspaceId,

Comment on lines +159 to +183
const handleSave = async () => {
if (!workspaceId) return;
setLoading(true);
try {
await axios.patch(
`/api/workspace/${workspaceId}/profile/${user.userid}/member-info`,
{
department,
lineManagerId: selectedManager?.userid || null,
timezone: selectedTimezone || null,
birthdayDay: birthdayDay ? parseInt(birthdayDay as string) : null,
birthdayMonth: birthdayMonth ? parseInt(birthdayMonth as string) : null,
discordId: discordId || null,
}
);

toast.success("Information updated!");
setEditing(false);
router.replace(router.asPath);
} catch (e) {
toast.error("Failed to update information");
} finally {
setLoading(false);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 184


🏁 Script executed:

# Search for the member-info API endpoint
rg -n "member-info" --type ts --type tsx

Repository: PlanetaryOrbit/orbit

Length of output: 91


🏁 Script executed:

# Search for workspace API routes to find authorization patterns
fd -t f -e ts -e tsx "api" | grep -E "(workspace|profile)" | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for the specific API route
rg -n "/api/workspace.*profile.*member-info" --type ts --type tsx

Repository: PlanetaryOrbit/orbit

Length of output: 91


🏁 Script executed:

find . -name "*.ts" -o -name "*.js" | grep -i "api" | head -30

Repository: PlanetaryOrbit/orbit

Length of output: 1081


🏁 Script executed:

# Search for member-info in any file type
grep -r "member-info" . --include="*.ts" --include="*.js" 2>/dev/null | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for workspace API routes directory structure
find . -type d -name "api" 2>/dev/null | head -10

Repository: PlanetaryOrbit/orbit

Length of output: 101


🏁 Script executed:

# Search for handleSave and related authorization patterns
grep -r "workspace.*member-info\|member-info.*authorization\|validateUser\|checkPermission" . --include="*.ts" --include="*.js" 2>/dev/null | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Find workspace API routes
find ./pages/api -name "*workspace*" -type f 2>/dev/null | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 93


🏁 Script executed:

# Search for profile endpoint
find ./pages/api -name "*profile*" -type f 2>/dev/null | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for files in workspace directory structure
find ./pages/api -type d 2>/dev/null | grep workspace

Repository: PlanetaryOrbit/orbit

Length of output: 2215


🏁 Script executed:

# List all files in pages/api directory to understand structure
ls -la ./pages/api/ 2>/dev/null | head -40

Repository: PlanetaryOrbit/orbit

Length of output: 1255


🏁 Script executed:

# List files in the profile directory
find ./pages/api/workspace/[id]/profile -type f 2>/dev/null

Repository: PlanetaryOrbit/orbit

Length of output: 163


🏁 Script executed:

# List files in the profile/[uid] directory
find "./pages/api/workspace" -path "*/profile/\[uid\]*" -type f 2>/dev/null

Repository: PlanetaryOrbit/orbit

Length of output: 163


🏁 Script executed:

# Try to list all files under workspace/[id]/profile
ls -la ./pages/api/workspace/[id]/profile/ 2>/dev/null

Repository: PlanetaryOrbit/orbit

Length of output: 292


🏁 Script executed:

# Search for member-info more broadly
rg "member-info" . 2>/dev/null | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 176


🏁 Script executed:

# Read the member-info API endpoint to check authorization
cat "./pages/api/workspace/[id]/profile/[uid]/member-info.ts"

Repository: PlanetaryOrbit/orbit

Length of output: 1936


🏁 Script executed:

# Also check the parent profile endpoint for any shared authorization logic
cat "./pages/api/workspace/[id]/profile/[uid].ts" | head -100

Repository: PlanetaryOrbit/orbit

Length of output: 2638


🏁 Script executed:

# Check what withSessionRoute does - is there any authorization built in?
cat ./lib/withSession.ts 2>/dev/null | head -50

Repository: PlanetaryOrbit/orbit

Length of output: 1784


🏁 Script executed:

# Also check if there's any middleware that might protect this endpoint
rg -A 5 "withSessionRoute.*member-info" . 2>/dev/null

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for any authorization checks in the profile directory
rg "withPermissionCheck\|checkAuthorization\|isAuthorized" "./pages/api/workspace/[id]/profile/" 2>/dev/null

Repository: PlanetaryOrbit/orbit

Length of output: 46


Add authorization validation to the member-info endpoint.

The PATCH endpoint at /pages/api/workspace/[id]/profile/[uid]/member-info.ts is missing critical authorization checks. Currently it only uses withSessionRoute (which verifies the user is logged in) but does not verify:

  • That the requesting user is editing their own profile or has permission to edit others
  • That the user belongs to the workspace
  • That the workspace and user IDs form a valid relationship

Any authenticated user can modify any user's profile in any workspace. The parent endpoint [uid].ts demonstrates the correct authorization pattern with isOwnProfile checks and role-based permissions—apply the same approach to the member-info endpoint.

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 163-173: Server-side request forgery
The URL of this request depends on a user-provided value.

🤖 Prompt for AI Agents
In components/profile/information.tsx around lines 159 to 183, the client calls
the PATCH member-info endpoint without ensuring the server enforces
authorization; update the server handler at
pages/api/workspace/[id]/profile/[uid]/member-info.ts to mirror the parent
[uid].ts checks: extract session user, verify the session user belongs to the
workspace, confirm the workspaceId and target uid form a valid relationship,
allow the update only if isOwnProfile(sessionUser, uid) or the sessionUser has
an elevated role/permission to edit other members, and return 401/403 for
unauthorized requests; ensure any missing or invalid workspace/user lookups
return appropriate not-found/forbidden responses and use the same role-check
helpers as the parent endpoint.

Comment on lines +30 to +35
const webhookConfig = await getConfig("birthday_webhook", workspace.groupId);

if (!webhookConfig || !webhookConfig.enabled || !webhookConfig.url) {
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "birthday_webhook" -S

Repository: PlanetaryOrbit/orbit

Length of output: 421


🏁 Script executed:

rg -n "getConfig\(\"birthday_webhook\"" -S

Repository: PlanetaryOrbit/orbit

Length of output: 314


🏁 Script executed:

fd -n "birthdays.ts" --type f

Repository: PlanetaryOrbit/orbit

Length of output: 294


🏁 Script executed:

wc -l pages/api/cron/birthdays.ts

Repository: PlanetaryOrbit/orbit

Length of output: 95


🏁 Script executed:

cat -n pages/api/cron/birthdays.ts

Repository: PlanetaryOrbit/orbit

Length of output: 5174


🏁 Script executed:

cat -n pages/api/workspace/[id]/settings/general/birthdays/hook.ts

Repository: PlanetaryOrbit/orbit

Length of output: 3165


🏁 Script executed:

rg -n "discord" pages/api/cron/birthdays.ts

Repository: PlanetaryOrbit/orbit

Length of output: 195


🏁 Script executed:

rg -n "webhook" pages/api/cron/birthdays.ts -i

Repository: PlanetaryOrbit/orbit

Length of output: 450


🏁 Script executed:

rg -n "canary.discord" --type-list | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 453


Add request timeout to prevent webhook fetch from hanging

The fetch(webhookConfig.url, ...) call lacks a timeout mechanism, which could cause the cron job to hang indefinitely if the webhook endpoint is unresponsive. While Discord webhook URL validation already exists in the config endpoint (hook.ts:58), add an AbortController with a timeout to ensure cron executions complete reliably:

          const response = await fetch(webhookConfig.url, {
+           signal: AbortSignal.timeout(10_000),
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify(webhookBody),
          });

This applies to line 98-104 where the webhook is invoked.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pages/api/cron/birthdays.ts around lines 30-35 and where the webhook is
invoked at lines ~98-104, the fetch call for webhookConfig.url has no timeout
and can hang; add an AbortController, start a timer (e.g., 5000 ms or
configurable) and pass controller.signal to fetch, clear the timer after fetch
completes, and catch AbortError to log a timeout-specific message and continue
so the cron job doesn't stall. Ensure the controller's timer is cleaned up in
both success and error paths to avoid leaks and keep behavior unchanged for
non-timeout errors.

Comment on lines +32 to +48
export default withPermissionCheck(handler, "view_entire_groups_activity");

export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
if (req.method !== "GET")
return res
.status(405)
.json({ success: false, error: "Method not allowed" });
if (!req.session.userid)
return res.status(401).json({ success: false, error: "Not logged in" });

const workspaceId = parseInt(req.query.id as string);
const cacheKey = `activity_users_${workspaceId}`;
const now = Date.now();
const cached = activityUsersCache.get(cacheKey);
if (cached && now - cached.timestamp < ACTIVITY_CACHE_DURATION) {
return res.status(200).json({ success: true, message: cached.data });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix auth regression: handler’s session check breaks withPermissionCheck service-key bypass + validate workspaceId.
withPermissionCheck(...) allows calling the handler with the x-planetary-cloud-service-key header even if there’s no session, but the handler rejects when req.session.userid is missing (Line 39-40). Also, parseInt(req.query.id as string) can yield NaN if the handler is invoked directly (tests/other callers).

 export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
   if (req.method !== "GET")
     return res
       .status(405)
       .json({ success: false, error: "Method not allowed" });
-  if (!req.session.userid)
-    return res.status(401).json({ success: false, error: "Not logged in" });
-
-  const workspaceId = parseInt(req.query.id as string);
+  const workspaceId = Number.parseInt(req.query.id as string, 10);
+  if (Number.isNaN(workspaceId)) {
+    return res.status(400).json({ success: false, error: "Invalid workspace id" });
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default withPermissionCheck(handler, "view_entire_groups_activity");
export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
if (req.method !== "GET")
return res
.status(405)
.json({ success: false, error: "Method not allowed" });
if (!req.session.userid)
return res.status(401).json({ success: false, error: "Not logged in" });
const workspaceId = parseInt(req.query.id as string);
const cacheKey = `activity_users_${workspaceId}`;
const now = Date.now();
const cached = activityUsersCache.get(cacheKey);
if (cached && now - cached.timestamp < ACTIVITY_CACHE_DURATION) {
return res.status(200).json({ success: true, message: cached.data });
}
export default withPermissionCheck(handler, "view_entire_groups_activity");
export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
if (req.method !== "GET")
return res
.status(405)
.json({ success: false, error: "Method not allowed" });
const workspaceId = Number.parseInt(req.query.id as string, 10);
if (Number.isNaN(workspaceId)) {
return res.status(400).json({ success: false, error: "Invalid workspace id" });
}
const cacheKey = `activity_users_${workspaceId}`;
const now = Date.now();
const cached = activityUsersCache.get(cacheKey);
if (cached && now - cached.timestamp < ACTIVITY_CACHE_DURATION) {
return res.status(200).json({ success: true, message: cached.data });
}

Comment on lines +213 to +285
const adjustments = await prisma.activityAdjustment.findMany({
where: {
workspaceGroupId: workspaceId,
createdAt: {
gte: startDate,
lte: currentDate,
},
},
});

adjustments.forEach((adjustment: any) => {
const found = combinedMinutes.find(
(x) => x.userId == Number(adjustment.userId)
);
const adjustmentMs = adjustment.minutes * 60000;
if (found) {
found.ms.push(adjustmentMs);
} else {
combinedMinutes.push({
userId: Number(adjustment.userId),
ms: [adjustmentMs],
});
}
});

const topStaff: TopStaff[] = [];
const processedUserIds = new Set<number>();
for (const min of combinedMinutes) {
const minSum = min.ms.reduce((partial, a) => partial + a, 0);
const found = users.find((x) => x.userid === BigInt(min.userId));
if (leaderboardRank && found) {
const userRank = (found as any).ranks?.[0]?.rankId;
if (!userRank || Number(userRank) < leaderboardRank) {
continue;
}
}

if (found) {
topStaff.push({
userId: min.userId,
username: found?.username || "Unknown",
ms: minSum,
picture: found?.picture || "Unknown",
});
processedUserIds.add(min.userId);
}
}
for (const user of users) {
const userId = Number(user.userid);
if (processedUserIds.has(userId)) continue;

if (leaderboardRank) {
const userRank = (user as any).ranks?.[0]?.rankId;
if (!userRank || Number(userRank) < leaderboardRank) {
continue;
}
}

topStaff.push({
userId: userId,
username: user.username || "Unknown",
ms: 0,
picture: user.picture || "Unknown",
});
}

const bestStaff = topStaff.sort((a, b) => {
if (b.ms !== a.ms) {
return b.ms - a.ms;
}
return a.username.localeCompare(b.username);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, get the file size and structure
wc -l pages/api/workspace/[id]/activity/users.ts

Repository: PlanetaryOrbit/orbit

Length of output: 110


🏁 Script executed:

# Check the full file to see all Number() conversions and context
cat -n pages/api/workspace/[id]/activity/users.ts

Repository: PlanetaryOrbit/orbit

Length of output: 9502


🏁 Script executed:

# Search for userId/userid type definitions in Prisma schema
fd -t f "schema.prisma" -o prisma -x cat -n {}

Repository: PlanetaryOrbit/orbit

Length of output: 195


🏁 Script executed:

# Search for userId/userid field definitions across the codebase
rg -n "userid.*BigInt|userId.*BigInt" --type ts --type tsx

Repository: PlanetaryOrbit/orbit

Length of output: 91


🏁 Script executed:

# Find the Prisma schema file
fd -t f "schema.prisma"

Repository: PlanetaryOrbit/orbit

Length of output: 84


🏁 Script executed:

# Search for userid/userId field definitions in schema.prisma
rg -A 3 -B 1 "userid|userId" prisma/schema.prisma | head -100

Repository: PlanetaryOrbit/orbit

Length of output: 3422


🏁 Script executed:

# Also check if there's documentation or constraints on user ID ranges
rg -n "userid.*Int|userId.*Int" -A 2 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 50376


Store user IDs as BigInt or string; do not convert database BigInt IDs to Number.

Lines 164, 172, 193, 207, 225, 232, 261 convert BigInt IDs to Number, causing precision loss for large user IDs. Line 242's lookup users.find((x) => x.userid === BigInt(min.userId)) cannot recover the lost precision when comparing back to BigInt, resulting in silent lookup failures. Additionally, Set<number> deduplication at line 239 will not work correctly for IDs exceeding Number.MAX_SAFE_INTEGER. Use BigInt throughout (matching the codebase pattern seen in utils/permissionsManager.ts and auth handlers), or adjust types to handle string IDs if the API contract requires it.

🤖 Prompt for AI Agents
pages/api/workspace/[id]/activity/users.ts lines 213-285: the code converts DB
BigInt user IDs to Number (e.g., Number(adjustment.userId), Number(user.userid))
causing precision loss and broken lookups; change all ID handling to BigInt (or
to strings consistently): keep adjustment.userId and user.userid as BigInt (use
BigInt(adjustment.userId) if needed), use Set<bigint> for processedUserIds,
compare BigInts directly (user.userid === min.userId as BigInt), and avoid any
Number(...) conversions; if the external API expects string IDs, convert to
string only at the final output (e.g., userId: min.userId.toString()) rather
than mid-flow.

Comment on lines +22 to +27
user: {
birthdayDay: { not: null },
birthdayMonth: { not: null }
}
},
include: { user: { select: { userid: true, username: true, picture: true } } }
include: { user: { select: { userid: true, username: true, picture: true, birthdayDay: true, birthdayMonth: true } } }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find Prisma schema file
find . -name "schema.prisma" -o -name "*.prisma" | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 86


🏁 Script executed:

# Look at the file structure to understand the project
git ls-files | grep -E "(prisma|pages/api)" | head -30

Repository: PlanetaryOrbit/orbit

Length of output: 973


🏁 Script executed:

# Inspect the Prisma schema for user and workspaceMember models
cat ./prisma/schema.prisma

Repository: PlanetaryOrbit/orbit

Length of output: 16714


🏁 Script executed:

# Also get the actual file under review
cat -n pages/api/workspace/[id]/home/upcoming.ts | head -60

Repository: PlanetaryOrbit/orbit

Length of output: 2614


🏁 Script executed:

# Check if there are migration files that show the schema changes
find . -path "./prisma/migrations" -type d

Repository: PlanetaryOrbit/orbit

Length of output: 83


🏁 Script executed:

# Look for any database migration files or migration history
ls -la prisma/migrations/ 2>/dev/null | head -30

Repository: PlanetaryOrbit/orbit

Length of output: 1961


🏁 Script executed:

# Search for birthday-related migrations
grep -r "birthdayDay\|birthdayMonth" prisma/migrations/ | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 397


🏁 Script executed:

# Check the most recent migrations for schema changes
ls -lt prisma/migrations/ | head -10 | awk '{print $NF}' | while read dir; do
  if [ -n "$dir" ] && [ "$dir" != "total" ]; then
    echo "=== Migration: $dir ==="
    cat "prisma/migrations/$dir/migration.sql" 2>/dev/null | head -20
  fi
done

Repository: PlanetaryOrbit/orbit

Length of output: 4860


🏁 Script executed:

# Read the birthday migration file
cat prisma/migrations/20250613_birthdays/migration.sql

Repository: PlanetaryOrbit/orbit

Length of output: 152


🏁 Script executed:

# Read the recent hw_25 migration that also mentions birthdays
cat prisma/migrations/20251011203302_hw_25/migration.sql

Repository: PlanetaryOrbit/orbit

Length of output: 3553


🏁 Script executed:

# Check if there are any other references to workspaceMember birthday fields in the codebase
rg "workspaceMember.*birthday|birthday.*workspaceMember" --type ts --type js

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Check all API files that might use birthday data to see if any still reference workspaceMember
rg "birthdayDay|birthdayMonth" pages/api/ --type ts --type js -B 2 -A 2

Repository: PlanetaryOrbit/orbit

Length of output: 9062


Critical: Birthday field migration is incomplete—multiple endpoints still write to workspaceMember instead of user.

The code reads birthdayDay and birthdayMonth from the user relation, but other endpoints continue to write these fields to workspaceMember:

  • pages/api/workspace/[id]/birthday.ts writes to workspaceMember
  • pages/api/workspace/[id]/birthday/[userId].ts writes to workspaceMember
  • pages/api/workspace/[id]/profile/[uid]/member-info.ts writes to workspaceMember

This split implementation means birthdays updated via those endpoints won't be visible in the upcoming.ts query. Migrate all write operations to use user instead of workspaceMember, or revert this query to read from workspaceMember to match the write operations.

🤖 Prompt for AI Agents
In pages/api/workspace/[id]/home/upcoming.ts around lines 22–27 the query reads
birthdayDay and birthdayMonth from the user relation while other endpoints still
persist those fields on workspaceMember, causing inconsistent data; update the
write endpoints to persist birthdayDay and birthdayMonth on the user record
(load user id from workspaceMember and update User model) so all reads use user,
or revert this query to select birthdays from workspaceMember instead to match
current writers—pick one approach and make all read/write code consistent across
the four listed files.

Comment on lines +5 to +11
async function handler(req: NextApiRequest, res: NextApiResponse) {
const workspaceId = Number(req.query.id as string);
const userId = String(req.query.uid as string);

if (!workspaceId || !userId) {
return res.status(400).json({ success: false, error: "Missing workspace ID or user ID" });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing authorization check.

This endpoint allows any authenticated user to modify any other user's profile data (department, timezone, birthday, etc.) without verifying they have permission. Consider using withPermissionCheck or adding explicit authorization logic to verify the requesting user is either the profile owner or has appropriate admin permissions.

-import { withSessionRoute } from "@/lib/withSession";
+import { withPermissionCheck } from "@/utils/permissionsManager";

 async function handler(req: NextApiRequest, res: NextApiResponse) {
   const workspaceId = Number(req.query.id as string);
   const userId = String(req.query.uid as string);
+  const requestingUserId = (req as any).session?.userid;

   if (!workspaceId || !userId) {
     return res.status(400).json({ success: false, error: "Missing workspace ID or user ID" });
   }

+  // Allow if user is editing their own profile or has admin permission
+  const isSelf = requestingUserId && BigInt(requestingUserId) === BigInt(userId);
+  if (!isSelf) {
+    // Permission check is handled by withPermissionCheck wrapper for non-self edits
+    // This path requires 'manage_members' or 'admin' permission
+  }
+
   if (req.method === "PATCH") {

At the bottom:

-export default withSessionRoute(handler);
+export default withPermissionCheck(handler, 'manage_members');

Alternatively, implement self-edit logic that bypasses permission checks when userId === requestingUserId.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pages/api/workspace/[id]/profile/[uid]/member-info.ts around lines 5 to 11,
there is no authorization check so any authenticated user can modify another
user's profile; add an authorization gate that retrieves the requesting user
(from the session/JWT), then allow the operation only if requestingUserId ===
userId (self-edit) OR the requesting user has the workspace admin/appropriate
permission (use existing withPermissionCheck helper if available or check
workspace role/permissions explicitly); if neither condition is met return
res.status(403) with an error, and ensure this check runs before any profile
modifications are applied.

Comment on lines +17 to +53
const { id } = req.query;
const { timezone } = req.body;

if (!timezone || typeof timezone !== "string") {
return res.status(400).json({ error: "Invalid timezone" });
}

try {
const existingMember = await prisma.workspaceMember.findUnique({
where: {
workspaceGroupId_userId: {
workspaceGroupId: parseInt(id as string),
userId: BigInt(userId),
},
},
select: {
timezone: true,
},
});
if (!existingMember?.timezone) {
await prisma.workspaceMember.update({
where: {
workspaceGroupId_userId: {
workspaceGroupId: parseInt(id as string),
userId: BigInt(userId),
},
},
data: {
timezone: timezone,
},
});

return res.status(200).json({ success: true, updated: true });
}

return res.status(200).json({ success: true, updated: false });
} catch (error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle “member not found” explicitly (current code will 500)

If findUnique returns null, !existingMember?.timezone is true and update() will throw “Record to update not found”.

   const { id } = req.query;
   const { timezone } = req.body;
+  const workspaceGroupId = parseInt(id as string);
+  if (Number.isNaN(workspaceGroupId)) {
+    return res.status(400).json({ error: "Invalid workspace id" });
+  }
 ...
     const existingMember = await prisma.workspaceMember.findUnique({
       where: {
         workspaceGroupId_userId: {
-          workspaceGroupId: parseInt(id as string),
+          workspaceGroupId,
           userId: BigInt(userId),
         },
       },
       select: {
         timezone: true,
       },
     });
-    if (!existingMember?.timezone) {
+    if (!existingMember) {
+      return res.status(404).json({ error: "Workspace member not found" });
+    }
+    if (!existingMember.timezone) {
       await prisma.workspaceMember.update({
         where: {
           workspaceGroupId_userId: {
-            workspaceGroupId: parseInt(id as string),
+            workspaceGroupId,
             userId: BigInt(userId),
           },
         },
         data: {
           timezone: timezone,
         },
       });
🤖 Prompt for AI Agents
In pages/api/workspace/[id]/timezone.ts around lines 17 to 53, the code treats a
null result from prisma.workspaceMember.findUnique as if a row exists and then
calls update (causing a "Record to update not found" 500). Explicitly check if
existingMember is null and return a 404 (or appropriate 4xx) JSON error before
attempting any update; otherwise, compare existingMember.timezone to the new
timezone and only call prisma.workspaceMember.update when the member exists and
the timezone actually needs changing, then return the appropriate
success/updated response.

Comment on lines +324 to +333
// If we have a scope from the pattern dialog, use it instead of asking again
const deleteScope = scope || (deleteAll ? "all" : "single");

await axios.delete(
`/api/workspace/${workspace.groupId}/sessions/${session.id}/delete`,
{
data: { deleteAll },
data: {
deleteAll: deleteScope === "all",
deleteScope: deleteScope, // Pass the scope for future/single distinction
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, search for deleteScope references to understand the scope handling
rg -n "deleteScope" --type ts --type tsx -A 3 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 91


🏁 Script executed:

# Search for the backend delete endpoint implementation
rg -n "delete.*sessions.*delete" --type ts --type tsx --type js --type jsx -A 5 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 91


🏁 Script executed:

# Look for the delete modal implementation to see what options are offered
rg -n "deleteAll|deleteModal|delete.*modal" --type ts --type tsx --type jsx --type js -A 3 -B 2 | head -100

Repository: PlanetaryOrbit/orbit

Length of output: 91


🏁 Script executed:

# Search for "future" scope references to see if it's used elsewhere
rg -n "future" --type ts --type tsx -i | grep -i "scope\|delete"

Repository: PlanetaryOrbit/orbit

Length of output: 91


🏁 Script executed:

# Search for deleteScope references without file type restriction
rg -n "deleteScope" -A 3 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 1767


🏁 Script executed:

# Search for the delete endpoint in API routes
find . -type f -name "*.ts" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx" | xargs rg -l "delete.*sessions.*delete\|deleteScope" | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for API route handlers - typically in /pages/api or /src/pages/api
fd -e ts -e js -e tsx -e jsx "api" --type d

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for the delete modal or scope selection logic
rg -n "deleteAll.*single.*all\|scope.*future\|single.*all.*delete" -A 3 -B 2 | head -50

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for API routes - look for pages/api structure
find . -path "*/pages/api*" -type f \( -name "*.ts" -o -name "*.js" \) | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 690


🏁 Script executed:

# Search for the delete endpoint in API
rg -n "sessions.*delete\|/api.*delete" --glob "*/pages/api/**" -A 5 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 212


🏁 Script executed:

# Search for where scope parameter comes from - the pattern dialog
rg -n "scope.*=\|setScope\|pattern.*dialog" -A 2 -B 2 | head -80

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for deleteAll parameter usage and modal implementation
rg -n "deleteAll.*=\|setDeleteAll\|openDeleteModal\|DeleteModal" -A 3 -B 2 | head -100

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for workspace API routes
find . -path "*/pages/api/workspace*" -type f

Repository: PlanetaryOrbit/orbit

Length of output: 6237


🏁 Script executed:

# Search for files containing "delete" and "workspace"
rg -l "workspace.*delete\|delete.*workspace" pages/api/

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search entire codebase for the delete endpoint handler
rg -n "req.method.*DELETE\|DELETE.*sessions" -A 10 | head -100

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for the delete modal or scope selection dialog
rg -n "Modal\|Dialog\|scope.*\[" pages/workspace -A 3 -B 2 | head -150

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for the specific file with the delete handler
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec grep -l "sessions.*delete\|deleteScope" {} \; 2>/dev/null

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Read the delete endpoint that's called from the edit page
cat -n ./pages/api/workspace/[id]/sessions/[sid]/delete.ts

Repository: PlanetaryOrbit/orbit

Length of output: 2852


🏁 Script executed:

# Also check the other delete endpoint to understand the difference
cat -n ./pages/api/workspace/[id]/sessions/manage/[sid]/delete.ts

Repository: PlanetaryOrbit/orbit

Length of output: 1510


🏁 Script executed:

# Search for where scope comes from in the edit page - look for pattern dialog
rg -n "scope\|pattern.*dialog\|Dialog" pages/workspace/[id]/sessions/edit/ -A 2 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for "scope" in the edit page more broadly
cat -n pages/workspace/[id]/sessions/edit/[sid].tsx | head -350 | tail -100

Repository: PlanetaryOrbit/orbit

Length of output: 4017


🏁 Script executed:

# Look for pattern dialog or scope-related state
rg -n "pattern\|scope\|deleteAll" pages/workspace/[id]/sessions/edit/ --type-add='tsx:*.tsx' -A 2 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for Dialog, Modal, or where scope state is set
rg -n "useState.*scope\|setScope\|const scope" pages/workspace/[id]/sessions/edit/ --type-add='tsx:*.tsx'

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for delete modal/dialog implementation
find pages/workspace -name "*delete*" -o -name "*modal*" -o -name "*dialog*" | head -20

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for the pattern dialog in the file - search for Dialog or Modal components
rg -n "Dialog\|Modal\|<dialog\|<modal" pages/workspace/[id]/sessions/edit/[sid].tsx -A 5 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for the pattern dialog component definition
find pages/workspace -type f -name "*.tsx" -o -name "*.ts" | xargs rg -l "PatternDialog\|pattern.*dialog" 2>/dev/null

Repository: PlanetaryOrbit/orbit

Length of output: 48


🏁 Script executed:

# Look at the complete file to find scope initialization
wc -l pages/workspace/[id]/sessions/edit/[sid].tsx

Repository: PlanetaryOrbit/orbit

Length of output: 112


🏁 Script executed:

# Get the beginning of the file to see scope declaration
head -100 pages/workspace/[id]/sessions/edit/[sid].tsx

Repository: PlanetaryOrbit/orbit

Length of output: 2557


🏁 Script executed:

# Search for scope variable declaration and usage throughout the file
rg -n "scope\|Pattern" pages/workspace/[id]/sessions/edit/[sid].tsx | head -50

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look around the delete function for the pattern dialog
sed -n '200,400p' pages/workspace/[id]/sessions/edit/[sid].tsx | cat -n

Repository: PlanetaryOrbit/orbit

Length of output: 8414


🏁 Script executed:

# Search for where deleteAll is set and the delete modal
rg -n "deleteAll\|DeleteModal\|showDeleteModal" pages/workspace/[id]/sessions/edit/[sid].tsx -A 2 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for all useState calls
rg -n "useState" pages/workspace/[id]/sessions/edit/[sid].tsx

Repository: PlanetaryOrbit/orbit

Length of output: 656


🏁 Script executed:

# Search for the delete modal implementation
rg -n "showDeleteModal\|DeleteModal" pages/workspace/[id]/sessions/edit/[sid].tsx -A 20 | head -80

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Look for where the modal offers delete options
sed -n '400,700p' pages/workspace/[id]/sessions/edit/[sid].tsx | cat -n

Repository: PlanetaryOrbit/orbit

Length of output: 20361


🏁 Script executed:

# Search for what the modal buttons/options are
rg -n "deleteAll.*true\|setDeleteAll\|Delete all\|Delete this" pages/workspace/[id]/sessions/edit/[sid].tsx -A 2 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Search for the update-pattern API implementation
cat -n ./pages/api/workspace/[id]/sessions/[sid]/update-pattern.ts | head -100

Repository: PlanetaryOrbit/orbit

Length of output: 4028


🏁 Script executed:

# Check if update-pattern API handles "future" properly
rg -n "future\|updateScope" ./pages/api/workspace/[id]/sessions/[sid]/update-pattern.ts -A 3 -B 2

Repository: PlanetaryOrbit/orbit

Length of output: 46


🏁 Script executed:

# Check the complete delete endpoint to confirm deleteScope is not used
cat -n ./pages/api/workspace/[id]/sessions/[sid]/delete.ts | grep -A 30 "deleteAll"

Repository: PlanetaryOrbit/orbit

Length of output: 2164


Delete scope logic doesn't properly handle "future" scope.

The delete endpoint at /api/workspace/${workspace.groupId}/sessions/${session.id}/delete only accepts a deleteAll boolean parameter and completely ignores the deleteScope parameter sent by the frontend. When deleteScope is set to "future", the code calculates deleteAll: false (line 331), which causes the backend to delete only the single session instead of deleting this session and all future sessions on the same day of week. However, the frontend displays a success message stating "This and future sessions deleted successfully" (lines 339-340), creating a mismatch between the user's expectation and the actual backend behavior.

To fix this, the delete endpoint needs to implement the same scope handling logic currently used in the update-pattern API (update-pattern.ts), which properly supports "single", "future", and "all" scopes by filtering sessions based on schedule and day of week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants