Admin Dashboard enhancements, OAuth session fixes, and tracking script improvements #37

Merged
uz1mani merged 9 commits from staging into main 2026-02-27 12:27:37 +00:00
uz1mani commented 2026-02-27 12:26:03 +00:00 (Migrated from github.com)

Work Item

Summary

  • Implemented Pulse Admin Dashboard with ability to manually grant plans to organizations and improved organization list with click-to-copy IDs and site name fallbacks
  • Fixed OAuth session flow issues when opening Pulse from Ciphera Drop, ensuring proper callback handling even without state parameter
  • Improved Pulse tracking script reliability for embedded sites like Shopify with better cross-origin and iframe handling

Changes

Admin Dashboard (pulse-frontend):

  • Added feature to manually grant subscription plans to organizations
  • Implemented click-to-copy functionality for organization IDs in admin list
  • Added site name fallback display when organization name is unavailable
  • Fixed admin organizations list document visibility logic
  • Removed date-fns dependency to fix build issues
  • Replaced Card component with native equivalents for build compatibility
  • Updated admin layout for improved responsiveness across screen sizes

Authentication & Session Flow:

  • Fixed OAuth session flow validation to treat stateless callbacks as valid when coming from trusted origins
  • Resolved session flow issue when opening Pulse from the Ciphera Drop interface
  • Improved handling of session-flow callbacks without state parameter

Tracking Script:

  • Enhanced Pulse tracking script for better compatibility with embedded sites (Shopify, etc.)
  • Fixed tracking issues in iframe and cross-origin contexts

Test Plan

  • Verify admin can manually grant plans to organizations through the admin dashboard
  • Confirm click-to-copy works for organization IDs in the admin list
  • Check that site name fallback displays correctly when org name is missing
  • Test OAuth login flow from Ciphera Drop to Pulse works without session errors
  • Verify tracking script loads and functions correctly on embedded Shopify sites
  • Confirm admin dashboard is responsive on mobile and tablet viewports
  • Ensure build passes without date-fns dependency
## Work Item ## Summary - Implemented Pulse Admin Dashboard with ability to manually grant plans to organizations and improved organization list with click-to-copy IDs and site name fallbacks - Fixed OAuth session flow issues when opening Pulse from Ciphera Drop, ensuring proper callback handling even without state parameter - Improved Pulse tracking script reliability for embedded sites like Shopify with better cross-origin and iframe handling ## Changes **Admin Dashboard (`pulse-frontend`):** - Added feature to manually grant subscription plans to organizations - Implemented click-to-copy functionality for organization IDs in admin list - Added site name fallback display when organization name is unavailable - Fixed admin organizations list document visibility logic - Removed `date-fns` dependency to fix build issues - Replaced `Card` component with native equivalents for build compatibility - Updated admin layout for improved responsiveness across screen sizes **Authentication & Session Flow:** - Fixed OAuth session flow validation to treat stateless callbacks as valid when coming from trusted origins - Resolved session flow issue when opening Pulse from the Ciphera Drop interface - Improved handling of session-flow callbacks without state parameter **Tracking Script:** - Enhanced Pulse tracking script for better compatibility with embedded sites (Shopify, etc.) - Fixed tracking issues in iframe and cross-origin contexts ## Test Plan - [x] Verify admin can manually grant plans to organizations through the admin dashboard - [x] Confirm click-to-copy works for organization IDs in the admin list - [x] Check that site name fallback displays correctly when org name is missing - [x] Test OAuth login flow from Ciphera Drop to Pulse works without session errors - [x] Verify tracking script loads and functions correctly on embedded Shopify sites - [x] Confirm admin dashboard is responsive on mobile and tablet viewports - [x] Ensure build passes without date-fns dependency
greptile-apps[bot] commented 2026-02-27 12:30:45 +00:00 (Migrated from github.com)

Greptile Summary

This PR adds a comprehensive admin dashboard for managing organizations and subscription plans, while fixing OAuth session flow issues when opening Pulse from the Ciphera hub. The admin features include organization listing with click-to-copy IDs, detailed organization views, and the ability to manually grant subscription plans.

Key Changes:

  • New admin dashboard at /admin with organization management UI
  • Manual plan granting feature for admins with configurable limits and billing intervals
  • OAuth callback now accepts stateless requests to support SSO flow from auth hub
  • Organizations list now includes orgs without billing records

Critical Issue:

  • The OAuth callback change (lines 59-63 in app/auth/callback/page.tsx) removes CSRF protection by accepting callbacks without a state parameter and provides no origin validation. This allows potential CSRF attacks where malicious actors could craft callback URLs to hijack user sessions.

Other Issues:

  • Datetime input timezone mismatch: label says "UTC" but input uses local time
  • Click-to-copy shows truncated ID but copies full ID (minor UX inconsistency)

Confidence Score: 2/5

  • This PR has a critical OAuth CSRF vulnerability that should be addressed before merging
  • Score reflects a critical security issue in the OAuth callback flow. While the admin dashboard implementation is solid and well-structured, the removal of state parameter validation in OAuth callbacks creates a CSRF vulnerability. The backend may have additional validation, but the frontend should verify callback origin when accepting stateless requests. The other issues are minor UX concerns.
  • app/auth/callback/page.tsx requires immediate attention for the CSRF vulnerability. app/admin/orgs/[id]/page.tsx needs the timezone label fix.

Important Files Changed

Filename Overview
app/admin/layout.tsx Admin authentication check with client-side redirect - relies on backend API validation
app/admin/orgs/page.tsx Organizations list with click-to-copy IDs - minor: copies shortened ID but shows full ID would be more useful
app/admin/orgs/[id]/page.tsx Organization detail with plan granting - datetime input timezone mismatch and no plan-limit validation
app/auth/callback/page.tsx OAuth callback modified to accept stateless requests - CSRF vulnerability without origin validation

Sequence Diagram

sequenceDiagram
    participant User
    participant AuthHub as auth.ciphera.net
    participant Pulse as pulse.ciphera.net
    participant API as Pulse API
    
    Note over User,API: SSO Flow (Stateless)
    User->>AuthHub: Already authenticated
    User->>AuthHub: Click "Open Pulse"
    AuthHub->>Pulse: Redirect to /auth/callback?code=xyz (no state)
    Pulse->>Pulse: Check state param (missing)
    Pulse->>Pulse: Clear localStorage PKCE data
    Pulse->>API: Exchange code for tokens
    API->>Pulse: Return access_token + refresh_token
    Pulse->>User: Redirect to dashboard (logged in)
    
    Note over User,API: Standard OAuth Flow
    User->>Pulse: Click "Sign In"
    Pulse->>Pulse: Generate state + code_verifier
    Pulse->>Pulse: Store in localStorage
    Pulse->>AuthHub: Redirect with state + code_challenge
    AuthHub->>Pulse: Redirect to /auth/callback?code=abc&state=xyz
    Pulse->>Pulse: Validate state matches localStorage
    Pulse->>API: Exchange code with code_verifier
    API->>Pulse: Return tokens
    Pulse->>User: Redirect to dashboard

Last reviewed commit: 335cfc1

<h3>Greptile Summary</h3> This PR adds a comprehensive admin dashboard for managing organizations and subscription plans, while fixing OAuth session flow issues when opening Pulse from the Ciphera hub. The admin features include organization listing with click-to-copy IDs, detailed organization views, and the ability to manually grant subscription plans. **Key Changes:** - New admin dashboard at `/admin` with organization management UI - Manual plan granting feature for admins with configurable limits and billing intervals - OAuth callback now accepts stateless requests to support SSO flow from auth hub - Organizations list now includes orgs without billing records **Critical Issue:** - The OAuth callback change (lines 59-63 in `app/auth/callback/page.tsx`) removes CSRF protection by accepting callbacks without a `state` parameter and provides no origin validation. This allows potential CSRF attacks where malicious actors could craft callback URLs to hijack user sessions. **Other Issues:** - Datetime input timezone mismatch: label says "UTC" but input uses local time - Click-to-copy shows truncated ID but copies full ID (minor UX inconsistency) <h3>Confidence Score: 2/5</h3> - This PR has a critical OAuth CSRF vulnerability that should be addressed before merging - Score reflects a critical security issue in the OAuth callback flow. While the admin dashboard implementation is solid and well-structured, the removal of state parameter validation in OAuth callbacks creates a CSRF vulnerability. The backend may have additional validation, but the frontend should verify callback origin when accepting stateless requests. The other issues are minor UX concerns. - app/auth/callback/page.tsx requires immediate attention for the CSRF vulnerability. app/admin/orgs/[id]/page.tsx needs the timezone label fix. <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | app/admin/layout.tsx | Admin authentication check with client-side redirect - relies on backend API validation | | app/admin/orgs/page.tsx | Organizations list with click-to-copy IDs - minor: copies shortened ID but shows full ID would be more useful | | app/admin/orgs/[id]/page.tsx | Organization detail with plan granting - datetime input timezone mismatch and no plan-limit validation | | app/auth/callback/page.tsx | OAuth callback modified to accept stateless requests - CSRF vulnerability without origin validation | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant User participant AuthHub as auth.ciphera.net participant Pulse as pulse.ciphera.net participant API as Pulse API Note over User,API: SSO Flow (Stateless) User->>AuthHub: Already authenticated User->>AuthHub: Click "Open Pulse" AuthHub->>Pulse: Redirect to /auth/callback?code=xyz (no state) Pulse->>Pulse: Check state param (missing) Pulse->>Pulse: Clear localStorage PKCE data Pulse->>API: Exchange code for tokens API->>Pulse: Return access_token + refresh_token Pulse->>User: Redirect to dashboard (logged in) Note over User,API: Standard OAuth Flow User->>Pulse: Click "Sign In" Pulse->>Pulse: Generate state + code_verifier Pulse->>Pulse: Store in localStorage Pulse->>AuthHub: Redirect with state + code_challenge AuthHub->>Pulse: Redirect to /auth/callback?code=abc&state=xyz Pulse->>Pulse: Validate state matches localStorage Pulse->>API: Exchange code with code_verifier API->>Pulse: Return tokens Pulse->>User: Redirect to dashboard ``` <sub>Last reviewed commit: 335cfc1</sub>
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-27 12:30:51 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

7 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

<sub>7 files reviewed, 3 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -0,0 +196,4 @@
/>
</div>
<div className="space-y-2">
greptile-apps[bot] (Migrated from github.com) commented 2026-02-27 12:30:50 +00:00

Label says "UTC" but datetime-local input uses the browser's local timezone. User enters "2024-01-01 00:00" in PST thinking it's UTC, but line 95 converts it as PST → UTC (becomes "2024-01-01T08:00:00Z"). Either change label to "Local Time" or use a UTC-aware input component.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/admin/orgs/[id]/page.tsx
Line: 199

Comment:
Label says "UTC" but datetime-local input uses the browser's local timezone. User enters "2024-01-01 00:00" in PST thinking it's UTC, but line 95 converts it as PST → UTC (becomes "2024-01-01T08:00:00Z"). Either change label to "Local Time" or use a UTC-aware input component.

How can I resolve this? If you propose a fix, please make it concise.
Label says "UTC" but datetime-local input uses the browser's local timezone. User enters "2024-01-01 00:00" in PST thinking it's UTC, but line 95 converts it as PST → UTC (becomes "2024-01-01T08:00:00Z"). Either change label to "Local Time" or use a UTC-aware input component. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/admin/orgs/[id]/page.tsx Line: 199 Comment: Label says "UTC" but datetime-local input uses the browser's local timezone. User enters "2024-01-01 00:00" in PST thinking it's UTC, but line 95 converts it as PST → UTC (becomes "2024-01-01T08:00:00Z"). Either change label to "Local Time" or use a UTC-aware input component. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -0,0 +23,4 @@
onClick={copy}
className="font-mono text-xs text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange cursor-pointer transition-colors text-left"
title="Click to copy"
>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-27 12:30:51 +00:00

Copies only first 8 chars of org ID (${id.substring(0, 8)}...) but navigator.clipboard.writeText(id) copies the full ID. The truncated display is inconsistent with what gets copied - consider showing this visually or copy the truncated version if that's the intent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/admin/orgs/page.tsx
Line: 26

Comment:
Copies only first 8 chars of org ID (`${id.substring(0, 8)}...`) but `navigator.clipboard.writeText(id)` copies the full ID. The truncated display is inconsistent with what gets copied - consider showing this visually or copy the truncated version if that's the intent.

How can I resolve this? If you propose a fix, please make it concise.
Copies only first 8 chars of org ID (`${id.substring(0, 8)}...`) but `navigator.clipboard.writeText(id)` copies the full ID. The truncated display is inconsistent with what gets copied - consider showing this visually or copy the truncated version if that's the intent. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/admin/orgs/page.tsx Line: 26 Comment: Copies only first 8 chars of org ID (`${id.substring(0, 8)}...`) but `navigator.clipboard.writeText(id)` copies the full ID. The truncated display is inconsistent with what gets copied - consider showing this visually or copy the truncated version if that's the intent. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -65,2 +66,4 @@
const isFullOAuth = !!storedState && !!codeVerifier
if (isFullOAuth && state !== storedState) {
logger.error('State mismatch', { received: state, stored: storedState })
setError('Invalid state')
greptile-apps[bot] (Migrated from github.com) commented 2026-02-27 12:30:49 +00:00

Accepting OAuth callbacks without state parameter creates a CSRF vulnerability. An attacker could craft a malicious callback URL with their own authorization code and trick users into executing it, logging them into the attacker's account.

The PR description mentions "trusted origins" but there's no code validating the origin. At minimum, check document.referrer to ensure it starts with the auth domain when state is missing:

// * Session flow (from auth hub): redirect has code but no state. Clear stale PKCE
// * data from any previous app-initiated OAuth so exchange proceeds without validation.
if (!state) {
  // * Verify callback came from trusted auth domain to prevent CSRF
  if (typeof document !== 'undefined' && document.referrer) {
    const referrerUrl = new URL(document.referrer)
    const authUrl = new URL(AUTH_URL)
    if (referrerUrl.origin !== authUrl.origin) {
      logger.error('Stateless callback from untrusted origin', { referrer: document.referrer })
      setError('Invalid state')
      return
    }
  }
  localStorage.removeItem('oauth_state')
  localStorage.removeItem('oauth_code_verifier')
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/auth/callback/page.tsx
Line: 59-63

Comment:
Accepting OAuth callbacks without `state` parameter creates a CSRF vulnerability. An attacker could craft a malicious callback URL with their own authorization code and trick users into executing it, logging them into the attacker's account.

The PR description mentions "trusted origins" but there's no code validating the origin. At minimum, check `document.referrer` to ensure it starts with the auth domain when `state` is missing:

```suggestion
// * Session flow (from auth hub): redirect has code but no state. Clear stale PKCE
// * data from any previous app-initiated OAuth so exchange proceeds without validation.
if (!state) {
  // * Verify callback came from trusted auth domain to prevent CSRF
  if (typeof document !== 'undefined' && document.referrer) {
    const referrerUrl = new URL(document.referrer)
    const authUrl = new URL(AUTH_URL)
    if (referrerUrl.origin !== authUrl.origin) {
      logger.error('Stateless callback from untrusted origin', { referrer: document.referrer })
      setError('Invalid state')
      return
    }
  }
  localStorage.removeItem('oauth_state')
  localStorage.removeItem('oauth_code_verifier')
```

How can I resolve this? If you propose a fix, please make it concise.
Accepting OAuth callbacks without `state` parameter creates a CSRF vulnerability. An attacker could craft a malicious callback URL with their own authorization code and trick users into executing it, logging them into the attacker's account. The PR description mentions "trusted origins" but there's no code validating the origin. At minimum, check `document.referrer` to ensure it starts with the auth domain when `state` is missing: ```suggestion // * Session flow (from auth hub): redirect has code but no state. Clear stale PKCE // * data from any previous app-initiated OAuth so exchange proceeds without validation. if (!state) { // * Verify callback came from trusted auth domain to prevent CSRF if (typeof document !== 'undefined' && document.referrer) { const referrerUrl = new URL(document.referrer) const authUrl = new URL(AUTH_URL) if (referrerUrl.origin !== authUrl.origin) { logger.error('Stateless callback from untrusted origin', { referrer: document.referrer }) setError('Invalid state') return } } localStorage.removeItem('oauth_state') localStorage.removeItem('oauth_code_verifier') ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/auth/callback/page.tsx Line: 59-63 Comment: Accepting OAuth callbacks without `state` parameter creates a CSRF vulnerability. An attacker could craft a malicious callback URL with their own authorization code and trick users into executing it, logging them into the attacker's account. The PR description mentions "trusted origins" but there's no code validating the origin. At minimum, check `document.referrer` to ensure it starts with the auth domain when `state` is missing: ```suggestion // * Session flow (from auth hub): redirect has code but no state. Clear stale PKCE // * data from any previous app-initiated OAuth so exchange proceeds without validation. if (!state) { // * Verify callback came from trusted auth domain to prevent CSRF if (typeof document !== 'undefined' && document.referrer) { const referrerUrl = new URL(document.referrer) const authUrl = new URL(AUTH_URL) if (referrerUrl.origin !== authUrl.origin) { logger.error('Stateless callback from untrusted origin', { referrer: document.referrer }) setError('Invalid state') return } } localStorage.removeItem('oauth_state') localStorage.removeItem('oauth_code_verifier') ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
Sign in to join this conversation.
No description provided.