@manojkmfsi/monodog 1.1.30 → 1.1.31
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 +1 -15
- package/CHANGELOG.md +12 -0
- package/dist/controllers/auth-controller.js +196 -0
- package/dist/controllers/permission-controller.js +152 -0
- package/dist/routes/auth-routes.js +7 -338
- package/dist/routes/permission-routes.js +4 -139
- package/dist/services/auth-service.js +239 -0
- package/dist/services/permission-service.js +30 -0
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
# GitHub OAuth Configuration
|
|
2
2
|
GITHUB_CLIENT_ID=your_client_id_here
|
|
3
3
|
GITHUB_CLIENT_SECRET=your_client_secret_here
|
|
4
|
-
OAUTH_REDIRECT_URI=http://localhost:
|
|
4
|
+
OAUTH_REDIRECT_URI=http://localhost:3010/auth/callback
|
|
5
5
|
|
|
6
|
-
# Database Configuration
|
|
7
|
-
# DATABASE_URL=postgresql://user:password@localhost:5432/monodog
|
|
8
|
-
|
|
9
|
-
# # Server Configuration
|
|
10
|
-
# SERVER_HOST=localhost
|
|
11
|
-
# SERVER_PORT=5000
|
|
12
|
-
# DASHBOARD_HOST=localhost
|
|
13
|
-
# DASHBOARD_PORT=3000
|
|
14
|
-
|
|
15
|
-
# # Logging Level (debug, info, warn, error)
|
|
16
|
-
# LOG_LEVEL=info
|
|
17
|
-
|
|
18
|
-
# # Environment
|
|
19
|
-
# NODE_ENV=development
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @manojkmfsi/monoapp
|
|
2
2
|
|
|
3
|
+
## 1.1.31
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`0541cc8`](https://github.com/manojkmfsi/monodog/commit/0541cc88c639265386c29a59b09e6b63cdee6064) - vnbbnbn,n,mnm,m
|
|
8
|
+
|
|
9
|
+
- [`785a90a`](https://github.com/manojkmfsi/monodog/commit/785a90a9c15354559f13a88bf1f9486c9794fc18) - ffhjhkklnbncxdflk,
|
|
10
|
+
|
|
11
|
+
- [`3a32768`](https://github.com/manojkmfsi/monodog/commit/3a32768c4e1be683847d9308632c1d4eb85e43fd) - ccfckjjlkklkm,m.,m.,
|
|
12
|
+
|
|
13
|
+
- [`efee08b`](https://github.com/manojkmfsi/monodog/commit/efee08b5d8cc52f35b7a21dae69ca7dde5b99427) - fgkjlkn,m cgfcbv
|
|
14
|
+
|
|
3
15
|
## 1.1.30
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Authentication Controller
|
|
4
|
+
* Thin controller that handles HTTP concerns and delegates business logic to auth-service
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.refresh = exports.logout = exports.validate = exports.me = exports.callback = exports.login = void 0;
|
|
8
|
+
const auth_service_1 = require("../services/auth-service");
|
|
9
|
+
const logger_1 = require("../middleware/logger");
|
|
10
|
+
/**
|
|
11
|
+
* Start OAuth login flow
|
|
12
|
+
* GET /auth/login
|
|
13
|
+
*/
|
|
14
|
+
const login = (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const redirectUrl = req.query.redirect || '/';
|
|
17
|
+
const result = (0, auth_service_1.initiateLogin)(redirectUrl);
|
|
18
|
+
res.json({
|
|
19
|
+
success: true,
|
|
20
|
+
authUrl: result.authUrl,
|
|
21
|
+
message: 'Redirect to this URL to authenticate with GitHub',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
logger_1.AppLogger.error(`Login initiation failed: ${error}`);
|
|
26
|
+
res.status(500).json({
|
|
27
|
+
success: false,
|
|
28
|
+
error: 'Login failed',
|
|
29
|
+
message: error instanceof Error ? error.message : 'Failed to initiate GitHub OAuth flow',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
exports.login = login;
|
|
34
|
+
/**
|
|
35
|
+
* OAuth callback handler
|
|
36
|
+
* GET /auth/callback?code=...&state=...
|
|
37
|
+
*/
|
|
38
|
+
const callback = async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const { code, state, error, error_description } = req.query;
|
|
41
|
+
// Handle OAuth errors from GitHub
|
|
42
|
+
if (error) {
|
|
43
|
+
logger_1.AppLogger.warn(`OAuth error: ${error} - ${error_description}`);
|
|
44
|
+
res.status(400).json({
|
|
45
|
+
success: false,
|
|
46
|
+
error: error,
|
|
47
|
+
message: error_description,
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!code || !state) {
|
|
52
|
+
logger_1.AppLogger.warn('OAuth callback missing code or state');
|
|
53
|
+
res.status(400).json({
|
|
54
|
+
success: false,
|
|
55
|
+
error: 'Missing parameters',
|
|
56
|
+
message: 'OAuth code and state are required',
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const result = await (0, auth_service_1.handleOAuthCallback)(code, state);
|
|
61
|
+
res.json({
|
|
62
|
+
success: true,
|
|
63
|
+
message: 'Authentication successful',
|
|
64
|
+
...result,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
logger_1.AppLogger.error(`OAuth callback failed: ${error}`);
|
|
69
|
+
const message = error instanceof Error ? error.message : 'Failed to complete GitHub OAuth flow';
|
|
70
|
+
if (message.includes('CSRF')) {
|
|
71
|
+
res.status(400).json({
|
|
72
|
+
success: false,
|
|
73
|
+
error: 'Invalid state',
|
|
74
|
+
message,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
res.status(500).json({
|
|
79
|
+
success: false,
|
|
80
|
+
error: 'Authentication failed',
|
|
81
|
+
message,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
exports.callback = callback;
|
|
87
|
+
/**
|
|
88
|
+
* Get current user session
|
|
89
|
+
* GET /auth/me
|
|
90
|
+
*/
|
|
91
|
+
const me = (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const session = (0, auth_service_1.getCurrentSession)(req);
|
|
94
|
+
res.json({
|
|
95
|
+
success: true,
|
|
96
|
+
...session,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger_1.AppLogger.error(`Failed to get user session: ${error}`);
|
|
101
|
+
res.status(401).json({
|
|
102
|
+
success: false,
|
|
103
|
+
error: 'Unauthorized',
|
|
104
|
+
message: error instanceof Error ? error.message : 'Failed to retrieve user information',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
exports.me = me;
|
|
109
|
+
/**
|
|
110
|
+
* Validate session
|
|
111
|
+
* POST /auth/validate
|
|
112
|
+
*/
|
|
113
|
+
const validate = async (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await (0, auth_service_1.validateCurrentSession)(req);
|
|
116
|
+
res.json({
|
|
117
|
+
success: true,
|
|
118
|
+
...result,
|
|
119
|
+
message: 'Session is valid',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger_1.AppLogger.error(`Session validation failed: ${error}`);
|
|
124
|
+
if (error instanceof Error && error.message.includes('no longer valid')) {
|
|
125
|
+
res.status(401).json({
|
|
126
|
+
success: false,
|
|
127
|
+
valid: false,
|
|
128
|
+
error: 'Unauthorized',
|
|
129
|
+
message: error.message,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
res.status(500).json({
|
|
134
|
+
success: false,
|
|
135
|
+
valid: false,
|
|
136
|
+
error: 'Validation failed',
|
|
137
|
+
message: error instanceof Error ? error.message : 'Failed to validate session',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
exports.validate = validate;
|
|
143
|
+
/**
|
|
144
|
+
* Logout
|
|
145
|
+
* POST /auth/logout
|
|
146
|
+
*/
|
|
147
|
+
const logout = (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
(0, auth_service_1.logoutUser)(req);
|
|
150
|
+
res.json({
|
|
151
|
+
success: true,
|
|
152
|
+
message: 'Logout successful',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logger_1.AppLogger.error(`Logout failed: ${error}`);
|
|
157
|
+
res.status(500).json({
|
|
158
|
+
success: false,
|
|
159
|
+
error: 'Logout failed',
|
|
160
|
+
message: error instanceof Error ? error.message : 'Failed to logout',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
exports.logout = logout;
|
|
165
|
+
/**
|
|
166
|
+
* Refresh session (token)
|
|
167
|
+
* POST /auth/refresh
|
|
168
|
+
*/
|
|
169
|
+
const refresh = async (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const result = await (0, auth_service_1.refreshUserSession)(req);
|
|
172
|
+
res.json({
|
|
173
|
+
success: true,
|
|
174
|
+
message: 'Session refreshed successfully',
|
|
175
|
+
...result,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
logger_1.AppLogger.error(`Session refresh failed: ${error}`);
|
|
180
|
+
if (error instanceof Error && error.message.includes('no longer valid')) {
|
|
181
|
+
res.status(401).json({
|
|
182
|
+
success: false,
|
|
183
|
+
error: 'Unauthorized',
|
|
184
|
+
message: error.message,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
res.status(500).json({
|
|
189
|
+
success: false,
|
|
190
|
+
error: 'Refresh failed',
|
|
191
|
+
message: error instanceof Error ? error.message : 'Failed to refresh session',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
exports.refresh = refresh;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Permission Controller
|
|
4
|
+
* Thin controller that handles HTTP concerns and delegates business logic to permission-service
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.invalidateCache = exports.checkActionPermission = exports.getRepositoryPermission = void 0;
|
|
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
|
+
/**
|
|
12
|
+
* Get user's permission for a specific repository
|
|
13
|
+
* GET /permissions/:owner/:repo
|
|
14
|
+
*/
|
|
15
|
+
const getRepositoryPermission = async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
18
|
+
if (!session) {
|
|
19
|
+
res.status(401).json({
|
|
20
|
+
success: false,
|
|
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
|
+
success: false,
|
|
31
|
+
error: 'Bad request',
|
|
32
|
+
message: 'Owner and repo parameters are required',
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
logger_1.AppLogger.debug(`Checking permission for ${session.user.login} in ${owner}/${repo}`);
|
|
37
|
+
const permissionCheck = await (0, permission_service_1.checkRepositoryPermission)(session.accessToken, session.user.id, session.user.login, owner, repo, forceRefresh);
|
|
38
|
+
res.json({
|
|
39
|
+
success: true,
|
|
40
|
+
owner,
|
|
41
|
+
repo,
|
|
42
|
+
user: session.user.login,
|
|
43
|
+
...permissionCheck,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger_1.AppLogger.error(`Failed to check permission: ${error}`);
|
|
48
|
+
res.status(500).json({
|
|
49
|
+
success: false,
|
|
50
|
+
error: 'Internal server error',
|
|
51
|
+
message: 'Failed to check repository permission',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
exports.getRepositoryPermission = getRepositoryPermission;
|
|
56
|
+
/**
|
|
57
|
+
* Check if user can perform a specific action
|
|
58
|
+
* POST /permissions/:owner/:repo/can-action
|
|
59
|
+
*/
|
|
60
|
+
const checkActionPermission = async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
63
|
+
if (!session) {
|
|
64
|
+
res.status(401).json({
|
|
65
|
+
success: false,
|
|
66
|
+
error: 'Unauthorized',
|
|
67
|
+
message: 'No active session',
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const { owner, repo } = req.params;
|
|
72
|
+
const { action } = req.body;
|
|
73
|
+
if (!owner || !repo) {
|
|
74
|
+
res.status(400).json({
|
|
75
|
+
success: false,
|
|
76
|
+
error: 'Bad request',
|
|
77
|
+
message: 'Owner and repo parameters are required',
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!action || !['read', 'write', 'maintain', 'admin'].includes(action)) {
|
|
82
|
+
res.status(400).json({
|
|
83
|
+
success: false,
|
|
84
|
+
error: 'Bad request',
|
|
85
|
+
message: 'Valid action is required (read, write, maintain, or admin)',
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
logger_1.AppLogger.debug(`Checking if ${session.user.login} can perform '${action}' in ${owner}/${repo}`);
|
|
90
|
+
const actionCheck = await (0, permission_service_1.checkUserAction)(session.accessToken, session.user.id, session.user.login, owner, repo, action);
|
|
91
|
+
res.json({
|
|
92
|
+
success: true,
|
|
93
|
+
owner,
|
|
94
|
+
repo,
|
|
95
|
+
user: session.user.login,
|
|
96
|
+
...actionCheck,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger_1.AppLogger.error(`Failed to check action permission: ${error}`);
|
|
101
|
+
res.status(500).json({
|
|
102
|
+
success: false,
|
|
103
|
+
error: 'Internal server error',
|
|
104
|
+
message: 'Failed to check action permission',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
exports.checkActionPermission = checkActionPermission;
|
|
109
|
+
/**
|
|
110
|
+
* Invalidate permission cache for a repository
|
|
111
|
+
* POST /permissions/:owner/:repo/invalidate
|
|
112
|
+
*/
|
|
113
|
+
const invalidateCache = (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
116
|
+
if (!session) {
|
|
117
|
+
res.status(401).json({
|
|
118
|
+
success: false,
|
|
119
|
+
error: 'Unauthorized',
|
|
120
|
+
message: 'No active session',
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const { owner, repo } = req.params;
|
|
125
|
+
if (!owner || !repo) {
|
|
126
|
+
res.status(400).json({
|
|
127
|
+
success: false,
|
|
128
|
+
error: 'Bad request',
|
|
129
|
+
message: 'Owner and repo parameters are required',
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
logger_1.AppLogger.debug(`Invalidating permission cache for ${session.user.login} in ${owner}/${repo}`);
|
|
134
|
+
(0, permission_service_1.invalidatePermissionCache)(session.user.id, owner, repo);
|
|
135
|
+
res.json({
|
|
136
|
+
success: true,
|
|
137
|
+
message: 'Permission cache invalidated',
|
|
138
|
+
owner,
|
|
139
|
+
repo,
|
|
140
|
+
user: session.user.login,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
logger_1.AppLogger.error(`Failed to invalidate cache: ${error}`);
|
|
145
|
+
res.status(500).json({
|
|
146
|
+
success: false,
|
|
147
|
+
error: 'Internal server error',
|
|
148
|
+
message: 'Failed to invalidate permission cache',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
exports.invalidateCache = invalidateCache;
|
|
@@ -5,368 +5,37 @@
|
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const express_1 = require("express");
|
|
8
|
-
const
|
|
8
|
+
const auth_controller_1 = require("../controllers/auth-controller");
|
|
9
9
|
const auth_middleware_1 = require("../middleware/auth-middleware");
|
|
10
|
-
const permission_service_1 = require("../services/permission-service");
|
|
11
|
-
const logger_1 = require("../middleware/logger");
|
|
12
|
-
const utilities_1 = require("../utils/utilities");
|
|
13
10
|
const router = (0, express_1.Router)();
|
|
14
|
-
// OAuth configuration (should come from environment variables)
|
|
15
|
-
// const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || '';
|
|
16
|
-
// const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || '';
|
|
17
|
-
// const OAUTH_REDIRECT_URI = process.env.OAUTH_REDIRECT_URI || 'http://localhost:3000/auth/callback';
|
|
18
|
-
// State store for CSRF protection
|
|
19
|
-
const stateStore = new Map();
|
|
20
|
-
const STATE_EXPIRY = 10 * 60 * 1000; // 10 minutes
|
|
21
|
-
/**
|
|
22
|
-
* Generate random state for CSRF protection
|
|
23
|
-
*/
|
|
24
|
-
function generateState() {
|
|
25
|
-
return Math.random().toString(36).substring(2, 15) +
|
|
26
|
-
Math.random().toString(36).substring(2, 15);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Validate OAuth state
|
|
30
|
-
*/
|
|
31
|
-
function validateState(state) {
|
|
32
|
-
const entry = stateStore.get(state);
|
|
33
|
-
if (!entry) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
// Check if state has expired
|
|
37
|
-
if (Date.now() - entry.createdAt > STATE_EXPIRY) {
|
|
38
|
-
stateStore.delete(state);
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Get redirect URL from state
|
|
45
|
-
*/
|
|
46
|
-
function getRedirectUrl(state) {
|
|
47
|
-
const entry = stateStore.get(state);
|
|
48
|
-
return entry?.redirectUrl;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Clear state after use
|
|
52
|
-
*/
|
|
53
|
-
function clearState(state) {
|
|
54
|
-
stateStore.delete(state);
|
|
55
|
-
}
|
|
56
11
|
/**
|
|
57
12
|
* Start OAuth login flow
|
|
58
13
|
* GET /auth/login
|
|
59
14
|
*/
|
|
60
|
-
router.get('/login',
|
|
61
|
-
try {
|
|
62
|
-
if (!process.env.GITHUB_CLIENT_ID) {
|
|
63
|
-
logger_1.AppLogger.error('GitHub client ID not configured');
|
|
64
|
-
res.status(500).json({
|
|
65
|
-
error: 'OAuth not configured',
|
|
66
|
-
message: 'GitHub OAuth is not properly configured',
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const state = generateState();
|
|
71
|
-
const redirectUrl = req.query.redirect || '/';
|
|
72
|
-
// Store state for validation
|
|
73
|
-
stateStore.set(state, {
|
|
74
|
-
createdAt: Date.now(),
|
|
75
|
-
redirectUrl,
|
|
76
|
-
});
|
|
77
|
-
const authUrl = (0, github_oauth_service_1.generateAuthorizationUrl)(process.env.GITHUB_CLIENT_ID, process.env.OAUTH_REDIRECT_URI, state);
|
|
78
|
-
res.json({
|
|
79
|
-
success: true,
|
|
80
|
-
authUrl,
|
|
81
|
-
message: 'Redirect to this URL to authenticate with GitHub',
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
logger_1.AppLogger.error(`Login initiation failed: ${error}`);
|
|
86
|
-
res.status(500).json({
|
|
87
|
-
error: 'Login failed',
|
|
88
|
-
message: 'Failed to initiate GitHub OAuth flow',
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
});
|
|
15
|
+
router.get('/login', auth_controller_1.login);
|
|
92
16
|
/**
|
|
93
17
|
* OAuth callback handler
|
|
94
18
|
* GET /auth/callback?code=...&state=...
|
|
95
19
|
*/
|
|
96
|
-
router.get('/callback',
|
|
97
|
-
try {
|
|
98
|
-
const { code, state, error, error_description } = req.query;
|
|
99
|
-
// Handle OAuth errors
|
|
100
|
-
if (error) {
|
|
101
|
-
logger_1.AppLogger.warn(`OAuth error: ${error} - ${error_description}`);
|
|
102
|
-
res.status(400).json({
|
|
103
|
-
success: false,
|
|
104
|
-
error: error,
|
|
105
|
-
message: error_description,
|
|
106
|
-
});
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
// Validate code and state
|
|
110
|
-
if (!code || !state) {
|
|
111
|
-
logger_1.AppLogger.warn('OAuth callback missing code or state');
|
|
112
|
-
res.status(400).json({
|
|
113
|
-
success: false,
|
|
114
|
-
error: 'Missing parameters',
|
|
115
|
-
message: 'OAuth code and state are required',
|
|
116
|
-
});
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
// Validate state for CSRF protection
|
|
120
|
-
if (!validateState(state)) {
|
|
121
|
-
logger_1.AppLogger.warn(`Invalid or expired state in OAuth callback: ${state}`);
|
|
122
|
-
res.status(400).json({
|
|
123
|
-
success: false,
|
|
124
|
-
error: 'Invalid state',
|
|
125
|
-
message: 'CSRF validation failed',
|
|
126
|
-
});
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (!process.env.GITHUB_CLIENT_SECRET) {
|
|
130
|
-
logger_1.AppLogger.error('GitHub client secret not configured');
|
|
131
|
-
res.status(500).json({
|
|
132
|
-
error: 'OAuth not configured',
|
|
133
|
-
message: 'GitHub OAuth is not properly configured',
|
|
134
|
-
});
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// Exchange code for access token
|
|
138
|
-
logger_1.AppLogger.debug('Exchanging OAuth code for access token');
|
|
139
|
-
const tokenResponse = await (0, github_oauth_service_1.exchangeCodeForToken)(code, process.env.GITHUB_CLIENT_ID, process.env.GITHUB_CLIENT_SECRET, process.env.OAUTH_REDIRECT_URI);
|
|
140
|
-
// Get user information
|
|
141
|
-
logger_1.AppLogger.debug('Retrieving authenticated user information');
|
|
142
|
-
const user = await (0, github_oauth_service_1.getAuthenticatedUser)(tokenResponse.access_token);
|
|
143
|
-
// Fetch user's repository permission
|
|
144
|
-
let permission = null;
|
|
145
|
-
try {
|
|
146
|
-
logger_1.AppLogger.debug(`Fetching repository permission for user ${user.login}`);
|
|
147
|
-
// Extract repository info from git remote
|
|
148
|
-
const repoInfo = await (0, utilities_1.getRepositoryInfoFromGit)();
|
|
149
|
-
if (!repoInfo) {
|
|
150
|
-
logger_1.AppLogger.warn('Could not extract repository info from git remote - permission fetch skipped');
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
const { owner, repo } = repoInfo;
|
|
154
|
-
permission = await (0, permission_service_1.getUserRepositoryPermission)(tokenResponse.access_token, user.id, user.login, owner, repo);
|
|
155
|
-
logger_1.AppLogger.info(`User ${user.login} has ${permission.permission} permission on ${owner}/${repo}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
catch (permError) {
|
|
159
|
-
logger_1.AppLogger.error(`Failed to fetch repository permission: ${permError}`);
|
|
160
|
-
// Continue without permission - will be checked on protected routes
|
|
161
|
-
permission = null;
|
|
162
|
-
}
|
|
163
|
-
// Create session with permission
|
|
164
|
-
const session = {
|
|
165
|
-
accessToken: tokenResponse.access_token,
|
|
166
|
-
expiresIn: 3600, // 1 hour default
|
|
167
|
-
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
|
|
168
|
-
user,
|
|
169
|
-
scopes: tokenResponse.scope.split(','),
|
|
170
|
-
permission, // Include fetched permission in session
|
|
171
|
-
};
|
|
172
|
-
// Store session and get token
|
|
173
|
-
const sessionToken = (0, auth_middleware_1.storeSession)(session);
|
|
174
|
-
// Get redirect URL
|
|
175
|
-
const redirectUrl = getRedirectUrl(state) || '/';
|
|
176
|
-
// Clear state
|
|
177
|
-
clearState(state);
|
|
178
|
-
logger_1.AppLogger.info(`User authenticated: ${user.login} with permission: ${permission?.permission || 'unknown'}`);
|
|
179
|
-
res.json({
|
|
180
|
-
success: true,
|
|
181
|
-
message: 'Authentication successful',
|
|
182
|
-
sessionToken,
|
|
183
|
-
redirectUrl,
|
|
184
|
-
user: {
|
|
185
|
-
id: user.id,
|
|
186
|
-
login: user.login,
|
|
187
|
-
name: user.name,
|
|
188
|
-
avatar_url: user.avatar_url,
|
|
189
|
-
},
|
|
190
|
-
permission: permission ? {
|
|
191
|
-
level: permission.permission,
|
|
192
|
-
role: permission.role,
|
|
193
|
-
owner: permission.owner,
|
|
194
|
-
repo: permission.repo,
|
|
195
|
-
} : null,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
catch (error) {
|
|
199
|
-
logger_1.AppLogger.error(`OAuth callback failed: ${error}`);
|
|
200
|
-
res.status(500).json({
|
|
201
|
-
success: false,
|
|
202
|
-
error: 'Authentication failed',
|
|
203
|
-
message: 'Failed to complete GitHub OAuth flow',
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
});
|
|
20
|
+
router.get('/callback', auth_controller_1.callback);
|
|
207
21
|
/**
|
|
208
22
|
* Get current user session
|
|
209
23
|
* GET /auth/me
|
|
210
24
|
*/
|
|
211
|
-
router.get('/me', auth_middleware_1.authenticationMiddleware,
|
|
212
|
-
try {
|
|
213
|
-
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
214
|
-
if (!session) {
|
|
215
|
-
res.status(401).json({
|
|
216
|
-
error: 'Unauthorized',
|
|
217
|
-
message: 'No active session',
|
|
218
|
-
});
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
res.json({
|
|
222
|
-
success: true,
|
|
223
|
-
user: {
|
|
224
|
-
id: session.user.id,
|
|
225
|
-
login: session.user.login,
|
|
226
|
-
name: session.user.name,
|
|
227
|
-
email: session.user.email,
|
|
228
|
-
avatar_url: session.user.avatar_url,
|
|
229
|
-
public_repos: session.user.public_repos,
|
|
230
|
-
followers: session.user.followers,
|
|
231
|
-
following: session.user.following,
|
|
232
|
-
},
|
|
233
|
-
scopes: session.scopes,
|
|
234
|
-
expiresAt: session.expiresAt,
|
|
235
|
-
permission: session.permission || null,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
catch (error) {
|
|
239
|
-
logger_1.AppLogger.error(`Failed to get user session: ${error}`);
|
|
240
|
-
res.status(500).json({
|
|
241
|
-
error: 'Internal server error',
|
|
242
|
-
message: 'Failed to retrieve user information',
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
});
|
|
25
|
+
router.get('/me', auth_middleware_1.authenticationMiddleware, auth_controller_1.me);
|
|
246
26
|
/**
|
|
247
27
|
* Validate session
|
|
248
28
|
* POST /auth/validate
|
|
249
29
|
*/
|
|
250
|
-
router.post('/validate', auth_middleware_1.authenticationMiddleware,
|
|
251
|
-
try {
|
|
252
|
-
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
253
|
-
if (!session) {
|
|
254
|
-
res.status(401).json({
|
|
255
|
-
success: false,
|
|
256
|
-
valid: false,
|
|
257
|
-
message: 'No active session',
|
|
258
|
-
});
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
// Validate token with GitHub
|
|
262
|
-
const isValid = await (0, github_oauth_service_1.validateToken)(session.accessToken);
|
|
263
|
-
if (!isValid) {
|
|
264
|
-
// Token is no longer valid, invalidate session
|
|
265
|
-
const token = req.headers.authorization?.replace('Bearer ', '') ||
|
|
266
|
-
req.cookies?.['auth-token'];
|
|
267
|
-
if (token) {
|
|
268
|
-
(0, auth_middleware_1.invalidateSession)(token);
|
|
269
|
-
}
|
|
270
|
-
res.status(401).json({
|
|
271
|
-
success: false,
|
|
272
|
-
valid: false,
|
|
273
|
-
message: 'Session token is no longer valid',
|
|
274
|
-
});
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
res.json({
|
|
278
|
-
success: true,
|
|
279
|
-
valid: true,
|
|
280
|
-
message: 'Session is valid',
|
|
281
|
-
expiresAt: session.expiresAt,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
logger_1.AppLogger.error(`Session validation failed: ${error}`);
|
|
286
|
-
res.status(500).json({
|
|
287
|
-
success: false,
|
|
288
|
-
valid: false,
|
|
289
|
-
error: 'Validation failed',
|
|
290
|
-
message: 'Failed to validate session',
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
});
|
|
30
|
+
router.post('/validate', auth_middleware_1.authenticationMiddleware, auth_controller_1.validate);
|
|
294
31
|
/**
|
|
295
32
|
* Logout
|
|
296
33
|
* POST /auth/logout
|
|
297
34
|
*/
|
|
298
|
-
router.post('/logout', auth_middleware_1.authenticationMiddleware,
|
|
299
|
-
try {
|
|
300
|
-
const token = req.headers.authorization?.replace('Bearer ', '') ||
|
|
301
|
-
req.cookies?.['auth-token'];
|
|
302
|
-
if (token) {
|
|
303
|
-
(0, auth_middleware_1.invalidateSession)(token);
|
|
304
|
-
}
|
|
305
|
-
res.json({
|
|
306
|
-
success: true,
|
|
307
|
-
message: 'Logout successful',
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
logger_1.AppLogger.error(`Logout failed: ${error}`);
|
|
312
|
-
res.status(500).json({
|
|
313
|
-
success: false,
|
|
314
|
-
error: 'Logout failed',
|
|
315
|
-
message: 'Failed to logout',
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
});
|
|
35
|
+
router.post('/logout', auth_middleware_1.authenticationMiddleware, auth_controller_1.logout);
|
|
319
36
|
/**
|
|
320
37
|
* Refresh session (token)
|
|
321
38
|
* POST /auth/refresh
|
|
322
39
|
*/
|
|
323
|
-
router.post('/refresh', auth_middleware_1.authenticationMiddleware,
|
|
324
|
-
try {
|
|
325
|
-
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
326
|
-
if (!session) {
|
|
327
|
-
res.status(401).json({
|
|
328
|
-
success: false,
|
|
329
|
-
error: 'Unauthorized',
|
|
330
|
-
message: 'No active session',
|
|
331
|
-
});
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
// Validate token is still valid
|
|
335
|
-
const isValid = await (0, github_oauth_service_1.validateToken)(session.accessToken);
|
|
336
|
-
if (!isValid) {
|
|
337
|
-
res.status(401).json({
|
|
338
|
-
success: false,
|
|
339
|
-
error: 'Unauthorized',
|
|
340
|
-
message: 'Token is no longer valid with GitHub',
|
|
341
|
-
});
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
// Create new session with updated expiry
|
|
345
|
-
const newSession = {
|
|
346
|
-
...session,
|
|
347
|
-
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // Extend 24 hours
|
|
348
|
-
};
|
|
349
|
-
const newToken = (0, auth_middleware_1.storeSession)(newSession);
|
|
350
|
-
// Invalidate old token
|
|
351
|
-
const oldToken = req.headers.authorization?.replace('Bearer ', '') ||
|
|
352
|
-
req.cookies?.['auth-token'];
|
|
353
|
-
if (oldToken) {
|
|
354
|
-
(0, auth_middleware_1.invalidateSession)(oldToken);
|
|
355
|
-
}
|
|
356
|
-
res.json({
|
|
357
|
-
success: true,
|
|
358
|
-
message: 'Session refreshed successfully',
|
|
359
|
-
sessionToken: newToken,
|
|
360
|
-
expiresAt: newSession.expiresAt,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
catch (error) {
|
|
364
|
-
logger_1.AppLogger.error(`Session refresh failed: ${error}`);
|
|
365
|
-
res.status(500).json({
|
|
366
|
-
success: false,
|
|
367
|
-
error: 'Refresh failed',
|
|
368
|
-
message: 'Failed to refresh session',
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
});
|
|
40
|
+
router.post('/refresh', auth_middleware_1.authenticationMiddleware, auth_controller_1.refresh);
|
|
372
41
|
exports.default = router;
|
|
@@ -6,156 +6,21 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const express_1 = require("express");
|
|
8
8
|
const auth_middleware_1 = require("../middleware/auth-middleware");
|
|
9
|
-
const
|
|
10
|
-
const logger_1 = require("../middleware/logger");
|
|
9
|
+
const permission_controller_1 = require("../controllers/permission-controller");
|
|
11
10
|
const router = (0, express_1.Router)();
|
|
12
11
|
/**
|
|
13
12
|
* Get user's permission for a specific repository
|
|
14
13
|
* GET /permissions/:owner/:repo
|
|
15
14
|
*/
|
|
16
|
-
router.get('/:owner/:repo', auth_middleware_1.authenticationMiddleware,
|
|
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
|
-
});
|
|
15
|
+
router.get('/:owner/:repo', auth_middleware_1.authenticationMiddleware, permission_controller_1.getRepositoryPermission);
|
|
64
16
|
/**
|
|
65
17
|
* Check if user can perform a specific action
|
|
66
18
|
* POST /permissions/:owner/:repo/can-action
|
|
67
19
|
*/
|
|
68
|
-
router.post('/:owner/:repo/can-action', auth_middleware_1.authenticationMiddleware,
|
|
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
|
-
});
|
|
20
|
+
router.post('/:owner/:repo/can-action', auth_middleware_1.authenticationMiddleware, permission_controller_1.checkActionPermission);
|
|
119
21
|
/**
|
|
120
22
|
* Invalidate permission cache for a repository
|
|
121
23
|
* POST /permissions/:owner/:repo/invalidate
|
|
122
24
|
*/
|
|
123
|
-
router.post('/:owner/:repo/invalidate', auth_middleware_1.authenticationMiddleware,
|
|
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
|
-
});
|
|
25
|
+
router.post('/:owner/:repo/invalidate', auth_middleware_1.authenticationMiddleware, permission_controller_1.invalidateCache);
|
|
161
26
|
exports.default = router;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Authentication Service
|
|
4
|
+
* Handles all authentication business logic including OAuth flow, session management
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.initiateLogin = initiateLogin;
|
|
8
|
+
exports.handleOAuthCallback = handleOAuthCallback;
|
|
9
|
+
exports.getCurrentSession = getCurrentSession;
|
|
10
|
+
exports.validateCurrentSession = validateCurrentSession;
|
|
11
|
+
exports.logoutUser = logoutUser;
|
|
12
|
+
exports.refreshUserSession = refreshUserSession;
|
|
13
|
+
const github_oauth_service_1 = require("./github-oauth-service");
|
|
14
|
+
const auth_middleware_1 = require("../middleware/auth-middleware");
|
|
15
|
+
const permission_service_1 = require("./permission-service");
|
|
16
|
+
const logger_1 = require("../middleware/logger");
|
|
17
|
+
const utilities_1 = require("../utils/utilities");
|
|
18
|
+
// State store for CSRF protection
|
|
19
|
+
const stateStore = new Map();
|
|
20
|
+
const STATE_EXPIRY = 10 * 60 * 1000; // 10 minutes
|
|
21
|
+
/**
|
|
22
|
+
* Generate random state for CSRF protection
|
|
23
|
+
*/
|
|
24
|
+
function generateState() {
|
|
25
|
+
return Math.random().toString(36).substring(2, 15) +
|
|
26
|
+
Math.random().toString(36).substring(2, 15);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate OAuth state
|
|
30
|
+
*/
|
|
31
|
+
function validateState(state) {
|
|
32
|
+
const entry = stateStore.get(state);
|
|
33
|
+
if (!entry) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// Check if state has expired
|
|
37
|
+
if (Date.now() - entry.createdAt > STATE_EXPIRY) {
|
|
38
|
+
stateStore.delete(state);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get redirect URL from state
|
|
45
|
+
*/
|
|
46
|
+
function getRedirectUrl(state) {
|
|
47
|
+
const entry = stateStore.get(state);
|
|
48
|
+
return entry?.redirectUrl;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Clear state after use
|
|
52
|
+
*/
|
|
53
|
+
function clearState(state) {
|
|
54
|
+
stateStore.delete(state);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Initiate login by generating authorization URL
|
|
58
|
+
* @param redirectUrl - Where to redirect after login
|
|
59
|
+
* @returns Login URL response with state
|
|
60
|
+
*/
|
|
61
|
+
function initiateLogin(redirectUrl = '/') {
|
|
62
|
+
if (!process.env.GITHUB_CLIENT_ID) {
|
|
63
|
+
throw new Error('GitHub client ID not configured');
|
|
64
|
+
}
|
|
65
|
+
const state = generateState();
|
|
66
|
+
// Store state for validation
|
|
67
|
+
stateStore.set(state, {
|
|
68
|
+
createdAt: Date.now(),
|
|
69
|
+
redirectUrl,
|
|
70
|
+
});
|
|
71
|
+
const authUrl = (0, github_oauth_service_1.generateAuthorizationUrl)(process.env.GITHUB_CLIENT_ID, process.env.OAUTH_REDIRECT_URI, state);
|
|
72
|
+
return {
|
|
73
|
+
authUrl,
|
|
74
|
+
state,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Handle OAuth callback and create session
|
|
79
|
+
* @param code - OAuth authorization code
|
|
80
|
+
* @param state - CSRF protection state
|
|
81
|
+
* @returns OAuth callback response with session token
|
|
82
|
+
*/
|
|
83
|
+
async function handleOAuthCallback(code, state) {
|
|
84
|
+
// Validate code and state
|
|
85
|
+
if (!code || !state) {
|
|
86
|
+
throw new Error('OAuth code and state are required');
|
|
87
|
+
}
|
|
88
|
+
// Validate state for CSRF protection
|
|
89
|
+
if (!validateState(state)) {
|
|
90
|
+
throw new Error('Invalid or expired state - CSRF validation failed');
|
|
91
|
+
}
|
|
92
|
+
if (!process.env.GITHUB_CLIENT_SECRET) {
|
|
93
|
+
throw new Error('GitHub client secret not configured');
|
|
94
|
+
}
|
|
95
|
+
// Exchange code for access token
|
|
96
|
+
logger_1.AppLogger.debug('Exchanging OAuth code for access token');
|
|
97
|
+
const tokenResponse = await (0, github_oauth_service_1.exchangeCodeForToken)(code, process.env.GITHUB_CLIENT_ID, process.env.GITHUB_CLIENT_SECRET, process.env.OAUTH_REDIRECT_URI);
|
|
98
|
+
// Get user information
|
|
99
|
+
logger_1.AppLogger.debug('Retrieving authenticated user information');
|
|
100
|
+
const user = await (0, github_oauth_service_1.getAuthenticatedUser)(tokenResponse.access_token);
|
|
101
|
+
// Fetch user's repository permission
|
|
102
|
+
let permission = null;
|
|
103
|
+
try {
|
|
104
|
+
logger_1.AppLogger.debug(`Fetching repository permission for user ${user.login}`);
|
|
105
|
+
// Extract repository info from git remote
|
|
106
|
+
const repoInfo = await (0, utilities_1.getRepositoryInfoFromGit)();
|
|
107
|
+
if (!repoInfo) {
|
|
108
|
+
logger_1.AppLogger.warn('Could not extract repository info from git remote - permission fetch skipped');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const { owner, repo } = repoInfo;
|
|
112
|
+
permission = await (0, permission_service_1.getUserRepositoryPermission)(tokenResponse.access_token, user.id, user.login, owner, repo);
|
|
113
|
+
logger_1.AppLogger.info(`User ${user.login} has ${permission.permission} permission on ${owner}/${repo}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (permError) {
|
|
117
|
+
logger_1.AppLogger.error(`Failed to fetch repository permission: ${permError}`);
|
|
118
|
+
// Continue without permission - will be checked on protected routes
|
|
119
|
+
permission = null;
|
|
120
|
+
}
|
|
121
|
+
// Create session with permission
|
|
122
|
+
const session = {
|
|
123
|
+
accessToken: tokenResponse.access_token,
|
|
124
|
+
expiresIn: 3600, // 1 hour default
|
|
125
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
|
|
126
|
+
user,
|
|
127
|
+
scopes: tokenResponse.scope.split(','),
|
|
128
|
+
permission, // Include fetched permission in session
|
|
129
|
+
};
|
|
130
|
+
// Store session and get token
|
|
131
|
+
const sessionToken = (0, auth_middleware_1.storeSession)(session);
|
|
132
|
+
// Get redirect URL
|
|
133
|
+
const redirectUrl = getRedirectUrl(state) || '/';
|
|
134
|
+
// Clear state
|
|
135
|
+
clearState(state);
|
|
136
|
+
logger_1.AppLogger.info(`User authenticated: ${user.login} with permission: ${permission?.permission || 'unknown'}`);
|
|
137
|
+
return {
|
|
138
|
+
sessionToken,
|
|
139
|
+
redirectUrl,
|
|
140
|
+
user: {
|
|
141
|
+
id: user.id,
|
|
142
|
+
login: user.login,
|
|
143
|
+
name: user.name,
|
|
144
|
+
avatar_url: user.avatar_url,
|
|
145
|
+
},
|
|
146
|
+
permission: permission ? {
|
|
147
|
+
level: permission.permission,
|
|
148
|
+
role: permission.role,
|
|
149
|
+
owner: permission.owner,
|
|
150
|
+
repo: permission.repo,
|
|
151
|
+
} : null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get current user session from request
|
|
156
|
+
* @param req - Express request
|
|
157
|
+
* @returns Session response
|
|
158
|
+
*/
|
|
159
|
+
function getCurrentSession(req) {
|
|
160
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
161
|
+
if (!session) {
|
|
162
|
+
throw new Error('No active session');
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
user: session.user,
|
|
166
|
+
scopes: session.scopes,
|
|
167
|
+
expiresAt: session.expiresAt,
|
|
168
|
+
permission: session.permission || null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Validate current session with GitHub
|
|
173
|
+
* @param req - Express request
|
|
174
|
+
* @returns Validation response
|
|
175
|
+
*/
|
|
176
|
+
async function validateCurrentSession(req) {
|
|
177
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
178
|
+
if (!session) {
|
|
179
|
+
throw new Error('No active session');
|
|
180
|
+
}
|
|
181
|
+
// Validate token with GitHub
|
|
182
|
+
const isValid = await (0, github_oauth_service_1.validateToken)(session.accessToken);
|
|
183
|
+
if (!isValid) {
|
|
184
|
+
// Token is no longer valid, invalidate session
|
|
185
|
+
const token = req.headers.authorization?.replace('Bearer ', '') ||
|
|
186
|
+
req.cookies?.['auth-token'];
|
|
187
|
+
if (token) {
|
|
188
|
+
(0, auth_middleware_1.invalidateSession)(token);
|
|
189
|
+
}
|
|
190
|
+
throw new Error('Session token is no longer valid');
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
valid: true,
|
|
194
|
+
expiresAt: session.expiresAt,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Logout user by invalidating session
|
|
199
|
+
* @param req - Express request
|
|
200
|
+
*/
|
|
201
|
+
function logoutUser(req) {
|
|
202
|
+
const token = req.headers.authorization?.replace('Bearer ', '') ||
|
|
203
|
+
req.cookies?.['auth-token'];
|
|
204
|
+
if (token) {
|
|
205
|
+
(0, auth_middleware_1.invalidateSession)(token);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Refresh session token
|
|
210
|
+
* @param req - Express request
|
|
211
|
+
* @returns New session token and expiry
|
|
212
|
+
*/
|
|
213
|
+
async function refreshUserSession(req) {
|
|
214
|
+
const session = (0, auth_middleware_1.getSessionFromRequest)(req);
|
|
215
|
+
if (!session) {
|
|
216
|
+
throw new Error('No active session');
|
|
217
|
+
}
|
|
218
|
+
// Validate token is still valid
|
|
219
|
+
const isValid = await (0, github_oauth_service_1.validateToken)(session.accessToken);
|
|
220
|
+
if (!isValid) {
|
|
221
|
+
throw new Error('Token is no longer valid with GitHub');
|
|
222
|
+
}
|
|
223
|
+
// Create new session with updated expiry
|
|
224
|
+
const newSession = {
|
|
225
|
+
...session,
|
|
226
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // Extend 24 hours
|
|
227
|
+
};
|
|
228
|
+
const newToken = (0, auth_middleware_1.storeSession)(newSession);
|
|
229
|
+
// Invalidate old token
|
|
230
|
+
const oldToken = req.headers.authorization?.replace('Bearer ', '') ||
|
|
231
|
+
req.cookies?.['auth-token'];
|
|
232
|
+
if (oldToken) {
|
|
233
|
+
(0, auth_middleware_1.invalidateSession)(oldToken);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
sessionToken: newToken,
|
|
237
|
+
expiresAt: newSession.expiresAt,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -11,6 +11,8 @@ exports.invalidateUserCache = invalidateUserCache;
|
|
|
11
11
|
exports.clearAllCache = clearAllCache;
|
|
12
12
|
exports.getCacheStats = getCacheStats;
|
|
13
13
|
exports.canPerformAction = canPerformAction;
|
|
14
|
+
exports.checkRepositoryPermission = checkRepositoryPermission;
|
|
15
|
+
exports.checkUserAction = checkUserAction;
|
|
14
16
|
const github_oauth_service_1 = require("./github-oauth-service");
|
|
15
17
|
const logger_1 = require("../middleware/logger");
|
|
16
18
|
// Cache storage: key = `${userId}:${owner}/${repo}`
|
|
@@ -172,3 +174,31 @@ function canPerformAction(permission, requiredAction) {
|
|
|
172
174
|
const allowedPermissions = actionPermissionMap[requiredAction] || [];
|
|
173
175
|
return allowedPermissions.includes(permission);
|
|
174
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Get user's permission for a specific repository with response formatting
|
|
179
|
+
*/
|
|
180
|
+
async function checkRepositoryPermission(accessToken, userId, username, owner, repo, forceRefresh = false) {
|
|
181
|
+
const cachedPermission = await getUserRepositoryPermission(accessToken, userId, username, owner, repo, forceRefresh);
|
|
182
|
+
return {
|
|
183
|
+
permission: cachedPermission.permission,
|
|
184
|
+
role: cachedPermission.role,
|
|
185
|
+
canAdmin: cachedPermission.permission === 'admin',
|
|
186
|
+
canMaintain: canPerformAction(cachedPermission.permission, 'maintain'),
|
|
187
|
+
canWrite: canPerformAction(cachedPermission.permission, 'write'),
|
|
188
|
+
canRead: canPerformAction(cachedPermission.permission, 'read'),
|
|
189
|
+
denied: cachedPermission.permission === 'none',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if user can perform a specific action with response formatting
|
|
194
|
+
*/
|
|
195
|
+
async function checkUserAction(accessToken, userId, username, owner, repo, action) {
|
|
196
|
+
const cachedPermission = await getUserRepositoryPermission(accessToken, userId, username, owner, repo);
|
|
197
|
+
const can = canPerformAction(cachedPermission.permission, action);
|
|
198
|
+
return {
|
|
199
|
+
action,
|
|
200
|
+
can,
|
|
201
|
+
permission: cachedPermission.permission,
|
|
202
|
+
role: cachedPermission.role,
|
|
203
|
+
};
|
|
204
|
+
}
|