Skip to content

Enabling PAT backend - #336

Merged
yash-pouranik merged 8 commits into
geturbackend:mainfrom
Ayush4958:enabling-PAT-backend-clean
Jun 25, 2026
Merged

Enabling PAT backend #336
yash-pouranik merged 8 commits into
geturbackend:mainfrom
Ayush4958:enabling-PAT-backend-clean

Conversation

@Ayush4958

@Ayush4958 Ayush4958 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🚀 Pull Request Description

I had enabled PAT for project, Now CLI/AI Agents can easily authenticate against Dashboard API.
Added the PAT generator which adds prefix to generated PAT.
Also included PAT schema in developer model.
Added strict PAT validation middleware with Redis caching

🛠️ Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🎨 UI/UX improvement (Frontend only)
  • ⚙️ Refactor / Chore

🧪 Testing & Validation

Backend Verification:

  • I have run npm test in the backend/ directory and all tests passed.
  • I have verified the API endpoints using Postman/Thunder Client.
  • New unit tests have been added (if applicable).

Frontend Verification:

  • I have run npm run lint in the frontend/ directory.
  • Verified the UI changes on different screen sizes (Responsive).
  • Checked for any console errors in the browser dev tools.

✅ Checklist

  • My code follows the code style of this project.
  • I have performed a self-review of my code.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generate no new warnings or errors.
  • I have updated the documentation (README/Docs) accordingly.

Summary by CodeRabbit

  • New Features
    • Added personal access token (PAT) API endpoints to create, list, and revoke tokens.
    • Tokens support labels, scopes, bounded TTL (default 30 days, 1–365 days), and return masked token details plus usage/expiry metadata.
    • Enabled CLI authentication using Bearer PATs with a required prefix, including cache-backed validation to reduce repeated lookups.
  • Tests
    • Updated route tests to mock PAT controller create/list/revoke behaviors.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@yash-pouranik, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 31 minutes and 13 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b29199eb-ce15-45c6-a3e9-10edd63fe6d0

📥 Commits

Reviewing files that changed from the base of the PR and between da5670f and 390401f.

📒 Files selected for processing (8)
  • apps/dashboard-api/src/__tests__/routes.user.test.js
  • apps/dashboard-api/src/controllers/pat.controller.js
  • apps/dashboard-api/src/middlewares/authenticateCLI.js
  • apps/dashboard-api/src/routes/user.js
  • packages/common/src/index.js
  • packages/common/src/models/Developer.js
  • packages/common/src/models/PAT.js
  • packages/common/src/utils/token.utils.js
📝 Walkthrough

Walkthrough

This PR adds PAT token generation and storage, new PAT create/list/revoke controllers, CLI PAT authentication middleware, user-route endpoints, and a test mock for the new controller module.

Changes

PAT Feature

Layer / File(s) Summary
Token utilities
packages/common/src/utils/token.utils.js, packages/common/src/index.js
Adds Base62 encoding, PAT generation, token hashing, and re-exports the helpers from the common package entry point.
PAT storage schema
packages/common/src/models/PAT.js, packages/common/src/models/Developer.js, packages/common/src/index.js
Adds a PAT model with TTL expiry and extends the Developer model with a pats array of PAT subdocuments.
PAT controllers
apps/dashboard-api/src/controllers/pat.controller.js
Adds create, list, and revoke controllers for PATs, including validation, masked responses, and Redis cache invalidation on revoke.
CLI authentication middleware
apps/dashboard-api/src/middlewares/authenticateCLI.js
Adds bearer-token CLI authentication with Redis caching, PAT lookup, expiry checks, usage updates, and request context attachment.
Route wiring and test mock
apps/dashboard-api/src/routes/user.js, apps/dashboard-api/src/__tests__/routes.user.test.js
Registers the PAT REST endpoints on the user router and mocks the PAT controller in the user routes test.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

feature, backend, enhancement

Suggested reviewers

  • Nitin-kumar-yadav1307
  • yash-pouranik

Poem

🐇 A PAT now hops from hand to cache,
With hashes, scopes, and tokens in a flash.
The CLI sniffs ubpat_ on its way,
And three new routes keep things neatly in play.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: adding PAT backend support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 7

🧹 Nitpick comments (1)
apps/dashboard-api/src/__tests__/routes.user.test.js (1)

45-50: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add route-wiring assertions for the three new PAT endpoints.

This file mocks PAT handlers but doesn’t verify POST/GET/DELETE /api/user/pats* wiring yet. Adding 3 lightweight route tests would close that regression gap.

Example test additions
+const patController = require('../controllers/pat.controller');
+
+test('POST /api/user/pats is wired to authenticated createPAT', async () => {
+    const res = await request(app).post('/api/user/pats').send({ label: 'cli', scopes: ['read'] });
+    expect(res.status).toBe(200);
+    expect(authMiddleware).toHaveBeenCalledTimes(1);
+    expect(patController.createPAT).toHaveBeenCalledTimes(1);
+});
+
+test('GET /api/user/pats is wired to authenticated listPATs', async () => {
+    const res = await request(app).get('/api/user/pats');
+    expect(res.status).toBe(200);
+    expect(authMiddleware).toHaveBeenCalledTimes(1);
+    expect(patController.listPATs).toHaveBeenCalledTimes(1);
+});
+
+test('DELETE /api/user/pats/:id is wired to authenticated revokePAT', async () => {
+    const res = await request(app).delete('/api/user/pats/mock_pat_id');
+    expect(res.status).toBe(200);
+    expect(authMiddleware).toHaveBeenCalledTimes(1);
+    expect(patController.revokePAT).toHaveBeenCalledTimes(1);
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/__tests__/routes.user.test.js` around lines 45 - 50,
Add three test cases after the mock definitions to verify route wiring for the
PAT endpoints. Create tests that make HTTP requests to POST /api/user/pats
(should trigger the mocked createPAT function), GET /api/user/pats (should
trigger the mocked listPATs function), and DELETE /api/user/pats/:id (should
trigger the mocked revokePAT function). Each test should verify that the
corresponding mocked handler function was called with the request and response
objects to ensure the routes are properly wired to their controller handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/dashboard-api/src/controllers/pat.controller.js`:
- Around line 96-98: The PAT revocation persists to the database first, then
attempts Redis cache eviction using redis.del with the key
`cli:pat:cache:${patToRevoke.tokenHash}`. Since the database revoke is the
critical operation and the Redis deletion is just a cache optimization, handle
any Redis errors separately by logging them without failing the entire response.
Modify the error handling so that if the Redis deletion fails, the client still
receives a success response because the actual revocation already succeeded in
the database, rather than returning a 500 error that misrepresents the
operation's status.
- Around line 15-16: The TTL validation logic in the pat.controller.js file has
a flaw where the default value assignment using `const days = Number(ttlDays) ||
30;` occurs before validation, causing invalid input like ttlDays=0 to be
silently replaced with 30 instead of being rejected. Separate the defaulting and
validation logic by first checking if ttlDays is provided and valid (between 1
and 365), then applying the default value of 30 only when ttlDays is undefined,
null, or empty, ensuring the bounds check on line 16 properly catches all
invalid inputs before they become the final days value.
- Around line 37-38: Replace all instances of req.user.id with req.user._id in
the pat.controller.js file. The production JWT tokens use the _id field (as
defined in auth.controller.js), not id, so the three occurrences of req.user.id
at the affected lines (used in database query operations with $push, $inc, and
other update operators) must be changed to req.user._id to ensure proper
database lookups in production. This fix is necessary because the test suite
masks this issue by using _id in mock objects, but real JWT tokens will only
contain the _id field.

In `@apps/dashboard-api/src/middlewares/authenticateCLI.js`:
- Around line 28-48: The Redis cache operations (redis.get, redis.del, and
redis.setex) are awaited synchronously and can block authentication if Redis is
slow or unavailable. Wrap each Redis operation (the redis.get call at the
beginning, the redis.del call when developer is not found in cache, and the
redis.setex call when caching valid tokens) in separate try-catch blocks so that
Redis failures are non-blocking. When a Redis operation fails, log a warning but
allow the authentication flow to continue by falling through to database
validation. This ensures that authentication proceeds with database lookups even
when Redis is unavailable, making the cache truly best-effort rather than a
blocker for valid PAT authentication.
- Around line 50-52: In the authenticateCLI middleware where the matchedPat
validation check occurs, when matchedPat is falsy (indicating a revoked or
rotated token), the code should clear the stale cache entry before returning the
401 error. Modify the if (!matchedPat) block to delete the cached developerId
entry from the cache before calling next(new AppError(401, ...)) to prevent
repeated cache hits on subsequently invalid tokens. This allows the cache to
self-heal when a developer's token has been revoked or rotated.
- Line 2: The CACHE_TTL constant in the authenticateCLI.js file is being
assigned directly from the process.env.CLI_PAT_CACHE_TTL environment variable,
which is a string type. Since this value is passed to redis.setex() which
expects an integer for the TTL parameter, parse the environment variable to an
integer using parseInt() or Number() before assignment, maintaining the fallback
default of 300 if the environment variable is not set or is not a valid number.

In `@packages/common/src/models/Developer.js`:
- Around line 121-123: The pats field in the Developer model lacks an index on
the nested tokenHash field, causing collection scans when the auth middleware
queries on pats.tokenHash. Add a compound index on the pats.tokenHash field in
the Developer schema definition to optimize lookups. You can do this by adding
an index option to the pats field definition that creates an index specifically
for the tokenHash nested property within the pats array.

---

Nitpick comments:
In `@apps/dashboard-api/src/__tests__/routes.user.test.js`:
- Around line 45-50: Add three test cases after the mock definitions to verify
route wiring for the PAT endpoints. Create tests that make HTTP requests to POST
/api/user/pats (should trigger the mocked createPAT function), GET
/api/user/pats (should trigger the mocked listPATs function), and DELETE
/api/user/pats/:id (should trigger the mocked revokePAT function). Each test
should verify that the corresponding mocked handler function was called with the
request and response objects to ensure the routes are properly wired to their
controller handlers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2e0e56c9-c40c-40bd-9ac4-cec416adca00

📥 Commits

Reviewing files that changed from the base of the PR and between da5670f and 44dfd21.

📒 Files selected for processing (7)
  • apps/dashboard-api/src/__tests__/routes.user.test.js
  • apps/dashboard-api/src/controllers/pat.controller.js
  • apps/dashboard-api/src/middlewares/authenticateCLI.js
  • apps/dashboard-api/src/routes/user.js
  • packages/common/src/index.js
  • packages/common/src/models/Developer.js
  • packages/common/src/utils/token.utils.js

Comment thread apps/dashboard-api/src/controllers/pat.controller.js Outdated
Comment thread apps/dashboard-api/src/controllers/pat.controller.js Outdated
Comment thread apps/dashboard-api/src/controllers/pat.controller.js
Comment thread apps/dashboard-api/src/middlewares/authenticateCLI.js Outdated
Comment thread apps/dashboard-api/src/middlewares/authenticateCLI.js Outdated
Comment thread apps/dashboard-api/src/middlewares/authenticateCLI.js
Comment thread packages/common/src/models/Developer.js Outdated
@Ayush4958

Copy link
Copy Markdown
Contributor Author

@yash-pouranik
This PR is ready for review

@Ayush4958 Ayush4958 changed the title Enabling pat backend clean Enabling PAT backend Jun 23, 2026
Comment thread packages/common/src/models/Developer.js Outdated
Comment on lines +47 to +57
const patSchema = new mongoose.Schema({
tokenHash: { type: String, required: true },
suffix: { type: String, required: true },
label: { type: String, required: true },
type: { type: String, enum: ['human', 'agent'], required: true },
scopes: { type: [String], required: true },
lastUsedAt: { type: Date, default: null },
lastUsedIp: { type: String, default: null },
expiresAt: { type: Date, required: true },
createdAt: { type: Date, default: Date.now }
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add TTL to expire automatically, and default select to false.

Comment on lines +75 to +82
if (matchedPat.expiresAt && new Date() > new Date(matchedPat.expiresAt)) {
try {
await redis.del(cacheKey);
} catch (redisErr) {
console.warn("Redis DEL failed:", redisErr);
}
return next(new AppError(401, 'Unauthorized: Token has expired.'));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If in model we have added the ttl then we dont have to check the expiry.

Comment on lines +87 to +95
Developer.updateOne(
{ _id: developer._id, 'pats._id': matchedPat._id },
{
$set: {
'pats.$.lastUsedAt': new Date(),
'pats.$.lastUsedIp': ip
}
}
).catch(err => console.error("Failed to update PAT metadata:", err));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This query can b queued I think?

Comment on lines +39 to +72
if (developerId) {
developer = await Developer.findById(developerId);
if (!developer) {
try {
await redis.del(cacheKey);
} catch (redisErr) {
console.warn("Redis DEL failed:", redisErr);
}
return next(new AppError(401, 'Unauthorized: Developer not found.'));
}
matchedPat = developer.pats.find(p => p.tokenHash === tokenHash);
} else {
developer = await Developer.findOne({ 'pats.tokenHash': tokenHash });
if (!developer) {
return next(new AppError(401, 'Unauthorized: Invalid or revoked token.'));
}
matchedPat = developer.pats.find(p => p.tokenHash === tokenHash);

// Cache valid token to prevent DB hammering
try {
await redis.setex(cacheKey, CACHE_TTL, developer._id.toString());
} catch (redisErr) {
console.warn("Redis SETEX failed:", redisErr);
}
}

if (!matchedPat) {
try {
await redis.del(cacheKey);
} catch (redisErr) {
console.warn("Redis DEL failed:", redisErr);
}
return next(new AppError(401, 'Unauthorized: Invalid token state.'));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel this is not a true cache hit, since after retrieving the developerId from Redis we still perform a MongoDB lookup via Developer.findById().

Current flow:

Redis Hit > findById() > PAT lookup in document

So while it avoids the expensive findOne({ 'pats.tokenHash': ... }) query, it still hits the database on every authenticated request.

Would it make sense to cache the PAT auth context itself (developerId, patId, scopes, expiry, type, etc.) and invalidate the cache on PAT updates/revocation? That would allow Redis hits to bypass MongoDB entirely and provide a larger performance benefit for frequent CLI requests.

@yash-pouranik

Copy link
Copy Markdown
Member

@coderabbitai
please validate the comments

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch all review comments and issue comments on the PR
gh pr view 336 --repo geturbackend/urBackend --json comments,reviews,reviewThreads

Length of output: 1163


🏁 Script executed:

#!/bin/bash
# Fetch reviews and comments
gh pr view 336 --repo geturbackend/urBackend --json comments,reviews,latestReviews

echo "---REVIEW COMMENTS---"
# Fetch inline review comments via REST API
gh api repos/geturbackend/urBackend/pulls/336/comments --jq '.[] | {id: .id, user: .user.login, body: .body, path: .path, line: .line, diff_hunk: .diff_hunk}'

Length of output: 76777



@yash-pouranik Here's my validation of the four inline comments you left on the updated code:


Comment 1 — patSchema: Add TTL + select: false (Developer.js line 57)

Partially valid.

  • select: false on tokenHash — This is a good security practice. It prevents tokenHash from being accidentally leaked in responses when the full pats array is returned. It should be added:
    tokenHash: { type: String, required: true, select: false },
  • ⚠️ MongoDB TTL index on expiresAtThis won't work as expected. MongoDB TTL indexes only auto-expire top-level documents, not subdocuments embedded in an array. Since PATs are stored as elements inside developer.pats[], a TTL index on patSchema.expiresAt will have no effect. To auto-clean expired PATs you'd need a scheduled job (e.g., a cron or Bull queue task) that runs Developer.updateMany({}, { $pull: { pats: { expiresAt: { $lt: new Date() } } } }).

Comment 2 — Skip expiry check if model TTL handles it (authenticateCLI.js line 82)

Not valid with the current data model.

Because MongoDB TTL doesn't expire array subdocuments (as explained above), the application-level check:

if (matchedPat.expiresAt && new Date() > new Date(matchedPat.expiresAt))

is still required to block requests using an expired PAT. This check should stay until a cleanup job is in place.


Comment 3 — Queue the lastUsedAt/lastUsedIp update (authenticateCLI.js line 95)

Valid suggestion for production scale.

The current fire-and-forget .catch() approach is acceptable for now, but at high request rates it can create a backlog of DB writes. Using a queue (e.g., Bull/BullMQ backed by Redis, which is already available) would make these writes more reliable and batching-friendly. Good candidate for a follow-up improvement.


Comment 4 — Cache full PAT context instead of just developerId (authenticateCLI.js line 72)

Valid and well-reasoned.

You're right — a Redis hit currently still triggers Developer.findById(), so the DB is hit on every request. Caching the full auth context as a JSON blob would allow bypassing MongoDB entirely on cache hits:

// On cache miss, store full context:
const context = JSON.stringify({
    developerId: developer._id.toString(),
    patId: matchedPat._id.toString(),
    scopes: matchedPat.scopes,
    type: matchedPat.type,
    expiresAt: matchedPat.expiresAt
});
await redis.setex(cacheKey, CACHE_TTL, context);

// On cache hit:
const context = JSON.parse(cached);
req.cliScopes = context.scopes;
req.cliTokenType = context.type;
// skip DB entirely

The cache must be invalidated on revokePAT (already done) and also on updatePAT if that's added later. This is a meaningful performance win and worth implementing.


Summary:

Comment Valid? Action
select: false on tokenHash ✅ Yes Apply the change
MongoDB TTL on patSchema ⚠️ Incorrect Use a cleanup job instead
Skip app-level expiry check ❌ No Keep the check until cleanup job exists
Queue lastUsedAt writes ✅ Yes (future) Good follow-up task
Cache full PAT context ✅ Yes Recommended improvement

@yash-pouranik

Copy link
Copy Markdown
Member

cool
@coderabbitai

can we create pat as a seperate model that will never touch developer model??
or should keep this?
what github or stipe like big techs do??

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@yash-pouranik Great question — this is actually an important architectural decision. Here's what the industry does and my recommendation:


What GitHub & Stripe Do

GitHub stores PATs in a separate table (personal_access_tokens), not embedded in the User record. Each token row has a foreign key (user_id) pointing back to the user. This is why you can query GET /user/personal-access-tokens with pagination and why revoking one token doesn't require loading the entire user object.

Stripe does the same — API keys live in a separate restricted_keys / api_keys resource, independently addressable and queryable by key prefix.

The pattern is universal: any system that issues more than a handful of tokens per user (GitHub, Stripe, GitLab, Vercel, Netlify) uses a separate collection/table.


Why a Separate Model is Better Here

Concern Embedded (developer.pats[]) Separate PAT Collection
Lookup by tokenHash Must load entire Developer doc first Direct index: PAT.findOne({ tokenHash })
MongoDB 16 MB doc limit Risk if user creates many PATs Not a concern
Pagination Awkward slice of array Native .find().skip().limit()
authenticateCLI efficiency Redis hit → load Developer → linear scan for PAT Redis hit → PAT.findOne({ tokenHash }) with index
TTL / cleanup Can't use MongoDB TTL on subdocs Can use expireAfterSeconds index on expiresAt directly
Revoke by id $pull on developer array Simple PAT.deleteOne({ _id })
Analytics / audit log Buried in developer doc Independent queries

The biggest win for your use case: a separate collection fixes the MongoDB TTL problem from the previous comment — you can add { expiresAt: 1 }, { expireAfterSeconds: 0 } directly on the PAT collection and MongoDB will auto-delete expired tokens natively.


Suggested Schema

// packages/common/src/models/PAT.js
const patSchema = new mongoose.Schema({
  developer: { type: mongoose.Schema.Types.ObjectId, ref: 'Developer', required: true, index: true },
  tokenHash: { type: String, required: true, unique: true, select: false },
  suffix:    { type: String, required: true },
  label:     { type: String, required: true },
  type:      { type: String, enum: ['human', 'agent'], default: 'human' },
  scopes:    [{ type: String }],
  expiresAt: { type: Date, required: true, index: true }, // TTL index works here ✅
  lastUsedAt:  { type: Date, default: null },
  lastUsedIp:  { type: String, default: null },
  createdAt:   { type: Date, default: Date.now },
});

// Native MongoDB TTL — auto-deletes expired PATs
patSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });

module.exports = mongoose.model('PAT', patSchema);

And authenticateCLI simplifies to:

// Cache miss path — single indexed query, no Developer doc needed
const pat = await PAT.findOne({ tokenHash: hash }).select('+tokenHash').lean();
if (!pat) return next(new AppError(401, 'Invalid token'));

// Cache full context (fixes Comment 4 from earlier)
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify({
  developerId: pat.developer.toString(),
  patId: pat._id.toString(),
  scopes: pat.scopes,
  type: pat.type,
  expiresAt: pat.expiresAt,
}));

Recommendation

Switch to a separate PAT model. It resolves the TTL issue, the authenticateCLI DB-hit-on-every-request issue, and aligns with how GitHub/Stripe/Vercel handle it. The Developer model would just lose the pats field entirely — cleaner all around.

Would you like me to open a follow-up issue to track this refactor, or kick off the code changes directly?

@yash-pouranik

Copy link
Copy Markdown
Member

yes following this way will not have to run a cron just to expire those pats and in middleware we dont have to check for if that really exists or not
but one thing will be there that
if ttl is going to be delete by mongo in next 3 mins and some one just used if then we had already the key in redis for 5 mins. So it will even deleted pat will be available for extra 2 mins. that is not secure I feel.
@coderabbitai

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@yash-pouranik You've identified a real and important security gap. Here are two complementary fixes that together fully close it:


Fix 1 — Check expiresAt from the cached context on every hit

Since we're already proposing to cache the full PAT context as JSON (including expiresAt), you don't need a DB round-trip to validate expiry — just check the field on every cache hit:

const cached = await redis.get(cacheKey);
if (cached) {
  const context = JSON.parse(cached);

  // ✅ Check expiry even on cache hit — no DB needed
  if (new Date() > new Date(context.expiresAt)) {
    await redis.del(cacheKey); // evict stale key immediately
    return next(new AppError(401, 'Token has expired'));
  }

  req.cliScopes    = context.scopes;
  req.cliTokenType = context.type;
  // attach developer etc. from context...
  return next();
}

This makes the Redis cache authoritative for expiry — the 2-minute window is gone because we're checking the timestamp on every request, not relying on MongoDB to have cleaned up yet.


Fix 2 — Set Redis TTL = min(CACHE_TTL, remainingPATLifetime)

This prevents dead keys from lingering in Redis at all:

const remainingMs  = new Date(pat.expiresAt) - Date.now();
const remainingSec = Math.floor(remainingMs / 1000);
const redisTTL     = Math.min(CACHE_TTL, remainingSec);

if (redisTTL <= 0) {
  return next(new AppError(401, 'Token has expired'));
}

await redis.setex(cacheKey, redisTTL, JSON.stringify({ ...context }));

Combined result

sequenceDiagram
  participant C as CLI Client
  participant M as authenticateCLI
  participant R as Redis
  participant DB as MongoDB (PAT collection)

  C->>M: Bearer ubpat_...
  M->>R: GET cli:pat:cache:<hash>
  alt Cache HIT
    R-->>M: context JSON (incl. expiresAt)
    M->>M: if now > expiresAt → 401 + DEL key
    M->>C: 401 Expired (no DB hit)
  else Cache MISS
    M->>DB: PAT.findOne({ tokenHash })
    DB-->>M: PAT doc (or null → 401)
    M->>M: if now > expiresAt → 401
    M->>R: SETEX min(CACHE_TTL, remainingSecs) context
    M->>C: 200 OK
  end
Loading

With both fixes:

  • A PAT expiring in 3 min → Redis TTL is set to ≤3 min (Fix 2).
  • Even if a stale key somehow exists, the expiresAt check on cache hit will catch it (Fix 1).
  • MongoDB auto-deletes the expired doc eventually — you're not racing against it.

Would you like me to open a GitHub issue to track the refactor to a separate PAT model along with these middleware improvements?

@yash-pouranik

yash-pouranik commented Jun 23, 2026

Copy link
Copy Markdown
Member

okay cool
you can proceed with this (Read all the comments)
@Ayush4958

@Nitin-kumar-yadav1307

Copy link
Copy Markdown
Collaborator

Tip

For best results, initiate chat on the files or code changes.

@yash-pouranik You've identified a real and important security gap. Here are two complementary fixes that together fully close it:

Fix 1 — Check expiresAt from the cached context on every hit

Since we're already proposing to cache the full PAT context as JSON (including expiresAt), you don't need a DB round-trip to validate expiry — just check the field on every cache hit:

const cached = await redis.get(cacheKey);
if (cached) {
  const context = JSON.parse(cached);

  // ✅ Check expiry even on cache hit — no DB needed
  if (new Date() > new Date(context.expiresAt)) {
    await redis.del(cacheKey); // evict stale key immediately
    return next(new AppError(401, 'Token has expired'));
  }

  req.cliScopes    = context.scopes;
  req.cliTokenType = context.type;
  // attach developer etc. from context...
  return next();
}

This makes the Redis cache authoritative for expiry — the 2-minute window is gone because we're checking the timestamp on every request, not relying on MongoDB to have cleaned up yet.

Fix 2 — Set Redis TTL = min(CACHE_TTL, remainingPATLifetime)

This prevents dead keys from lingering in Redis at all:

const remainingMs  = new Date(pat.expiresAt) - Date.now();
const remainingSec = Math.floor(remainingMs / 1000);
const redisTTL     = Math.min(CACHE_TTL, remainingSec);

if (redisTTL <= 0) {
  return next(new AppError(401, 'Token has expired'));
}

await redis.setex(cacheKey, redisTTL, JSON.stringify({ ...context }));

Combined result

sequenceDiagram
  participant C as CLI Client
  participant M as authenticateCLI
  participant R as Redis
  participant DB as MongoDB (PAT collection)

  C->>M: Bearer ubpat_...
  M->>R: GET cli:pat:cache:<hash>
  alt Cache HIT
    R-->>M: context JSON (incl. expiresAt)
    M->>M: if now > expiresAt → 401 + DEL key
    M->>C: 401 Expired (no DB hit)
  else Cache MISS
    M->>DB: PAT.findOne({ tokenHash })
    DB-->>M: PAT doc (or null → 401)
    M->>M: if now > expiresAt → 401
    M->>R: SETEX min(CACHE_TTL, remainingSecs) context
    M->>C: 200 OK
  end
Loading

With both fixes:

  • A PAT expiring in 3 min → Redis TTL is set to ≤3 min (Fix 2).
  • Even if a stale key somehow exists, the expiresAt check on cache hit will catch it (Fix 1).
  • MongoDB auto-deletes the expired doc eventually — you're not racing against it.

Would you like me to open a GitHub issue to track the refactor to a separate PAT model along with these middleware improvements?

wow ! thats ...

@yash-pouranik

Copy link
Copy Markdown
Member

what happen sir?
@Nitin-kumar-yadav1307

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
apps/dashboard-api/src/middlewares/authenticateCLI.js (1)

113-114: 🎯 Functional Correctness | 🟠 Major

Set req.user._id for CLI-authenticated requests.
Dashboard controllers read req.user._id, so req.user = { id: ... } leaves those requests with no usable user id.

Suggested fix
-        req.user = { id: developer._id.toString() }; // Maintain compatibility with dashboard controllers
+        req.user = { _id: developer._id.toString() }; // Match dashboard controller expectations
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/middlewares/authenticateCLI.js` around lines 113 -
114, The CLI authentication flow in authenticateCLI currently sets req.user with
only an id field, which leaves dashboard controllers unable to read
req.user._id. Update the request shaping in authenticateCLI so req.user includes
the developer id under _id (while preserving any existing compatibility fields
if needed), and keep req.developer assigned to the developer object.
🧹 Nitpick comments (1)
apps/dashboard-api/src/middlewares/authenticateCLI.js (1)

80-87: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

!matchedPat branch is unreachable.

By this point matchedPat is always truthy: the cache-hit branch (Lines 40–49) constructs it, and the cache-miss branch returns early at Line 54 when the lookup is empty. This block can be removed to reduce noise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/middlewares/authenticateCLI.js` around lines 80 - 87,
The !matchedPat branch in authenticateCLI is unreachable because matchedPat is
already guaranteed to exist before this point; remove the entire conditional
block, including the Redis DEL cleanup and the AppError(401) return, to simplify
the control flow. Keep the surrounding authenticateCLI logic and its
cache-hit/cache-miss handling intact, and ensure any remaining token-state
validation is handled earlier where matchedPat is assigned or where the cache
lookup can fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/common/src/index.js`:
- Line 18: The PAT model is required in index.js but not exposed through
module.exports, so consumers destructuring PAT from `@urbackend/common` will get
undefined. Update the exports in packages/common/src/index.js to include PAT
alongside the other shared models, using the existing PAT require symbol so
pat.controller.js and authenticateCLI.js can access
PAT.create/find/findOne/updateOne correctly.

In `@packages/common/src/models/PAT.js`:
- Around line 15-22: The PAT schema defines a duplicate index on expiresAt,
which causes the Mongoose 8 warning because the TTL index already covers it.
Update the PAT model in the patSchema definition to remove the inline index from
the expiresAt field and keep the existing patSchema.index({ expiresAt: 1 }, {
expireAfterSeconds: 0 }) TTL index as the single source of indexing.

---

Outside diff comments:
In `@apps/dashboard-api/src/middlewares/authenticateCLI.js`:
- Around line 113-114: The CLI authentication flow in authenticateCLI currently
sets req.user with only an id field, which leaves dashboard controllers unable
to read req.user._id. Update the request shaping in authenticateCLI so req.user
includes the developer id under _id (while preserving any existing compatibility
fields if needed), and keep req.developer assigned to the developer object.

---

Nitpick comments:
In `@apps/dashboard-api/src/middlewares/authenticateCLI.js`:
- Around line 80-87: The !matchedPat branch in authenticateCLI is unreachable
because matchedPat is already guaranteed to exist before this point; remove the
entire conditional block, including the Redis DEL cleanup and the AppError(401)
return, to simplify the control flow. Keep the surrounding authenticateCLI logic
and its cache-hit/cache-miss handling intact, and ensure any remaining
token-state validation is handled earlier where matchedPat is assigned or where
the cache lookup can fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8021c4b-f700-428e-baf9-ee9a7e996946

📥 Commits

Reviewing files that changed from the base of the PR and between 44dfd21 and a358f14.

📒 Files selected for processing (5)
  • apps/dashboard-api/src/controllers/pat.controller.js
  • apps/dashboard-api/src/middlewares/authenticateCLI.js
  • packages/common/src/index.js
  • packages/common/src/models/Developer.js
  • packages/common/src/models/PAT.js
💤 Files with no reviewable changes (1)
  • packages/common/src/models/Developer.js

Comment thread packages/common/src/index.js
Comment thread packages/common/src/models/PAT.js Outdated
@Ayush4958

Copy link
Copy Markdown
Contributor Author

@yash-pouranik ,
all changes are applied

@yash-pouranik

yash-pouranik commented Jun 25, 2026

Copy link
Copy Markdown
Member

looks goot t ome

have a look
@coderabbitai

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

@yash-pouranik I’ll review the updated changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yash-pouranik

Copy link
Copy Markdown
Member

@coderabbitai
??

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@yash-pouranik The last action only triggered a normal incremental review, so it didn’t generate a fresh detailed pass.

What happened:

  • CodeRabbit had already reviewed the existing commits on this PR
  • the command I sent did not force a re-review of all changes
  • so you got the “review finished” message without new findings

If you want, ask me to review the whole PR again, and I’ll trigger a full re-review of #336.

(◔_◔)

@yash-pouranik

Copy link
Copy Markdown
Member

Please review the new commit on the basis of all the past comments and what we designed earlier>
@coderabbitai

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

@yash-pouranik I’ll do a full re-review of #336 against the earlier agreed design as well — especially the separate PAT model, expiry-safe Redis caching, and avoiding PAT state on the Developer model.

✅ Action performed

Full review finished.

@yash-pouranik

Copy link
Copy Markdown
Member

Leave, looks good to me
lets go.

@yash-pouranik
yash-pouranik merged commit b530f94 into geturbackend:main Jun 25, 2026
8 checks passed
@yash-pouranik

Copy link
Copy Markdown
Member

now UI for this
@Ayush4958

@Ayush4958

Copy link
Copy Markdown
Contributor Author

@yash-pouranik
Pls add gssoc labels

@yash-pouranik

Copy link
Copy Markdown
Member

isnot GSSOC ended??

@Ayush4958

Copy link
Copy Markdown
Contributor Author

I don't think so

@coderabbitai coderabbitai Bot mentioned this pull request Jun 26, 2026
17 tasks
@Nitin-kumar-yadav1307

Copy link
Copy Markdown
Collaborator

@coderabbitai tell me PAT apis and location

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Nitin-kumar-yadav1307 PAT APIs are added on the user router here:

  • apps/dashboard-api/src/routes/user.js

Routes registered there:

  • POST /pats → create a PAT
  • GET /pats → list PATs
  • DELETE /pats/:id → revoke a PAT

Controller location:

  • apps/dashboard-api/src/controllers/pat.controller.js
    • createPAT
    • listPATs
    • revokePAT

Related PAT files:

  • Auth middleware for CLI token validation: apps/dashboard-api/src/middlewares/authenticateCLI.js
  • PAT model: packages/common/src/models/PAT.js
  • Token generation/hash utils: packages/common/src/utils/token.utils.js

One note: the exact full API URL depends on where apps/dashboard-api/src/routes/user.js is mounted in the app, but inside that router the PAT paths are /pats, /pats, and /pats/:id.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants