@@ -6,6 +6,7 @@ const TokenManager = require('./tokenManager');
6
6
const Logger = require ( './logs' ) ;
7
7
const log = new Logger ( 'Mattermost' ) ;
8
8
9
+ // Token operations
9
10
class TokenService {
10
11
constructor ( secret , expiresIn , encryptionKey ) {
11
12
this . tokenManager = new TokenManager ( secret , expiresIn , encryptionKey ) ;
@@ -20,119 +21,147 @@ class TokenService {
20
21
}
21
22
}
22
23
23
- class MattermostService {
24
- constructor ( config ) {
25
- this . validateConfig ( config ) ;
26
-
27
- this . token = config . token ;
28
- this . serverUrl = config . server_url ;
29
- this . username = config . username ;
30
- this . password = config . password ;
31
- this . disabledEndpoints = config . api_disabled || [ ] ;
32
-
24
+ // Mattermost authentication
25
+ class MattermostAuthService {
26
+ constructor ( { serverUrl, username, password } ) {
33
27
this . client = new Client4 ( ) ;
34
- this . client . setUrl ( this . serverUrl ) ;
35
-
36
- this . tokenService = new TokenService (
37
- this . token || 'fallback-secret-at-least-32-chars' ,
38
- config . roomTokenExpire || '15m' ,
39
- config . encryptionKey || 'fallback-encryption-key-32chars'
40
- ) ;
41
- }
42
-
43
- validateConfig ( config ) {
44
- if ( ! config . enabled || ! config . server_url || ! config . token || ! config . username || ! config . password ) {
45
- throw new Error ( 'Invalid Mattermost configuration' ) ;
46
- }
28
+ this . client . setUrl ( serverUrl ) ;
29
+ this . username = username ;
30
+ this . password = password ;
47
31
}
48
32
49
- async authenticate ( ) {
33
+ async login ( ) {
50
34
try {
51
35
const user = await this . client . login ( this . username , this . password ) ;
52
36
log . debug ( 'Logged into Mattermost as' , user . username ) ;
53
37
} catch ( error ) {
54
38
log . error ( 'Failed to log into Mattermost:' , error ) ;
55
39
}
56
40
}
41
+ }
42
+
43
+ // Meeting-related operations
44
+ class MeetingService {
45
+ constructor ( tokenService , serverUrl , disabledEndpoints = [ ] , secure = false ) {
46
+ this . tokenService = tokenService ;
47
+ this . serverUrl = serverUrl ;
48
+ this . disabledEndpoints = disabledEndpoints ;
49
+ this . secure = secure ;
50
+ }
57
51
58
52
isEndpointDisabled ( endpoint ) {
59
53
return this . disabledEndpoints . includes ( endpoint ) ;
60
54
}
61
55
62
- createMeetingToken ( userId ) {
63
- const payload = {
56
+ createTokenPayload ( userId ) {
57
+ return {
64
58
userId,
65
59
roomId : uuidV4 ( ) ,
66
60
timestamp : Date . now ( ) ,
67
61
} ;
62
+ }
63
+
64
+ createMeetingURL ( req ) {
65
+ return `${ this . getBaseURL ( req ) } /join/${ uuidV4 ( ) } ` ;
66
+ }
68
67
68
+ createSecureMeetingURL ( req , userId ) {
69
+ const payload = this . createTokenPayload ( userId ) ;
69
70
const token = this . tokenService . createToken ( payload ) ;
70
- return { token, payload } ;
71
+ return {
72
+ url : `${ this . getBaseURL ( req ) } /mattermost/join/${ encodeURIComponent ( token ) } ` ,
73
+ payload,
74
+ } ;
71
75
}
72
76
73
- validateToken ( token ) {
77
+ decodeToken ( token ) {
74
78
return this . tokenService . decodeToken ( token ) ;
75
79
}
76
80
77
- getMeetingURL ( req , roomToken ) {
81
+ getBaseURL ( req ) {
78
82
const host = req . headers . host ;
79
83
const protocol = host . includes ( 'localhost' ) ? 'http' : 'https' ;
80
- return `${ protocol } ://${ host } /mattermost/join/ ${ encodeURIComponent ( roomToken ) } ` ;
84
+ return `${ protocol } ://${ host } ` ;
81
85
}
82
86
}
83
87
88
+ // Just handles routing and delegates everything
84
89
class MattermostController {
85
- constructor ( app , mattermostCfg , htmlInjector , clientHtml ) {
90
+ constructor ( app , config , htmlInjector , clientHtml ) {
86
91
try {
87
- this . service = new MattermostService ( mattermostCfg ) ;
92
+ this . validateConfig ( config ) ;
93
+
94
+ const tokenService = new TokenService (
95
+ config . token || 'fallback-secret-at-least-32-chars' ,
96
+ config . roomTokenExpire || '15m' ,
97
+ config . encryptionKey || 'fallback-encryption-key-32chars'
98
+ ) ;
99
+
100
+ this . authService = new MattermostAuthService ( {
101
+ serverUrl : config . server_url ,
102
+ username : config . username ,
103
+ password : config . password ,
104
+ } ) ;
105
+
106
+ this . meetingService = new MeetingService (
107
+ tokenService ,
108
+ config . server_url ,
109
+ config . api_disabled ,
110
+ config . security
111
+ ) ;
112
+
113
+ this . token = config . token ;
114
+ this . app = app ;
115
+ this . htmlInjector = htmlInjector ;
116
+ this . clientHtml = clientHtml ;
117
+
118
+ this . authService . login ( ) ;
119
+ this . setupRoutes ( ) ;
120
+
88
121
} catch ( error ) {
89
122
log . error ( 'MattermostController disabled due to config error:' , error . message ) ;
90
- return ;
91
123
}
124
+ }
92
125
93
- this . htmlInjector = htmlInjector ;
94
- this . clientHtml = clientHtml ;
95
- this . app = app ;
96
- this . token = mattermostCfg . token ;
97
-
98
- this . service . authenticate ( ) ;
99
- this . setupRoutes ( ) ;
126
+ validateConfig ( cfg ) {
127
+ if ( ! cfg . enabled || ! cfg . server_url || ! cfg . token || ! cfg . username || ! cfg . password ) {
128
+ throw new Error ( 'Invalid Mattermost configuration' ) ;
129
+ }
100
130
}
101
131
102
132
setupRoutes ( ) {
103
133
this . app . post ( '/mattermost' , ( req , res ) => {
104
- if ( this . service . isEndpointDisabled ( 'mattermost' ) ) {
134
+ if ( this . meetingService . isEndpointDisabled ( 'mattermost' ) ) {
105
135
return res . end ( '`This endpoint has been disabled`. Please contact the administrator.' ) ;
106
136
}
107
137
108
138
const { token, text, command, channel_id, user_id } = req . body ;
109
-
110
139
if ( token !== this . token ) {
111
140
log . error ( 'Invalid token attempt' , { token } ) ;
112
141
return res . status ( 403 ) . send ( 'Invalid token' ) ;
113
142
}
114
143
115
- if ( command ?. trim ( ) === '/p2p' || text ?. trim ( ) === '/p2p' ) {
144
+ if ( this . isP2PCommand ( command , text ) ) {
116
145
try {
117
- const { token : roomToken } = this . service . createMeetingToken ( user_id ) ;
118
- const meetingUrl = this . service . getMeetingURL ( req , roomToken ) ;
146
+ const meetingUrl = this . generateMeetingUrl ( req , user_id ) ;
147
+ const message = this . getMeetingResponseMessage ( meetingUrl ) ;
119
148
120
149
return res . json ( {
121
150
response_type : 'in_channel' ,
122
- text : `🔗 [Click here to join your private meeting]( ${ meetingUrl } )` ,
151
+ text : message ,
123
152
channel_id,
124
153
} ) ;
125
154
} catch ( error ) {
126
- log . error ( 'Token creation failed' , error ) ;
127
- return res . status ( 500 ) . send ( 'Error creating meeting') ;
155
+ log . error ( 'Meeting creation failed' , { error, user_id } ) ;
156
+ return res . status ( 500 ) . json ( { error : 'Failed to create meeting' } ) ;
128
157
}
129
158
}
130
159
131
160
return res . status ( 404 ) . send ( 'Command not recognized' ) ;
132
161
} ) ;
133
162
134
163
this . app . get ( '/mattermost/join/:roomToken' , ( req , res ) => {
135
- if ( this . service . isEndpointDisabled ( 'mattermost' ) ) {
164
+ if ( this . meetingService . isEndpointDisabled ( 'mattermost' ) ) {
136
165
return res . end ( 'This endpoint has been disabled' ) ;
137
166
}
138
167
@@ -143,14 +172,13 @@ class MattermostController {
143
172
}
144
173
145
174
try {
146
- const payload = this . service . validateToken ( roomToken ) ;
147
- log . debug ( 'Decoded payload' , payload ) ;
148
-
175
+ const payload = this . meetingService . decodeToken ( roomToken ) ;
149
176
if ( ! payload || ! payload . userId || ! payload . roomId ) {
150
177
log . error ( 'Invalid or malformed token payload' , payload ) ;
151
178
return res . status ( 400 ) . send ( 'Invalid token' ) ;
152
179
}
153
180
181
+ log . debug ( 'Decoded payload' , payload ) ;
154
182
return this . htmlInjector . injectHtml ( this . clientHtml , res ) ;
155
183
} catch ( error ) {
156
184
log . error ( 'Token processing error' , {
@@ -161,6 +189,25 @@ class MattermostController {
161
189
}
162
190
} ) ;
163
191
}
192
+
193
+ isP2PCommand ( command , text ) {
194
+ const normalizedCommand = command ?. trim ( ) ;
195
+ const normalizedText = text ?. trim ( ) ;
196
+ return normalizedCommand === '/p2p' || normalizedText === '/p2p' ;
197
+ }
198
+
199
+ generateMeetingUrl ( req , userId ) {
200
+ if ( this . meetingService . secure ) {
201
+ return this . meetingService . createSecureMeetingURL ( req , userId ) . url ;
202
+ }
203
+ return this . meetingService . createMeetingURL ( req ) ;
204
+ }
205
+
206
+ getMeetingResponseMessage ( meetingUrl ) {
207
+ return this . meetingService . secure
208
+ ? `🔒 [Join your secure private meeting](${ meetingUrl } )`
209
+ : `🌐 Join meeting: ${ meetingUrl } ` ;
210
+ }
164
211
}
165
212
166
213
module . exports = MattermostController ;
0 commit comments