Skip to content

Commit be2cef7

Browse files
feat(cli): implement official urBackend CLI (@urbackend/cli) (#340)
* feat(cli): scaffold official CLI package * feat(cli): initialize CLI with Commander * feat(cli): scaffold official urBackend CLI authentication * feat(cli): implement authentication commands * pre-ui PAT commit * final commit- official urBackend CLI * ci: add REDIS_URL env for dashboard-api test job * test: mock authenticateCLI and authFlexible in routes.user.test * test: mock cli.controller to prevent ESM marked import in Jest * proposed fix by coderabbit * fixed ci.yml * coderabbit agentic fix * revert: restore NODE_ENV guard in authMiddleware console.error * removed extra landing space in ci.yml
1 parent 03e973e commit be2cef7

51 files changed

Lines changed: 5595 additions & 1265 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ jobs:
2727

2828
dashboard-api-tests:
2929
runs-on: ubuntu-latest
30+
services:
31+
redis:
32+
image: redis:7-alpine
33+
ports:
34+
- 6379:6379
35+
options: >-
36+
--health-cmd "redis-cli ping"
37+
--health-interval 10s
38+
--health-timeout 5s
39+
--health-retries 5
3040
permissions:
3141
contents: read
3242
steps:
@@ -44,6 +54,8 @@ jobs:
4454

4555
- name: Run Dashboard API Tests
4656
run: npm run test --workspace=dashboard-api
57+
env:
58+
REDIS_URL: redis://localhost:6379
4759

4860
public-api-tests:
4961
runs-on: ubuntu-latest

apps/dashboard-api/src/__tests__/routes.user.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,25 @@ jest.mock('../controllers/pat.controller', () => ({
4848
revokePAT: jest.fn((_req, res) => res.json({ success: true }))
4949
}));
5050

51+
jest.mock('../middlewares/authenticateCLI', () =>
52+
jest.fn((req, _res, next) => {
53+
req.user = { _id: 'mock_user_id', id: 'mock_user_id' };
54+
req.cliScopes = ['read', 'write'];
55+
next();
56+
})
57+
);
58+
59+
jest.mock('../middlewares/authFlexible', () =>
60+
jest.fn((req, _res, next) => {
61+
req.user = { _id: 'mock_user_id', id: 'mock_user_id' };
62+
next();
63+
})
64+
);
65+
66+
jest.mock('../controllers/cli.controller', () => ({
67+
getCLIProfile: jest.fn((_req, res) => res.json({ success: true, data: {} }))
68+
}));
69+
5170
const express = require('express');
5271
const request = require('supertest');
5372
const userRouter = require('../routes/user');

apps/dashboard-api/src/app.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,18 @@ const csrfProtection = csurf({
7575
}
7676
});
7777

78+
// AFTER
7879
app.use((req, res, next) => {
7980
// Exclude Razorpay webhook from CSRF protection since it's an external POST request
80-
if (req.path === '/api/billing/webhook') {
81+
// Exclude CLI routes — CLI authenticates via Bearer PAT, not cookies
82+
const isCliRoute =
83+
req.path === '/api/user/cli' || req.path.startsWith('/api/user/cli/');
84+
if (req.path === '/api/billing/webhook' || isCliRoute) {
8185
return next();
8286
}
8387
csrfProtection(req, res, next);
8488
});
8589

86-
8790
if (process.env.NODE_ENV !== 'test') {
8891
garbageCollect();
8992
storageGarbageCollect();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const { Developer, ApiResponse, AppError } = require("@urbackend/common");
2+
3+
module.exports.getCLIProfile = async (req, res, next) => {
4+
try {
5+
const developer = await Developer.findById(req.user._id)
6+
.select("email plan githubUsername avatarUrl");
7+
8+
if (!developer) {
9+
return next(new AppError(404, "Developer not found"));
10+
}
11+
12+
return new ApiResponse({
13+
developer: {
14+
id: developer._id,
15+
email: developer.email,
16+
plan: developer.plan,
17+
githubUsername: developer.githubUsername,
18+
avatarUrl: developer.avatarUrl,
19+
},
20+
auth: {
21+
scopes: req.cliScopes,
22+
tokenType: req.cliTokenType,
23+
},
24+
}).send(res);
25+
} catch (err) {
26+
next(err);
27+
}
28+
};

apps/dashboard-api/src/controllers/pat.controller.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,4 @@ exports.revokePAT = async (req, res, next) => {
104104
return next(new AppError(500, "An error occurred while revoking the token"));
105105
}
106106
};
107+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const authMiddleware = require('./authMiddleware');
2+
const authenticateCLI = require('./authenticateCLI');
3+
4+
/**
5+
* Flexible auth middleware — accepts either:
6+
* - Session cookie (browser dashboard)
7+
* - Bearer PAT token (CLI)
8+
*/
9+
const authFlexible = (req, res, next) => {
10+
const authHeader = req.headers.authorization;
11+
12+
if (authHeader && authHeader.startsWith('Bearer ubpat_')) {
13+
return authenticateCLI(req, res, next);
14+
}
15+
16+
return authMiddleware(req, res, next);
17+
};
18+
19+
module.exports = authFlexible;

apps/dashboard-api/src/middlewares/authMiddleware.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,10 @@ module.exports = function (req, res, next) {
3131

3232
// Proceed to the next middleware or route handler
3333
next();
34-
} catch (err) {
35-
if (process.env.NODE_ENV !== 'test') {
36-
console.error(err);
37-
}
38-
39-
return next(new AppError(401, 'Invalid Token'));
34+
} catch (err) {
35+
if (process.env.NODE_ENV !== 'test') {
36+
console.error(err);
4037
}
38+
return next(new AppError(401, "Invalid Token"));
39+
}
4140
};

apps/dashboard-api/src/middlewares/authenticateCLI.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const authenticateCLI = async (req, res, next) => {
110110
).catch(err => console.error("Failed to update PAT metadata:", err));
111111

112112
// Attach developer and PAT scopes for downstream controllers
113-
req.user = { id: developer._id.toString() }; // Maintain compatibility with dashboard controllers
113+
req.user = { _id: developer._id.toString(), id: developer._id.toString() }; // Maintain compatibility with dashboard controllers
114114
req.developer = developer;
115115
req.cliScopes = matchedPat.scopes;
116116
req.cliTokenType = matchedPat.type;

apps/dashboard-api/src/routes/analytics.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ const express = require("express");
22
const router = express.Router();
33
const analyticsController = require("../controllers/analytics.controller");
44
const authMiddleware = require("../middlewares/authMiddleware");
5+
const authFlexible = require("../middlewares/authFlexible");
56

6-
router.get("/stats", authMiddleware, analyticsController.getGlobalStats);
7-
router.get("/activity", authMiddleware, analyticsController.getRecentActivity);
7+
router.get("/stats", authFlexible, analyticsController.getGlobalStats);
8+
router.get("/activity", authFlexible, analyticsController.getRecentActivity);
89

910
// --- Metrics Stack ---
1011
router.get("/funnel", authMiddleware, analyticsController.getActivationFunnel);

apps/dashboard-api/src/routes/cli.routes.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)