012 json time format bug fix

This commit is contained in:
2025-10-13 08:50:34 +03:00
parent 2fdcd9598a
commit 55fa955326
4 changed files with 520 additions and 6 deletions
+1 -1
View File
@@ -77,7 +77,7 @@
<!--- global settings --->
<cfset request.RECORDS_PER_PAGE=500/>
<cfset request.APP_VERSION="0.00.011"/>
<cfset request.APP_VERSION="0.00.012"/>
<cfset request.STAND=getStand()/>
<!--- application constants --->
+422
View File
@@ -0,0 +1,422 @@
<cfcomponent
displayname="Application"
output="true"
hint="Handle the application.">
<!--- Pseudoconstructor --->
<!--- Set up the application. --->
<cfset this.Name = "PAYG-REPORT" />
<cfset this.applicationTimeout = createTimeSpan( 0, 3, 0, 0 ) />
<cfset this.sessionmanagement="Yes"/>
<cfset this.clientmanagement="No"/>
<cfset this.sessiontimeout=CreateTimeSpan(0, 1, 0, 0)/>
<cfset this.setclientcookies="No"/>
<cfset this.mappings = structNew() />
<cfset this.mappings["/mod"] = getDirectoryFromPath(getCurrentTemplatePath()) & "mod/" />
<cfset this.customTagPaths = expandPath(getDirectoryFromPath(getCurrentTemplatePath()) & "mod")/>
<!--- SSO --->
<cfset this.client_id = "payg-report.deck.nubes.ru"/>
<cfset this.auth_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth" />
<cfset this.access_token_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token" />
<cfset this.idpCertUrl = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs" />
<cfset this.client_secret = createObject("java", "java.lang.System").getEnv("IDP_CLIENT_SECRET")/>
<cfset this.datasource = "rpt"/>
<cfset this.defaultdatasource = this.datasource/>
<cfset request.DS = "#this.datasource#">
<cfset request.language="">
<cfset getDS(this.datasource)/>
<!--- кажется, нужно инициализировать датасорцы в псевдоконструкторе - onRequest не получается --->
<!--- *** проверить, что есть - если есть, не создавать --->
<!--- это работает с административно заданным датасорцем, хотя, казалось бы, не должно
(без окружения функция вернет пустую структуру). Видимо, административный датасорц обнаруживается раньше.
Проверять на пустоту не хочется, чтобы не плодить в псевдоконструкторе переменные --->
<!--- <cfdump var=#variables#/> --->
<cffunction
name="OnApplicationStart"
access="public"
returntype="boolean"
output="false"
hint="Fires when the application is first created.">
<!--- *** мы не проверяем срок жизни сертификата и его отзыв--->
<!--- Obtain idp certificate OnApplicationStart --->
<cfset application.idpCertificate = getIdpCertificate(this.idpCertUrl)/>
<cfreturn true />
</cffunction>
<cffunction
name="OnRequest"
access="public"
returntype="void"
output="true"
hint="Fires after pre page processing is complete.">
<cfargument name="template" type="string" required="true"/>
<cfset request.startTickCount=getTickCount()/>
<cfset setEncoding("FORM", "UTF-8")>
<cfset setEncoding("URL", "UTF-8")>
<cfset request.PERMISSION_NONE=0/>
<cfset request.PERMISSION_READ=1/>
<cfset request.PERMISSION_WRITE=2/>
<!--- <cfset request.UNDEFINED_USR_ID=-1/>
<cfset request.ANONYMOUS_USR_ID=2/>
<cfset request.GUEST_USR_ID=3/> --->
<!--- global settings --->
<cfset request.RECORDS_PER_PAGE=500/>
<cfset request.APP_VERSION="0.00.011"/>
<cfset request.STAND=getStand()/>
<!--- application constants --->
<!---<cfset request.SMTP_SERVER="172.16.16.16"/>
<cfset request.MAIL_FROM_FOR_NOTIFICATION="Pipeline tracker <pipeline@mail.ru>"/>
<cfset request.SUBJECT_PREFIX="[PIPELINE TRACKER] "/>--->
<!---<cfset request.BASE_URL="https://smtest.dtln.local/pipeline/"/>*** сделать нормально. Внимание! При наличии прокси CGI.SERVER_NAME может быть заменено --->
<cfset local = {} />
<cfset local.basePath = getDirectoryFromPath(
getCurrentTemplatePath()
) />
<cfset local.targetPath = getDirectoryFromPath(
expandPath( arguments.template )
) />
<cfset local.requestDepth = (
listLen( local.targetPath, "\/" ) -
listLen( local.basePath, "\/" )
) />
<cfset request.webRoot = repeatString(
"../",
local.requestDepth
) />
<!---
While we wouldn't normally do this for every page
request (it would normally be cached in the
application initialization), I'm going to calculate
the site URL based on the web root.
--->
<cfset request.siteUrl = (
IIF(
(CGI.server_port_secure <!--- CGI.https EQ "On" does not work with apache+tomcat and nginx+tomcat --->),
DE( "https://" ),
DE( "http://" )
) &
cgi.http_host &
reReplace(
getDirectoryFromPath( arguments.template ), "([^\\/]+[\\/]){#local.requestDepth#}$",
"",
"one"
)
) />
<cfset request.thisPage=Replace(ReplaceNoCase(expandPath(ARGUMENTS.template), local.basePath, ""), "\", "/")/>
<!--- <cfset request.thisUrl="#request.thisPage##len(CGI.queryString)#"/> чревато циклом --->
<!--- получение сертификата может занимать длительное время.
Блокировать на это время приложение или сессию не следует.
Вообще не следует ничего блокировать на время сетевого взаимодействия
Нижеследующая, на первый взгляд, странная конструкция ровно для этого придумана
--->
<!--- <cfoutput>onRequest:check for cert started #(getTickCount()-request.startTickCount)#</cfoutput> --->
<cfset var idpCertificateExists = false/>
<cflock scope="application" type="readonly" timeout=1>
<cfset request.APP_NAME=this.Name/>
<cfset idpCertificateExists = structKeyExists(application,"idpCertificate")/>
</cflock>
<cfif NOT idpCertificateExists>
<!--- Obtain IDP Certificate (onRequest) --->
<cfset var idpCertificate = getIdpCertificate(this.idpCertUrl) />
<cflock scope="application" type="exclusive" timeout=1>
<cfset application.idpCertificate = idpCertificate/>
</cflock>
</cfif>
<!--- <cfoutput>onRequest:check for cert finished #(getTickCount()-request.startTickCount)#</cfoutput> --->
<cfset this.redirect_uri = "https://#CGI.SERVER_NAME##CGI.SCRIPT_NAME#" />
<cfinclude template="inc/functions.cfm"/>
<cfcookie name="CFID" value="#session.CFID#"/>
<cfcookie name="CFTOKEN" value="#session.CFTOKEN#"/>
<!--- SSO --->
<cflock scope="session" type="exclusive" timeout="3">
<!---
<cfdump var=#session#/>
<cfoutput>#(getTickCount()-request.startTickCount)#</cfoutput>--->
<!---
Это первая проба, с сессией
Когда протухнет сессия, мы можем пойти к IDP и повторно аутентифицироваться. Поскольку у нас сохраняются куки IDP, его сессия, возможно, будет жива. Если мы обновляли сессию IDP в другом приложении SSO, по всей видимости, она будет продолжаться. Если мы видим, что сессия IDP состарилась и хочет протухнуть, мы можем ее освежить и получить новый access token (который мы пока не используем - мы пользуемся ответом IDP) Наверное, правильно обходиться для этих целей вообще без сессии *** --->
<!---
проверить sso
? сделать рефреш
? сделать логаут
? сделать отображение ошибки IDP
+ сделать получение сертификата
--->
<cfif structKeyExists(session, "auth")>
<cfset request.auth = structCopy(session.auth)/>
<!--- <cfset request.auth.wz = "WZ01553"/> --->
<!--- тут нужно сделать проверку и рефреш токена --->
<!--- сессия есть --->
<cfelse>
<!--- сессии нет --->
<cfset session.auth_state=createGUID().toString()/>
<cfscript>
jwt = new lib.jwt(this.client_secret);
idp = new lib.oauth2(this.client_id, this.client_secret, this.auth_endpoint, this.access_token_endpoint, this.redirect_uri);
/*
часто появляется ошибка: после неудачного запроса к IDP
в адресной строке браузера остается все, с чем его редиректил IDP,
а повторный запрос с этим контентом уже не работает. Получаем 400 Bad Request -
может быть, KeyCloak так защищается от циклических редиректов
Не очень понятно, как предотвратить повторный запрос (можно, конечно, использовать сессию, но странно)
*/
//writeOutput(getTickCount()-request.startTickCount);
if(structKeyExists(url, "code")) { //обрабатываем редирект от IDP *** наличие поля code в URL введет нас в заблуждение, но эvar = data, label =то только без валидной сессии
//dump(var = url.code, label = "URL.code");
// Request access token from idp with the
// authorization code that we got via the URL
//aFormFields = [];
//data = idp.makeAccessTokenRequest((url.code).content, aFormFields);
try {
writeOutput("idp.makeAccessTokenRequest");
writeOutput(getTickCount()-request.startTickCount);
resp = idp.makeAccessTokenRequest(url.code/*, aFormFields*/);
writeOutput("idp.makeAccessTokenRequest Done");
writeOutput(getTickCount()-request.startTickCount);
//dump(resp);
data = deserializeJson(resp.content);
//writedump(data);
// Print full response from idp
//dump(var = data, label = "makeAccessTokenRequest - Response from idp");
//echo('<a href="./index.cfm?refresh=#data.refresh_token#">Refresh token #data.refresh_token#</a>');
session.auth.expires_in = data.expires_in;
session.auth.refresh_expires_in = data.refresh_expires_in;
session.auth.access_token = data.access_token;
session.auth.refresh_token = data.refresh_token;
session.auth.id_token = data.id_token;
session.auth.token_type = data.token_type;
session.idp_response_content = structCopy(data);
//flush()
lock scope="application" type="readonly" timeout="1" {
token_data = (jwt.decode(data.access_token, application.idpCertificate,"RS256"));
}
writeOutput("jwt.decode Done");
writeOutput(getTickCount()-request.startTickCount);
writedump(token_data);
session.auth.login = token_data.preferred_username;
session.auth.wz = token_data.ClientID;
session.auth.fullname = token_data.name;
session.auth.groups = token_data.groups;
session.auth.session_state = token_data.session_state;
session.auth.sid = token_data.sid;
session.auth.auth_time = token_data.auth_time;
session.auth.exp = token_data.exp;
session.auth.iat = token_data.iat;
//session.sid = token_data.sid;
session.token_data = structCopy(token_data);
location(this.redirect_uri,false);
} catch (e) {echo('<br>****************** #e.message# : #e.detail# **********');}
} else {
strURL = idp.buildRedirectToAuthURL({"scope":'openid profile email',"state":session.auth_state,"allow_signup":false});
echo('<a href="#strURL#">self-made Auth link (idp) #strURL#</a> <br> <br>');
location(strUrl,false);
}
</cfscript>
</cfif>
</cflock><!--- session exclusive --->
<cfif structKeyExists(request, "auth")>
<cfinclude template="#ARGUMENTS.template#"/>
<cfelse>
<cfset currentDir = (
lCase(
replace(
getDirectoryFromPath(
replaceNoCase(
expandPath(
ARGUMENTS.template
),
getDirectoryFromPath(
getCurrentTemplatePath()
),
""
)
), "\", "/", "ALL"
)
)
)/>
<!--- <cfif currentDir EQ "saml/"><!--- note traling slash --->
<cfinclude template="#ARGUMENTS.template#"/>
<cfelse>
<cfinclude template="login.cfm" />
</cfif> --->
</cfif>
<cfreturn />
</cffunction>
<cffunction
name="OnRequestEnd"
access="public"
returntype="void"
output="true"
hint="Fires after the page processing is complete.">
<!--- Attention! Before CF9, OnrequestEnd is not executed in case of redirect. That is why we use session.save_login --->
<cfif structKeyExists(session, "save_login")>
<cfif session.save_login EQ "">
<!--- unset (expire) cookie --->
<cfcookie expires="-1" name="portalUser" value="">
<cfelse>
<!--- set persistent cookie with no expiration --->
<cfcookie expires="NEVER" name="portalUser" value="#encrypt(session.save_login,COOKIEENCKEY)#">
</cfif>
<cfset structDelete(session, "save_login")>
</cfif>
<cfreturn />
</cffunction>
<cffunction
name="getStand"
access="private"
returntype="string"
output="true"><!--- *** duplicated in svc-api --->
<cftry>
<cfquery name=qConfig>
select value as stand from config
where name='STAND'
</cfquery>
<cfreturn qConfig.stand/>
<cfcatch type="ANY">
<!--- do nothing, default will be returned --->
</cfcatch>
</cftry>
<cfreturn ""/>
</cffunction>
<cffunction
name="getDS"
access="private"
returntype="void"
output="true"
hint="Configure data source from environment variables (if datasource with the name provided already exists, does nothing). Convention: data source name is an environment varialble prefix">
<cfargument name="dsname" type="string" required="true"/>
<cfargument name="prefix" type="string" default=#dsname#/>
<cftry>
<cfquery name="qTestDs" datasource=#arguments.dsname#>
select 1;
</cfquery>
<cfcatch type="any">
<cfset var ds={}/>
<cfset var system = createObject("java", "java.lang.System")/>
<cfloop list="class,connectionString,database,driver,dbdriver,host,port,type,url,username,password,bundleName,bundleVersion,connectionLimit,liveTimeout,validate" item="field"><!--- driver vs dbdriver --->
<cfset var value=system.getEnv("#arguments.prefix#_#field#")/>
<cfif isDefined("value") AND len(value)>
<cfset structInsert(ds,field,value)/>
</cfif>
</cfloop>
<cfif structIsEmpty(ds)>
<cfthrow type="application" message="Datasource not configured" detail="Datasource not defined in the environment. Expected prefix is #arguments.prefix#"/>
</cfif>
<!--- test datasource (just to get exception if invalid) --->
<cftry>
<cfquery name="qTestDsNextTry" datasource=#ds#>
select 2;
</cfquery>
<cfcatch type="any">
<!--- <cfdump var=#ds#/><cfabort/> --->
<cfdump var=#arguments#/>
<cfdump var=#ds#/>
<cfrethrow/>
</cfcatch>
</cftry>
<cfset this.datasources["#arguments.dsname#"]=#ds#/> <!--- Интересно, доступен ли здесь this --->
</cfcatch>
</cftry>
<!--- <cfdump var=#ds#/> --->
<cfreturn/>
</cffunction>
<cfscript>
//https://keycloak.nubes.ru/admin
//https://keycloak.nubes.ru/realms/SSH_CA/account //smishchuk@mgmt.nubes.ru
//https://stackoverflow.com/questions/28658735/what-are-keycloaks-oauth2-openid-connect-endpoints
//http://https://keycloak.nubes.ru/realms/SSH_CA/.well-known/openid-configuration
//https://keycloak.nubes.ru/realms/cloud/.well-known/openid-configuration тут ссылка
//https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs тут серт
//только надо его обрамить -----BEGIN CERTIFICATE-----
function getIdpCertificate(certUrl) {
var httpService = new http();
httpService.setMethod( "get" );
httpService.setCharset( "utf-8" );
httpService.setUrl(arguments.certUrl);
var result = httpService.send().getPrefix();
var status_code = result.ResponseHeader['Status_Code'];
if ('200' == status_code) {
var content = deserializeJson(result.FileContent);
//мы подразумеваем структуру конкретно KeyCloak
var key = "";
for (key in content.keys) {
if ("RSA" == key.kty AND "sig" == key.use) {
return "-----BEGIN CERTIFICATE-----" & key.x5c[1] & "-----END CERTIFICATE-----";
}
}
throw(message="RSA cetificate not found");
} else {
throw (message="Cannot obtain IDP certificate, request failed", detail="Status_Code #status_code#");
}
}
</cfscript>
</cfcomponent>
+7 -5
View File
@@ -26,7 +26,7 @@
output="false"
hint="formates date and time like this: 2023-12-18T15:41:42.516+0300">
<cfargument name="input" required="true" />
<cfreturn #dateFormat( input, "yyyy-mm-dd" )#T#timeFormat( input, "HH:mm:ss.lXX" )#/>
<cfreturn "#dateFormat( input, "yyyy-mm-dd" )#T#timeFormat( input, "HH:mm:ss.lXX" )#"/>
</cffunction>
@@ -69,10 +69,12 @@
/>
<cfset out={
"hours"=#hours#,
"vcdComputingLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')#",
"vcdStorageLoadedAt"="#iso8601dtFormat(report.qStorageAge.dt_load,'YYYY-MM-DD')#",
"s3StorageLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')#",
"s3OpsTrfLoadedAt"="#iso8601dtFormat(report.qS3OpsTrfAge.dt_load,'YYYY-MM-DD')#",
"dtStart="=#iso8601dtFormat(dt_start)#,
"dtFinish"=#iso8601dtFormat(dt_finish)#,
"vcdComputingLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#",
"vcdStorageLoadedAt"="#iso8601dtFormat(report.qStorageAge.dt_load)#",
"s3StorageLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#",
"s3OpsTrfLoadedAt"="#iso8601dtFormat(report.qS3OpsTrfAge.dt_load)#",
"records"=#report.qCharge.recordCount#,
"queryDurationMs"=#(getTickCount() - msStartAt)#,
"report"=#report.qCharge.reduce((result, row) => {
+90
View File
@@ -0,0 +1,90 @@
<cfset msStartAt=getTickCount()/>
<cfparam name="dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
<cfparam name="dt_start" type="date" default=#dateAdd('m',-1,dt_finish)#/>
<cfset hours=dateDiff('h',dt_start,dt_finish)/>
<cfset tenant_wz_index=val(mid(request.auth.wz,3,5))/><!--- cut off WZ --->
<cffunction name="snake2camel"
returntype="string"
output="false"
hint="convert snake style name to camel style name">
<cfargument name="snake" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.snake,"_([a-z])","\u\1","ALL")#/>
</cffunction>
<cffunction name="camel2snake"
returntype="string"
output="false"
hint="convert camel style name to snake style name">
<cfargument name="camel" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.camel,"([A-Z])","_\l\1","ALL")#/>
</cffunction>
<cffunction name="iso8601dtFormat"
returntype="string"
output="false"
hint="formates date and time like this: 2023-12-18T15:41:42.516+0300">
<cfargument name="input" required="true" />
<cfreturn #dateFormat( input, "yyyy-mm-dd" )#T#timeFormat( input, "HH:mm:ss.lXX" )#/>
</cffunction>
<cffunction name="query2json"
output="false"
hint="print query in json format as array of structures">
<cfargument name="qry" type="query" required="true"/><!--- column names should not contain commas --->
<cfargument name="convertSnakeToCamel" type="boolean" default=true/>
<cfset var columnsIn=#arguments.qry.columnList()#/>
<cfset var columnsOut=""/>
<cfif arguments.convertSnakeToCamel>
<cfloop list=#columnsIn# item="col">
<cfset columnsOut=listAppend(columnsOut,snake2camel(col))/>
</cfloop>
<cfset var outArray=arrayNew(1)/>
//copy fields to structure, renaming fields, this preserves data type
<cfloop query=#arguments.qry#>
<cfset var structRow=structNew()/>
<cfloop index="i" from="1" to=#listLen(columnsOut)#>
<cfset structInsert(structRow,"#listGetAt(#columnsOut#,i)#",arguments.qry["#listGetAt(#columnsIn#,i)#"])/>
</cfloop>
<cfset arrayAppend(outArray,structRow)/>
</cfloop>
<cfreturn serializeJSON(outArray)/>
<cfelse>
<cfreturn serializeJSON(arguments.qry,"struct")/>
</cfif>
</cffunction>
<cfmodule template="mod/payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
output="report"
debug=#isDefined("DEBUG")#
/>
<cfset out={
"hours"=#hours#,
"dtStart="=#dt_start#,
"dtFinish"=#dt_finish#,
"vcdComputingLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#",
"vcdStorageLoadedAt"="#iso8601dtFormat(report.qStorageAge.dt_load)#",
"s3StorageLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#",
"s3OpsTrfLoadedAt"="#iso8601dtFormat(report.qS3OpsTrfAge.dt_load)#",
"records"=#report.qCharge.recordCount#,
"queryDurationMs"=#(getTickCount() - msStartAt)#,
"report"=#report.qCharge.reduce((result, row) => {
result.append(row);
return result;
}, [])#
}/>
<cfcontent
type="application/json"
/><cfoutput>#serializeJSON(out)#</cfoutput>