@ -3,6 +3,7 @@ import { getAccessIdOfBearerToken } from '@ghostfolio/api/helper/bearer-token.he
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service' ;
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service' ;
import { HEADER_KEY_TOKEN , MCP_REALM } from '@ghostfolio/common/config' ;
import { hasRole } from '@ghostfolio/common/permissions' ;
import type { ImpersonationContext } from '@ghostfolio/common/types' ;
import { RequestClient } from '@prisma/client' ;
@ -20,6 +21,10 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes';
* authorization server . A client which reads the challenge asks the user for
* the identifier of the access instead of starting a flow which would fail .
*
* With the subscription , the user who granted the access has the same limits
* as with the other clients : an inactive user and a user who exceeds the daily
* request limit get the status 429 , which also has to come from here .
*
* An authorized request is counted in the analytics of the user who granted
* the access , as the strategies do for the requests of the other clients .
* /
@ -47,24 +52,28 @@ export function createMcpAuthorizationMiddleware({
} ) ;
if ( ! impersonation . isActive ) {
const parameters = [ ` realm=" ${ MCP_REALM } " ` ] ;
return sendUnauthorizedResponse ( { accessId , response } ) ;
}
if ( configurationService . get ( 'ENABLE_FEATURE_SUBSCRIPTION' ) ) {
const user = await userService . user ( { id : impersonation.userId } ) ;
if ( accessId ) {
parameters . push (
'error="invalid_token"' ,
'error_description="The access cannot be resolved"'
) ;
if ( ! user ) {
return sendUnauthorizedResponse ( { accessId , response } ) ;
}
response . setHeader ( 'WWW-Authenticate' , ` Bearer ${ parameters . join ( ', ' ) } ` ) ;
const isRequestLimitExceeded =
hasRole ( user , 'INACTIVE' ) ||
( await userService . isDailyRequestLimitExceeded ( { user } ) ) ;
return response . status ( StatusCodes . UNAUTHORIZED ) . json ( {
error : getReasonPhrase ( StatusCodes . UNAUTHORIZED ) ,
message : 'A valid access is required as a bearer token'
} ) ;
}
if ( isRequestLimitExceeded ) {
return response . status ( StatusCodes . TOO_MANY_REQUESTS ) . json ( {
error : getReasonPhrase ( StatusCodes . TOO_MANY_REQUESTS ) ,
message :
'The request limit of the user who granted the access is exceeded'
} ) ;
}
if ( configurationService . get ( 'ENABLE_FEATURE_SUBSCRIPTION' ) ) {
await userService . updateAnalytics ( {
requestClient : RequestClient.MCP ,
userId : impersonation.userId
@ -78,3 +87,27 @@ export function createMcpAuthorizationMiddleware({
return next ( ) ;
} ;
}
function sendUnauthorizedResponse ( {
accessId ,
response
} : {
accessId : string ;
response : Response ;
} ) {
const parameters = [ ` realm=" ${ MCP_REALM } " ` ] ;
if ( accessId ) {
parameters . push (
'error="invalid_token"' ,
'error_description="The access cannot be resolved"'
) ;
}
response . setHeader ( 'WWW-Authenticate' , ` Bearer ${ parameters . join ( ', ' ) } ` ) ;
return response . status ( StatusCodes . UNAUTHORIZED ) . json ( {
error : getReasonPhrase ( StatusCodes . UNAUTHORIZED ) ,
message : 'A valid access is required as a bearer token'
} ) ;
}