@manojkmfsi/monodog 1.1.17 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/CHANGELOG.md +12 -0
- package/dist/middleware/auth-middleware.js +186 -0
- package/dist/middleware/server-startup.js +25 -0
- package/dist/repositories/package-repository.js +8 -16
- package/dist/routes/auth-routes.js +342 -0
- package/dist/routes/permission-routes.js +161 -0
- package/dist/serve.js +8 -2
- package/dist/services/github-oauth-service.js +223 -0
- package/dist/services/permission-service.js +174 -0
- package/dist/types/auth.js +5 -0
- package/monodog-dashboard/dist/assets/index-DN0rk9Ub.css +1 -0
- package/monodog-dashboard/dist/assets/index-DS89XTlx.js +12 -0
- package/monodog-dashboard/dist/index.html +2 -3
- package/package.json +3 -1
- package/monodog-dashboard/dist/assets/index-16320658.css +0 -1
- package/monodog-dashboard/dist/assets/index-f4941551.js +0 -71
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Repository Permission Routes
|
|
4
|
+
* Handles checking and managing repository permissions
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const auth_middleware_1 = require("../middleware/auth-middleware");
|
|
9
|
+
const permission_service_1 = require("../services/permission-service");
|
|
10
|
+
const logger_1 = require("../middleware/logger");
|
|
11
|
+
const router = (0, express_1.Router)();
|
|
12
|
+
/**
|
|
13
|
+
* Get user's permission for a specific repository
|
|
14
|
+
* GET /permissions/:owner/:repo
|
|
15
|
+
*/
|
|
16
|
+
router.get('/:owner/:repo', auth_middleware_1.authenticationMiddleware, async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
19
|
+
if (!session) {
|
|
20
|
+
res.status(401).json({
|
|
21
|
+
error: 'Unauthorized',
|
|
22
|
+
message: 'No active session',
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const { owner, repo } = req.params;
|
|
27
|
+
const forceRefresh = req.query.refresh === 'true';
|
|
28
|
+
if (!owner || !repo) {
|
|
29
|
+
res.status(400).json({
|
|
30
|
+
error: 'Bad request',
|
|
31
|
+
message: 'Owner and repo parameters are required',
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
logger_1.AppLogger.debug(`Checking permission for ${session.user.login} in ${owner}/${repo}`);
|
|
36
|
+
// Get permission from cache or GitHub API
|
|
37
|
+
const cachedPermission = await (0, permission_service_1.getUserRepositoryPermission)(session.accessToken, session.user.id, session.user.login, owner, repo, forceRefresh);
|
|
38
|
+
const response = {
|
|
39
|
+
permission: cachedPermission.permission,
|
|
40
|
+
role: cachedPermission.role,
|
|
41
|
+
canAdmin: cachedPermission.permission === 'admin',
|
|
42
|
+
canMaintain: (0, permission_service_1.canPerformAction)(cachedPermission.permission, 'maintain'),
|
|
43
|
+
canWrite: (0, permission_service_1.canPerformAction)(cachedPermission.permission, 'write'),
|
|
44
|
+
canRead: (0, permission_service_1.canPerformAction)(cachedPermission.permission, 'read'),
|
|
45
|
+
denied: cachedPermission.permission === 'none',
|
|
46
|
+
};
|
|
47
|
+
res.json({
|
|
48
|
+
success: true,
|
|
49
|
+
owner,
|
|
50
|
+
repo,
|
|
51
|
+
user: session.user.login,
|
|
52
|
+
...response,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
logger_1.AppLogger.error(`Failed to check permission: ${error}`);
|
|
57
|
+
res.status(500).json({
|
|
58
|
+
success: false,
|
|
59
|
+
error: 'Internal server error',
|
|
60
|
+
message: 'Failed to check repository permission',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Check if user can perform a specific action
|
|
66
|
+
* POST /permissions/:owner/:repo/can-action
|
|
67
|
+
*/
|
|
68
|
+
router.post('/:owner/:repo/can-action', auth_middleware_1.authenticationMiddleware, async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
71
|
+
if (!session) {
|
|
72
|
+
res.status(401).json({
|
|
73
|
+
error: 'Unauthorized',
|
|
74
|
+
message: 'No active session',
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const { owner, repo } = req.params;
|
|
79
|
+
const { action } = req.body;
|
|
80
|
+
if (!owner || !repo) {
|
|
81
|
+
res.status(400).json({
|
|
82
|
+
error: 'Bad request',
|
|
83
|
+
message: 'Owner and repo parameters are required',
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!action || !['read', 'write', 'maintain', 'admin'].includes(action)) {
|
|
88
|
+
res.status(400).json({
|
|
89
|
+
error: 'Bad request',
|
|
90
|
+
message: 'Valid action is required (read, write, maintain, or admin)',
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
logger_1.AppLogger.debug(`Checking if ${session.user.login} can perform '${action}' in ${owner}/${repo}`);
|
|
95
|
+
// Get permission
|
|
96
|
+
const cachedPermission = await (0, permission_service_1.getUserRepositoryPermission)(session.accessToken, session.user.id, session.user.login, owner, repo);
|
|
97
|
+
// Check if user can perform action
|
|
98
|
+
const can = (0, permission_service_1.canPerformAction)(cachedPermission.permission, action);
|
|
99
|
+
res.json({
|
|
100
|
+
success: true,
|
|
101
|
+
owner,
|
|
102
|
+
repo,
|
|
103
|
+
user: session.user.login,
|
|
104
|
+
action,
|
|
105
|
+
can,
|
|
106
|
+
permission: cachedPermission.permission,
|
|
107
|
+
role: cachedPermission.role,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
logger_1.AppLogger.error(`Failed to check action permission: ${error}`);
|
|
112
|
+
res.status(500).json({
|
|
113
|
+
success: false,
|
|
114
|
+
error: 'Internal server error',
|
|
115
|
+
message: 'Failed to check action permission',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* Invalidate permission cache for a repository
|
|
121
|
+
* POST /permissions/:owner/:repo/invalidate
|
|
122
|
+
*/
|
|
123
|
+
router.post('/:owner/:repo/invalidate', auth_middleware_1.authenticationMiddleware, (req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
126
|
+
if (!session) {
|
|
127
|
+
res.status(401).json({
|
|
128
|
+
error: 'Unauthorized',
|
|
129
|
+
message: 'No active session',
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const { owner, repo } = req.params;
|
|
134
|
+
if (!owner || !repo) {
|
|
135
|
+
res.status(400).json({
|
|
136
|
+
error: 'Bad request',
|
|
137
|
+
message: 'Owner and repo parameters are required',
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
logger_1.AppLogger.debug(`Invalidating permission cache for ${session.user.login} in ${owner}/${repo}`);
|
|
142
|
+
// Invalidate cache
|
|
143
|
+
(0, permission_service_1.invalidatePermissionCache)(session.user.id, owner, repo);
|
|
144
|
+
res.json({
|
|
145
|
+
success: true,
|
|
146
|
+
message: 'Permission cache invalidated',
|
|
147
|
+
owner,
|
|
148
|
+
repo,
|
|
149
|
+
user: session.user.login,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
logger_1.AppLogger.error(`Failed to invalidate cache: ${error}`);
|
|
154
|
+
res.status(500).json({
|
|
155
|
+
success: false,
|
|
156
|
+
error: 'Internal server error',
|
|
157
|
+
message: 'Failed to invalidate permission cache',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
exports.default = router;
|
package/dist/serve.js
CHANGED
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
* 1. Start the API server for the dashboard.
|
|
9
9
|
* 2. Start serving the dashboard frontend.
|
|
10
10
|
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
12
17
|
const index_1 = require("./index");
|
|
13
18
|
const utilities_1 = require("./utils/utilities");
|
|
14
|
-
let logLevel = '';
|
|
15
|
-
let nodeEnv = 'production';
|
|
19
|
+
let logLevel = process.env.LOG_LEVEL || 'info';
|
|
20
|
+
let nodeEnv = process.env.NODE_ENV || 'production';
|
|
16
21
|
const args = process.argv;
|
|
17
22
|
if (args.includes('--dev')) {
|
|
18
23
|
nodeEnv = 'development';
|
|
@@ -24,6 +29,7 @@ if (args.includes('--debug')) {
|
|
|
24
29
|
else if (args.includes('--info')) {
|
|
25
30
|
logLevel = 'info';
|
|
26
31
|
}
|
|
32
|
+
dotenv_1.default.config({ path: path_1.default.resolve(process.cwd(), '.env') });
|
|
27
33
|
process.env.LOG_LEVEL = logLevel;
|
|
28
34
|
process.env.NODE_ENV = nodeEnv;
|
|
29
35
|
const rootPath = (0, utilities_1.findMonorepoRoot)();
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub API Service
|
|
4
|
+
* Handles all GitHub API interactions including OAuth and permission checks
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.exchangeCodeForToken = exchangeCodeForToken;
|
|
11
|
+
exports.getAuthenticatedUser = getAuthenticatedUser;
|
|
12
|
+
exports.getUserEmail = getUserEmail;
|
|
13
|
+
exports.getRepositoryPermission = getRepositoryPermission;
|
|
14
|
+
exports.mapPermissionToRole = mapPermissionToRole;
|
|
15
|
+
exports.hasPermission = hasPermission;
|
|
16
|
+
exports.validateToken = validateToken;
|
|
17
|
+
exports.generateAuthorizationUrl = generateAuthorizationUrl;
|
|
18
|
+
const https_1 = __importDefault(require("https"));
|
|
19
|
+
const logger_1 = require("../middleware/logger");
|
|
20
|
+
const GITHUB_API_BASE = 'https://api.github.com';
|
|
21
|
+
const GITHUB_OAUTH_BASE = 'https://github.com';
|
|
22
|
+
/**
|
|
23
|
+
* Make an HTTPS request to GitHub API
|
|
24
|
+
*/
|
|
25
|
+
function makeGitHubRequest(options, data) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const request = https_1.default.request(options, (response) => {
|
|
28
|
+
let body = '';
|
|
29
|
+
response.on('data', (chunk) => {
|
|
30
|
+
body += chunk;
|
|
31
|
+
});
|
|
32
|
+
response.on('end', () => {
|
|
33
|
+
try {
|
|
34
|
+
if (response.statusCode && response.statusCode >= 400) {
|
|
35
|
+
reject(new Error(`GitHub API error: ${response.statusCode} - ${body}`));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const result = body ? JSON.parse(body) : {};
|
|
39
|
+
resolve(result);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
reject(new Error(`Failed to parse GitHub API response: ${error}`));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
request.on('error', (error) => {
|
|
48
|
+
logger_1.AppLogger.error(`GitHub API request failed: ${error.message}`);
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
request.setTimeout(10000, () => {
|
|
52
|
+
request.destroy();
|
|
53
|
+
reject(new Error('GitHub API request timeout'));
|
|
54
|
+
});
|
|
55
|
+
if (data) {
|
|
56
|
+
request.write(data);
|
|
57
|
+
}
|
|
58
|
+
request.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Exchange OAuth code for access token
|
|
63
|
+
*/
|
|
64
|
+
async function exchangeCodeForToken(code, clientId, clientSecret, redirectUri) {
|
|
65
|
+
const payload = JSON.stringify({
|
|
66
|
+
client_id: clientId,
|
|
67
|
+
client_secret: clientSecret,
|
|
68
|
+
code,
|
|
69
|
+
redirect_uri: redirectUri,
|
|
70
|
+
});
|
|
71
|
+
const options = {
|
|
72
|
+
hostname: 'github.com',
|
|
73
|
+
path: '/login/oauth/access_token',
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'Content-Length': String(Buffer.byteLength(payload)),
|
|
78
|
+
Accept: 'application/json',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
const response = await makeGitHubRequest(options, payload);
|
|
83
|
+
if (response.error) {
|
|
84
|
+
throw new Error(`OAuth exchange failed: ${response.error}`);
|
|
85
|
+
}
|
|
86
|
+
logger_1.AppLogger.debug('Successfully exchanged OAuth code for access token');
|
|
87
|
+
return response;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger_1.AppLogger.error(`Failed to exchange OAuth code: ${error}`);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get authenticated user information
|
|
96
|
+
*/
|
|
97
|
+
async function getAuthenticatedUser(accessToken) {
|
|
98
|
+
const options = {
|
|
99
|
+
hostname: 'api.github.com',
|
|
100
|
+
path: '/user',
|
|
101
|
+
method: 'GET',
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: `Bearer ${accessToken}`,
|
|
104
|
+
'User-Agent': 'MonoDog',
|
|
105
|
+
Accept: 'application/vnd.github+json',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
const user = await makeGitHubRequest(options);
|
|
110
|
+
logger_1.AppLogger.debug(`Retrieved user info: ${user.login}`);
|
|
111
|
+
return user;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
logger_1.AppLogger.error(`Failed to get user info: ${error}`);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get user's email from GitHub (with proper scopes)
|
|
120
|
+
*/
|
|
121
|
+
async function getUserEmail(accessToken) {
|
|
122
|
+
const options = {
|
|
123
|
+
hostname: 'api.github.com',
|
|
124
|
+
path: '/user/emails',
|
|
125
|
+
method: 'GET',
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${accessToken}`,
|
|
128
|
+
'User-Agent': 'MonoDog',
|
|
129
|
+
Accept: 'application/vnd.github+json',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
const emails = await makeGitHubRequest(options);
|
|
134
|
+
const primaryEmail = emails.find((e) => e.primary && e.verified);
|
|
135
|
+
return primaryEmail?.email || null;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
logger_1.AppLogger.warn(`Failed to get user email: ${error}`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get user's permission for a specific repository
|
|
144
|
+
* Returns the user's permission level in the target repository
|
|
145
|
+
*/
|
|
146
|
+
async function getRepositoryPermission(accessToken, owner, repo, username) {
|
|
147
|
+
const options = {
|
|
148
|
+
hostname: 'api.github.com',
|
|
149
|
+
path: `/repos/${owner}/${repo}/collaborators/${username}/permission`,
|
|
150
|
+
method: 'GET',
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${accessToken}`,
|
|
153
|
+
'User-Agent': 'MonoDog',
|
|
154
|
+
Accept: 'application/vnd.github+json',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
const response = await makeGitHubRequest(options);
|
|
159
|
+
logger_1.AppLogger.debug(`Retrieved permission for ${username} in ${owner}/${repo}: ${response.permission}`);
|
|
160
|
+
return response;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
logger_1.AppLogger.warn(`Failed to get repository permission for ${username} in ${owner}/${repo}: ${error}`);
|
|
164
|
+
// If error (likely 404 or no access), return 'none' permission
|
|
165
|
+
return { permission: 'none' };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Map GitHub permission to MonoDog role
|
|
170
|
+
*/
|
|
171
|
+
function mapPermissionToRole(permission) {
|
|
172
|
+
switch (permission) {
|
|
173
|
+
case 'admin':
|
|
174
|
+
return 'Admin';
|
|
175
|
+
case 'maintain':
|
|
176
|
+
return 'Maintainer';
|
|
177
|
+
case 'write':
|
|
178
|
+
case 'read':
|
|
179
|
+
return 'Collaborator';
|
|
180
|
+
case 'none':
|
|
181
|
+
default:
|
|
182
|
+
return 'Denied';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Check if user has required permission level
|
|
187
|
+
*/
|
|
188
|
+
function hasPermission(userPermission, requiredPermission) {
|
|
189
|
+
const permissionHierarchy = {
|
|
190
|
+
admin: 4,
|
|
191
|
+
maintain: 3,
|
|
192
|
+
write: 2,
|
|
193
|
+
read: 1,
|
|
194
|
+
none: 0,
|
|
195
|
+
};
|
|
196
|
+
return (permissionHierarchy[userPermission] >= permissionHierarchy[requiredPermission]);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Validate OAuth token is still valid
|
|
200
|
+
*/
|
|
201
|
+
async function validateToken(accessToken) {
|
|
202
|
+
try {
|
|
203
|
+
await getAuthenticatedUser(accessToken);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
logger_1.AppLogger.warn(`Token validation failed: ${error}`);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Generate OAuth authorization URL
|
|
213
|
+
*/
|
|
214
|
+
function generateAuthorizationUrl(clientId, redirectUri, state, scopes = ['read:user', 'user:email', 'repo']) {
|
|
215
|
+
const params = new URLSearchParams({
|
|
216
|
+
client_id: clientId,
|
|
217
|
+
redirect_uri: redirectUri,
|
|
218
|
+
state,
|
|
219
|
+
scope: scopes.join(','),
|
|
220
|
+
allow_signup: 'true',
|
|
221
|
+
});
|
|
222
|
+
return `${GITHUB_OAUTH_BASE}/login/oauth/authorize?${params.toString()}`;
|
|
223
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Permission Service
|
|
4
|
+
* Manages repository permission caching and validation
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.startCacheCleanup = startCacheCleanup;
|
|
8
|
+
exports.getUserRepositoryPermission = getUserRepositoryPermission;
|
|
9
|
+
exports.invalidatePermissionCache = invalidatePermissionCache;
|
|
10
|
+
exports.invalidateUserCache = invalidateUserCache;
|
|
11
|
+
exports.clearAllCache = clearAllCache;
|
|
12
|
+
exports.getCacheStats = getCacheStats;
|
|
13
|
+
exports.canPerformAction = canPerformAction;
|
|
14
|
+
const github_oauth_service_1 = require("./github-oauth-service");
|
|
15
|
+
const logger_1 = require("../middleware/logger");
|
|
16
|
+
// Cache storage: key = `${userId}:${owner}/${repo}`
|
|
17
|
+
const permissionCache = new Map();
|
|
18
|
+
// Configuration
|
|
19
|
+
const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
|
|
20
|
+
const MAX_CACHE_SIZE = 10000;
|
|
21
|
+
const CLEANUP_INTERVAL = 60 * 1000; // 1 minute
|
|
22
|
+
/**
|
|
23
|
+
* Start periodic cleanup of expired cache entries
|
|
24
|
+
*/
|
|
25
|
+
function startCacheCleanup() {
|
|
26
|
+
setInterval(() => {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
let cleanedCount = 0;
|
|
29
|
+
for (const [key, entry] of permissionCache.entries()) {
|
|
30
|
+
if (now > entry.cachedAt + entry.ttl) {
|
|
31
|
+
permissionCache.delete(key);
|
|
32
|
+
cleanedCount++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (cleanedCount > 0) {
|
|
36
|
+
logger_1.AppLogger.debug(`Cleaned ${cleanedCount} expired permission cache entries`);
|
|
37
|
+
}
|
|
38
|
+
}, CLEANUP_INTERVAL);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate cache key
|
|
42
|
+
*/
|
|
43
|
+
function getCacheKey(userId, owner, repo) {
|
|
44
|
+
return `${userId}:${owner}/${repo}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get cached permission if still valid
|
|
48
|
+
*/
|
|
49
|
+
function getCachedPermission(userId, owner, repo) {
|
|
50
|
+
const key = getCacheKey(userId, owner, repo);
|
|
51
|
+
const cached = permissionCache.get(key);
|
|
52
|
+
if (!cached) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
if (now > cached.cachedAt + cached.ttl) {
|
|
57
|
+
// Cache expired
|
|
58
|
+
permissionCache.delete(key);
|
|
59
|
+
logger_1.AppLogger.debug(`Cache expired for ${key}`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Set permission in cache
|
|
66
|
+
*/
|
|
67
|
+
function setCachedPermission(userId, username, owner, repo, permission, ttl = DEFAULT_TTL) {
|
|
68
|
+
const role = (0, github_oauth_service_1.mapPermissionToRole)(permission);
|
|
69
|
+
const cached = {
|
|
70
|
+
userId,
|
|
71
|
+
username,
|
|
72
|
+
owner,
|
|
73
|
+
repo,
|
|
74
|
+
permission,
|
|
75
|
+
role,
|
|
76
|
+
cachedAt: Date.now(),
|
|
77
|
+
ttl,
|
|
78
|
+
};
|
|
79
|
+
const key = getCacheKey(userId, owner, repo);
|
|
80
|
+
// Check cache size and evict oldest if needed
|
|
81
|
+
if (permissionCache.size >= MAX_CACHE_SIZE) {
|
|
82
|
+
let oldestKey = '';
|
|
83
|
+
let oldestTime = Date.now();
|
|
84
|
+
for (const [k, entry] of permissionCache.entries()) {
|
|
85
|
+
if (entry.cachedAt < oldestTime) {
|
|
86
|
+
oldestTime = entry.cachedAt;
|
|
87
|
+
oldestKey = k;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (oldestKey) {
|
|
91
|
+
permissionCache.delete(oldestKey);
|
|
92
|
+
logger_1.AppLogger.debug(`Evicted oldest cache entry: ${oldestKey}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
permissionCache.set(key, cached);
|
|
96
|
+
logger_1.AppLogger.debug(`Cached permission for ${key}: ${permission}`);
|
|
97
|
+
return cached;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get user's permission for a repository
|
|
101
|
+
* Checks cache first, then queries GitHub API if needed
|
|
102
|
+
*/
|
|
103
|
+
async function getUserRepositoryPermission(accessToken, userId, username, owner, repo, forceRefresh = false) {
|
|
104
|
+
// Check cache if not forcing refresh
|
|
105
|
+
if (!forceRefresh) {
|
|
106
|
+
const cached = getCachedPermission(userId, owner, repo);
|
|
107
|
+
if (cached) {
|
|
108
|
+
return cached;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Query GitHub API
|
|
112
|
+
logger_1.AppLogger.debug(`Querying GitHub API for ${username}'s permission in ${owner}/${repo}`);
|
|
113
|
+
try {
|
|
114
|
+
const response = await (0, github_oauth_service_1.getRepositoryPermission)(accessToken, owner, repo, username);
|
|
115
|
+
return setCachedPermission(userId, username, owner, repo, response.permission);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger_1.AppLogger.error(`Failed to get repository permission: ${error}`);
|
|
119
|
+
// Return 'none' permission on error
|
|
120
|
+
return setCachedPermission(userId, username, owner, repo, 'none');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Invalidate permission cache for a user-repository pair
|
|
125
|
+
*/
|
|
126
|
+
function invalidatePermissionCache(userId, owner, repo) {
|
|
127
|
+
const key = getCacheKey(userId, owner, repo);
|
|
128
|
+
permissionCache.delete(key);
|
|
129
|
+
logger_1.AppLogger.debug(`Invalidated cache for ${key}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Invalidate all permissions for a user
|
|
133
|
+
*/
|
|
134
|
+
function invalidateUserCache(userId) {
|
|
135
|
+
let invalidatedCount = 0;
|
|
136
|
+
for (const key of permissionCache.keys()) {
|
|
137
|
+
if (key.startsWith(`${userId}:`)) {
|
|
138
|
+
permissionCache.delete(key);
|
|
139
|
+
invalidatedCount++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
logger_1.AppLogger.debug(`Invalidated ${invalidatedCount} cache entries for user ${userId}`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Clear all permission cache (useful for testing)
|
|
146
|
+
*/
|
|
147
|
+
function clearAllCache() {
|
|
148
|
+
const size = permissionCache.size;
|
|
149
|
+
permissionCache.clear();
|
|
150
|
+
logger_1.AppLogger.info(`Cleared all permission cache (${size} entries)`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get cache statistics
|
|
154
|
+
*/
|
|
155
|
+
function getCacheStats() {
|
|
156
|
+
return {
|
|
157
|
+
size: permissionCache.size,
|
|
158
|
+
capacity: MAX_CACHE_SIZE,
|
|
159
|
+
utilizationPercent: (permissionCache.size / MAX_CACHE_SIZE) * 100,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if user can perform an action based on permission
|
|
164
|
+
*/
|
|
165
|
+
function canPerformAction(permission, requiredAction) {
|
|
166
|
+
const actionPermissionMap = {
|
|
167
|
+
read: ['read', 'write', 'maintain', 'admin'],
|
|
168
|
+
write: ['write', 'maintain', 'admin'],
|
|
169
|
+
maintain: ['maintain', 'admin'],
|
|
170
|
+
admin: ['admin'],
|
|
171
|
+
};
|
|
172
|
+
const allowedPermissions = actionPermissionMap[requiredAction] || [];
|
|
173
|
+
return allowedPermissions.includes(permission);
|
|
174
|
+
}
|