@taazkareem/clickup-mcp-server 0.8.4 → 0.9.0
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/LICENSE +9 -17
- package/README.md +33 -38
- package/build/enhanced_server.js +262 -0
- package/build/index.js +9 -3
- package/build/license.js +172 -0
- package/build/middleware/auth.js +211 -0
- package/build/routes/auth.js +306 -0
- package/build/schemas/member.js +13 -0
- package/build/server.js +15 -1
- package/build/server.log +15 -0
- package/build/services/auth/oauth2.js +236 -0
- package/build/services/auth/session.js +337 -0
- package/build/services/clickup/adapter.js +281 -0
- package/build/services/clickup/factory.js +339 -0
- package/build/services/clickup/task/task-attachments.js +20 -12
- package/build/services/clickup/task/task-comments.js +19 -9
- package/build/services/clickup/task/task-core.js +68 -4
- package/build/services/clickup/task/task-custom-fields.js +23 -13
- package/build/services/clickup/task/task-search.js +79 -71
- package/build/services/clickup/task/task-service.js +88 -9
- package/build/services/clickup/task/task-tags.js +25 -13
- package/build/sse_server.js +4 -4
- package/build/tools/documents.js +11 -4
- package/build/tools/health.js +23 -0
- package/build/tools/member.js +2 -4
- package/build/tools/task/bulk-operations.js +5 -5
- package/build/tools/task/handlers.js +62 -12
- package/build/tools/task/single-operations.js +9 -9
- package/build/tools/task/time-tracking.js +61 -170
- package/build/tools/task/utilities.js +56 -22
- package/build/utils/date-utils.js +341 -141
- package/build/utils/schema-compatibility.js +222 -0
- package/build/utils/universal-schema-compatibility.js +171 -0
- package/build/virtual-sdk/generator.js +53 -0
- package/build/virtual-sdk/registry.js +45 -0
- package/package.json +2 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* OAuth2 Authentication Service
|
|
6
|
+
*
|
|
7
|
+
* Handles OAuth2 authentication flow with ClickUp API:
|
|
8
|
+
* - Authorization URL generation
|
|
9
|
+
* - Token exchange and refresh
|
|
10
|
+
* - User information retrieval
|
|
11
|
+
* - Token validation and management
|
|
12
|
+
*/
|
|
13
|
+
import axios from 'axios';
|
|
14
|
+
import { Logger } from '../../logger.js';
|
|
15
|
+
/**
|
|
16
|
+
* OAuth2 service for ClickUp authentication
|
|
17
|
+
*/
|
|
18
|
+
export class OAuth2Service {
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = {
|
|
21
|
+
baseUrl: 'https://api.clickup.com/api/v2',
|
|
22
|
+
scopes: ['read', 'write'],
|
|
23
|
+
...config
|
|
24
|
+
};
|
|
25
|
+
this.logger = new Logger('OAuth2Service');
|
|
26
|
+
// Create HTTP client for OAuth2 operations
|
|
27
|
+
this.client = axios.create({
|
|
28
|
+
baseURL: this.config.baseUrl,
|
|
29
|
+
timeout: 30000,
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json'
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
this.logger.info('OAuth2 service initialized', {
|
|
35
|
+
clientId: this.config.clientId,
|
|
36
|
+
redirectUri: this.config.redirectUri,
|
|
37
|
+
scopes: this.config.scopes
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate OAuth2 authorization URL
|
|
42
|
+
* @param state Optional state parameter for CSRF protection
|
|
43
|
+
* @returns Authorization URL for user redirection
|
|
44
|
+
*/
|
|
45
|
+
generateAuthorizationUrl(state) {
|
|
46
|
+
const params = new URLSearchParams({
|
|
47
|
+
client_id: this.config.clientId,
|
|
48
|
+
redirect_uri: this.config.redirectUri,
|
|
49
|
+
response_type: 'code',
|
|
50
|
+
scope: this.config.scopes?.join(' ') || 'read write'
|
|
51
|
+
});
|
|
52
|
+
if (state) {
|
|
53
|
+
params.append('state', state);
|
|
54
|
+
}
|
|
55
|
+
const authUrl = `https://app.clickup.com/api?${params.toString()}`;
|
|
56
|
+
this.logger.info('Generated authorization URL', {
|
|
57
|
+
clientId: this.config.clientId,
|
|
58
|
+
redirectUri: this.config.redirectUri,
|
|
59
|
+
state: state || 'none'
|
|
60
|
+
});
|
|
61
|
+
return authUrl;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Exchange authorization code for access token
|
|
65
|
+
* @param code Authorization code from ClickUp callback
|
|
66
|
+
* @returns OAuth2 authentication result with tokens and user info
|
|
67
|
+
*/
|
|
68
|
+
async exchangeCodeForToken(code) {
|
|
69
|
+
try {
|
|
70
|
+
this.logger.info('Exchanging authorization code for token', { code: code.substring(0, 10) + '...' });
|
|
71
|
+
// Exchange code for token
|
|
72
|
+
const tokenResponse = await this.client.post('/oauth/token', {
|
|
73
|
+
client_id: this.config.clientId,
|
|
74
|
+
client_secret: this.config.clientSecret,
|
|
75
|
+
code: code,
|
|
76
|
+
redirect_uri: this.config.redirectUri,
|
|
77
|
+
grant_type: 'authorization_code'
|
|
78
|
+
});
|
|
79
|
+
const tokens = tokenResponse.data;
|
|
80
|
+
this.logger.info('Successfully exchanged code for token', {
|
|
81
|
+
tokenType: tokens.token_type,
|
|
82
|
+
expiresIn: tokens.expires_in,
|
|
83
|
+
hasRefreshToken: !!tokens.refresh_token
|
|
84
|
+
});
|
|
85
|
+
// Get user information using the access token
|
|
86
|
+
const userInfo = await this.getUserInfo(tokens.access_token);
|
|
87
|
+
if (!userInfo.success || !userInfo.user) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: {
|
|
91
|
+
message: 'Failed to retrieve user information',
|
|
92
|
+
details: userInfo.error
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
tokens,
|
|
99
|
+
user: userInfo.user
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.logger.error('Failed to exchange code for token', {
|
|
104
|
+
error: error.message,
|
|
105
|
+
status: error.response?.status,
|
|
106
|
+
data: error.response?.data
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: {
|
|
111
|
+
message: 'OAuth2 token exchange failed',
|
|
112
|
+
code: 'TOKEN_EXCHANGE_FAILED',
|
|
113
|
+
details: error.response?.data || error.message
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Refresh access token using refresh token
|
|
120
|
+
* @param refreshToken Refresh token from previous authentication
|
|
121
|
+
* @returns New token response or error
|
|
122
|
+
*/
|
|
123
|
+
async refreshToken(refreshToken) {
|
|
124
|
+
try {
|
|
125
|
+
this.logger.info('Refreshing access token');
|
|
126
|
+
const response = await this.client.post('/oauth/token', {
|
|
127
|
+
client_id: this.config.clientId,
|
|
128
|
+
client_secret: this.config.clientSecret,
|
|
129
|
+
refresh_token: refreshToken,
|
|
130
|
+
grant_type: 'refresh_token'
|
|
131
|
+
});
|
|
132
|
+
const tokens = response.data;
|
|
133
|
+
this.logger.info('Successfully refreshed token', {
|
|
134
|
+
tokenType: tokens.token_type,
|
|
135
|
+
expiresIn: tokens.expires_in
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
tokens
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.logger.error('Failed to refresh token', {
|
|
144
|
+
error: error.message,
|
|
145
|
+
status: error.response?.status
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: {
|
|
150
|
+
message: 'Token refresh failed',
|
|
151
|
+
code: 'TOKEN_REFRESH_FAILED',
|
|
152
|
+
details: error.response?.data || error.message
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get user information using access token
|
|
159
|
+
* @param accessToken Valid access token
|
|
160
|
+
* @returns User information or error
|
|
161
|
+
*/
|
|
162
|
+
async getUserInfo(accessToken) {
|
|
163
|
+
try {
|
|
164
|
+
this.logger.info('Retrieving user information');
|
|
165
|
+
const response = await this.client.get('/user', {
|
|
166
|
+
headers: {
|
|
167
|
+
'Authorization': `Bearer ${accessToken}`
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const userData = response.data;
|
|
171
|
+
const user = userData.user || userData;
|
|
172
|
+
this.logger.info('Successfully retrieved user information', {
|
|
173
|
+
userId: user.id,
|
|
174
|
+
username: user.username,
|
|
175
|
+
email: user.email
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
user
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.logger.error('Failed to retrieve user information', {
|
|
184
|
+
error: error.message,
|
|
185
|
+
status: error.response?.status
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
error: {
|
|
190
|
+
message: 'Failed to retrieve user information',
|
|
191
|
+
code: 'USER_INFO_FAILED',
|
|
192
|
+
details: error.response?.data || error.message
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Validate access token by making a test API call
|
|
199
|
+
* @param accessToken Token to validate
|
|
200
|
+
* @returns Whether the token is valid
|
|
201
|
+
*/
|
|
202
|
+
async validateToken(accessToken) {
|
|
203
|
+
try {
|
|
204
|
+
const result = await this.getUserInfo(accessToken);
|
|
205
|
+
return result.success;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
this.logger.warn('Token validation failed', { error });
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Revoke access token
|
|
214
|
+
* @param accessToken Token to revoke
|
|
215
|
+
* @returns Success status
|
|
216
|
+
*/
|
|
217
|
+
async revokeToken(accessToken) {
|
|
218
|
+
try {
|
|
219
|
+
this.logger.info('Revoking access token');
|
|
220
|
+
await this.client.post('/oauth/revoke', {
|
|
221
|
+
token: accessToken,
|
|
222
|
+
client_id: this.config.clientId,
|
|
223
|
+
client_secret: this.config.clientSecret
|
|
224
|
+
});
|
|
225
|
+
this.logger.info('Successfully revoked token');
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.logger.error('Failed to revoke token', {
|
|
230
|
+
error: error.message,
|
|
231
|
+
status: error.response?.status
|
|
232
|
+
});
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Simple Session Management Service
|
|
6
|
+
*
|
|
7
|
+
* Handles user session management for OAuth2 authentication:
|
|
8
|
+
* - In-memory session storage with TTL
|
|
9
|
+
* - Token management and refresh
|
|
10
|
+
* - Session validation and cleanup
|
|
11
|
+
* - Automatic expiration handling
|
|
12
|
+
*/
|
|
13
|
+
import { Logger } from '../../logger.js';
|
|
14
|
+
/**
|
|
15
|
+
* Simple in-memory session management service
|
|
16
|
+
*/
|
|
17
|
+
export class SessionService {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.sessions = new Map();
|
|
20
|
+
this.logger = new Logger('SessionService');
|
|
21
|
+
this.logger.info('Simple session service initialized (in-memory)');
|
|
22
|
+
// Start cleanup interval (every 10 minutes)
|
|
23
|
+
this.cleanupInterval = setInterval(() => this.cleanupExpiredSessions(), 10 * 60 * 1000);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Cleanup resources
|
|
27
|
+
*/
|
|
28
|
+
destroy() {
|
|
29
|
+
if (this.cleanupInterval) {
|
|
30
|
+
clearInterval(this.cleanupInterval);
|
|
31
|
+
}
|
|
32
|
+
this.sessions.clear();
|
|
33
|
+
this.logger.info('Session service destroyed');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a new user session
|
|
37
|
+
* @param sessionData Session creation data
|
|
38
|
+
* @returns Created session or error
|
|
39
|
+
*/
|
|
40
|
+
async createSession(sessionData) {
|
|
41
|
+
try {
|
|
42
|
+
this.logger.info('Creating new session', {
|
|
43
|
+
clickupUserId: sessionData.clickupUserId,
|
|
44
|
+
tokenType: sessionData.tokens.token_type
|
|
45
|
+
});
|
|
46
|
+
// Generate unique session ID
|
|
47
|
+
const sessionId = this.generateSessionId();
|
|
48
|
+
// Calculate expiration time
|
|
49
|
+
const expiresAt = new Date(Date.now() + (sessionData.tokens.expires_in * 1000));
|
|
50
|
+
const now = new Date();
|
|
51
|
+
// Create session object
|
|
52
|
+
const session = {
|
|
53
|
+
id: sessionId,
|
|
54
|
+
clickupUserId: sessionData.clickupUserId,
|
|
55
|
+
accessToken: sessionData.tokens.access_token,
|
|
56
|
+
refreshToken: sessionData.tokens.refresh_token,
|
|
57
|
+
tokenType: sessionData.tokens.token_type,
|
|
58
|
+
expiresAt,
|
|
59
|
+
createdAt: now,
|
|
60
|
+
lastAccessedAt: now
|
|
61
|
+
};
|
|
62
|
+
// Store in memory
|
|
63
|
+
this.sessions.set(sessionId, session);
|
|
64
|
+
this.logger.info('Session created successfully', {
|
|
65
|
+
sessionId: session.id,
|
|
66
|
+
clickupUserId: sessionData.clickupUserId,
|
|
67
|
+
expiresAt: session.expiresAt.toISOString()
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
data: session
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.logger.error('Failed to create session', {
|
|
76
|
+
error: error.message,
|
|
77
|
+
sessionData
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: {
|
|
82
|
+
message: 'Failed to create session',
|
|
83
|
+
code: 'SESSION_CREATION_FAILED',
|
|
84
|
+
details: error.message
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get session by ID
|
|
91
|
+
* @param sessionId Session ID
|
|
92
|
+
* @returns Session or null if not found/expired
|
|
93
|
+
*/
|
|
94
|
+
async getSession(sessionId) {
|
|
95
|
+
try {
|
|
96
|
+
this.logger.debug('Getting session', { sessionId });
|
|
97
|
+
const session = this.sessions.get(sessionId);
|
|
98
|
+
if (!session) {
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
data: null
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Check if session is expired
|
|
105
|
+
if (session.expiresAt <= new Date()) {
|
|
106
|
+
this.logger.debug('Session expired, removing', { sessionId });
|
|
107
|
+
this.sessions.delete(sessionId);
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
data: null
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Update last accessed time
|
|
114
|
+
session.lastAccessedAt = new Date();
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
data: session
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.logger.error('Failed to get session', {
|
|
122
|
+
error: error.message,
|
|
123
|
+
sessionId
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: {
|
|
128
|
+
message: 'Failed to get session',
|
|
129
|
+
code: 'SESSION_RETRIEVAL_FAILED',
|
|
130
|
+
details: error.message
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Update session tokens (after refresh)
|
|
137
|
+
* @param sessionId Session ID
|
|
138
|
+
* @param tokens New token data
|
|
139
|
+
* @returns Updated session or error
|
|
140
|
+
*/
|
|
141
|
+
async updateSessionTokens(sessionId, tokens) {
|
|
142
|
+
try {
|
|
143
|
+
this.logger.info('Updating session tokens', { sessionId });
|
|
144
|
+
const session = this.sessions.get(sessionId);
|
|
145
|
+
if (!session) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: {
|
|
149
|
+
message: 'Session not found',
|
|
150
|
+
code: 'SESSION_NOT_FOUND'
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Update session with new tokens
|
|
155
|
+
session.accessToken = tokens.access_token;
|
|
156
|
+
if (tokens.refresh_token) {
|
|
157
|
+
session.refreshToken = tokens.refresh_token;
|
|
158
|
+
}
|
|
159
|
+
session.tokenType = tokens.token_type;
|
|
160
|
+
session.expiresAt = new Date(Date.now() + (tokens.expires_in * 1000));
|
|
161
|
+
session.lastAccessedAt = new Date();
|
|
162
|
+
this.logger.info('Session tokens updated successfully', {
|
|
163
|
+
sessionId,
|
|
164
|
+
newExpiresAt: session.expiresAt.toISOString()
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
success: true,
|
|
168
|
+
data: session
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
this.logger.error('Failed to update session tokens', {
|
|
173
|
+
error: error.message,
|
|
174
|
+
sessionId
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
error: {
|
|
179
|
+
message: 'Failed to update session tokens',
|
|
180
|
+
code: 'SESSION_UPDATE_FAILED',
|
|
181
|
+
details: error.message
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Delete session
|
|
188
|
+
* @param sessionId Session ID
|
|
189
|
+
* @returns Success status
|
|
190
|
+
*/
|
|
191
|
+
async deleteSession(sessionId) {
|
|
192
|
+
try {
|
|
193
|
+
this.logger.info('Deleting session', { sessionId });
|
|
194
|
+
const deleted = this.sessions.delete(sessionId);
|
|
195
|
+
this.logger.info('Session deleted successfully', { sessionId, deleted });
|
|
196
|
+
return {
|
|
197
|
+
success: true,
|
|
198
|
+
data: deleted
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
this.logger.error('Failed to delete session', {
|
|
203
|
+
error: error.message,
|
|
204
|
+
sessionId
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
error: {
|
|
209
|
+
message: 'Failed to delete session',
|
|
210
|
+
code: 'SESSION_DELETION_FAILED',
|
|
211
|
+
details: error.message
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Delete all sessions for a user
|
|
218
|
+
* @param clickupUserId ClickUp user ID
|
|
219
|
+
* @returns Success status
|
|
220
|
+
*/
|
|
221
|
+
async deleteUserSessions(clickupUserId) {
|
|
222
|
+
try {
|
|
223
|
+
this.logger.info('Deleting all sessions for user', { clickupUserId });
|
|
224
|
+
let deletedCount = 0;
|
|
225
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
226
|
+
if (session.clickupUserId === clickupUserId) {
|
|
227
|
+
this.sessions.delete(sessionId);
|
|
228
|
+
deletedCount++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
this.logger.info('All user sessions deleted successfully', {
|
|
232
|
+
clickupUserId,
|
|
233
|
+
deletedCount
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
success: true,
|
|
237
|
+
data: true
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
this.logger.error('Failed to delete user sessions', {
|
|
242
|
+
error: error.message,
|
|
243
|
+
clickupUserId
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
error: {
|
|
248
|
+
message: 'Failed to delete user sessions',
|
|
249
|
+
code: 'USER_SESSIONS_DELETION_FAILED',
|
|
250
|
+
details: error.message
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get all active sessions for a user
|
|
257
|
+
* @param clickupUserId ClickUp user ID
|
|
258
|
+
* @returns List of active sessions
|
|
259
|
+
*/
|
|
260
|
+
async getUserSessions(clickupUserId) {
|
|
261
|
+
try {
|
|
262
|
+
this.logger.debug('Getting user sessions', { clickupUserId });
|
|
263
|
+
const userSessions = [];
|
|
264
|
+
const now = new Date();
|
|
265
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
266
|
+
if (session.clickupUserId === clickupUserId) {
|
|
267
|
+
// Check if session is expired
|
|
268
|
+
if (session.expiresAt <= now) {
|
|
269
|
+
this.sessions.delete(sessionId);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
userSessions.push(session);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
data: userSessions
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
this.logger.error('Failed to get user sessions', {
|
|
283
|
+
error: error.message,
|
|
284
|
+
clickupUserId
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
error: {
|
|
289
|
+
message: 'Failed to get user sessions',
|
|
290
|
+
code: 'USER_SESSIONS_RETRIEVAL_FAILED',
|
|
291
|
+
details: error.message
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Clean up expired sessions
|
|
298
|
+
* @private
|
|
299
|
+
*/
|
|
300
|
+
async cleanupExpiredSessions() {
|
|
301
|
+
try {
|
|
302
|
+
this.logger.debug('Starting expired session cleanup');
|
|
303
|
+
const now = new Date();
|
|
304
|
+
let cleanedCount = 0;
|
|
305
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
306
|
+
if (session.expiresAt <= now) {
|
|
307
|
+
this.sessions.delete(sessionId);
|
|
308
|
+
cleanedCount++;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (cleanedCount > 0) {
|
|
312
|
+
this.logger.info('Cleaned up expired sessions', {
|
|
313
|
+
cleanedCount,
|
|
314
|
+
remainingSessions: this.sessions.size
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
this.logger.error('Failed to cleanup expired sessions', { error });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Generate unique session ID
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
generateSessionId() {
|
|
327
|
+
return 'sess_' + Math.random().toString(36).substring(2, 15) +
|
|
328
|
+
Math.random().toString(36).substring(2, 15) +
|
|
329
|
+
Date.now().toString(36);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get session count (for monitoring)
|
|
333
|
+
*/
|
|
334
|
+
getSessionCount() {
|
|
335
|
+
return this.sessions.size;
|
|
336
|
+
}
|
|
337
|
+
}
|