Compare commits
2
Commits
55fa955326
...
a8bfd3de9e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8bfd3de9e | ||
|
|
46169b449e |
@@ -0,0 +1,7 @@
|
||||
t test
|
||||
<cftry>
|
||||
<cfdump var=#session#/>
|
||||
<cfcatch type="any"><cfdump var=#cfcatch#/></cfcatch>
|
||||
</cftry/>
|
||||
|
||||
<cfset jwt = new lib.jwt(this.client_secret)/>
|
||||
+35
-1
@@ -77,7 +77,7 @@
|
||||
|
||||
<!--- global settings --->
|
||||
<cfset request.RECORDS_PER_PAGE=500/>
|
||||
<cfset request.APP_VERSION="0.00.012"/>
|
||||
<cfset request.APP_VERSION="0.00.013"/>
|
||||
<cfset request.STAND=getStand()/>
|
||||
|
||||
<!--- application constants --->
|
||||
@@ -158,7 +158,41 @@
|
||||
<cfcookie name="CFID" value="#session.CFID#"/>
|
||||
<cfcookie name="CFTOKEN" value="#session.CFTOKEN#"/>
|
||||
|
||||
|
||||
|
||||
<!--- SSO --->
|
||||
<!--- Если без сессии. Проверяем наличие токена в заголовке (и в куках?)
|
||||
Если его нет, идем к IDP за токеном. Получив токен, складываем его в куки
|
||||
Если токен есть, распаковываем его. Проверяем протухание. Если прошла установленная часть времени жизни, идем за рефрешем (внимание: это сработает только в браузере,
|
||||
если клиент не умеет редиректы - то токен должен быть достаточно свежий)
|
||||
(для начала делаем без рефреша, только проверяем протухание)
|
||||
А рефреш можно сделать на серверной стороне?
|
||||
Кладем данные из токена в реквест и наслаждаемся --->
|
||||
|
||||
<!--- <cfscript>
|
||||
var requestData = GetHttpRequestData();
|
||||
var headers = requestData.headers;
|
||||
|
||||
writedump(headers);
|
||||
|
||||
if (structKeyExists(headers,"authorization")) {
|
||||
var authorizationHeader = headers.authorization;
|
||||
if (authorizationHeader.startsWith("Bearer ")) {
|
||||
var bearerToken = Mid(authorizationHeader, 8);
|
||||
writeDump(bearerToken);
|
||||
var token=toString(binaryDecode(bearerToken, "base64"));
|
||||
writeDump(token);
|
||||
jwt = new lib.jwt(this.client_secret);
|
||||
lock scope="application" type="readonly" timeout="1" {
|
||||
var token_data = (jwt.decode(token, application.idpCertificate,"RS256"));
|
||||
}
|
||||
writeDump(token_data);
|
||||
abort;
|
||||
}
|
||||
} else {
|
||||
writeoutput("No Authorization header present");//abort;
|
||||
}
|
||||
</cfscript> --->
|
||||
<cflock scope="session" type="exclusive" timeout="3">
|
||||
<!---
|
||||
<cfdump var=#session#/>
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
<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>
|
||||
@@ -16,6 +16,7 @@
|
||||
<cfif isDefined("output_xls")><cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/><layout:xml qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.xml"/>
|
||||
@@ -23,6 +24,7 @@
|
||||
</cfif><cfif isDefined("output_json")><cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/><!--- <cfdump var=#report#/><cfabort/> ---><layout:json qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.json"/>
|
||||
@@ -57,6 +59,7 @@
|
||||
<cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/>
|
||||
|
||||
@@ -61,13 +61,13 @@
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cfmodule template="mod/payg.cfm"
|
||||
<!--- <cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/>
|
||||
<cfset out={
|
||||
/> --->
|
||||
<!--- <cfset out={
|
||||
"hours"=#hours#,
|
||||
"dtStart="=#iso8601dtFormat(dt_start)#,
|
||||
"dtFinish"=#iso8601dtFormat(dt_finish)#,
|
||||
@@ -81,7 +81,7 @@
|
||||
result.append(row);
|
||||
return result;
|
||||
}, [])#
|
||||
}/>
|
||||
}/> ---><cfset out={"test"=true}/>
|
||||
|
||||
<cfcontent
|
||||
type="application/json"
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
<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>
|
||||
|
||||
|
||||
+21
-9
@@ -5,9 +5,10 @@
|
||||
<cfparam name="ATTRIBUTES.dt_start" type="date" default=#dateAdd('m',-1,ATTRIBUTES.dt_finish)#/>
|
||||
<cfparam name="ATTRIBUTES.output" type="string" default="report"/>
|
||||
<cfparam name="ATTRIBUTES.debug" type="boolean" default=false/>
|
||||
<cfparam name="ATTRIBUTES.tenant_wz_index" type="string"/>
|
||||
|
||||
<cfset hours=dateDiff('h', ATTRIBUTES.dt_start, ATTRIBUTES.dt_finish)/>
|
||||
<cfset tenant_wz_index=val(mid(request.auth.wz,3,5))/><!--- cut off WZ --->
|
||||
|
||||
|
||||
|
||||
<!--- QoQ does not support join syntax, at least of Lucee 5.4--->
|
||||
@@ -17,10 +18,15 @@
|
||||
<cfquery name="qSpec">
|
||||
select * from (
|
||||
values
|
||||
('Объектное хранилище S3 CEPH','Дисковое пространство: Объем хранения','paas.s3.ceph.vol-m','ГБайт',1.80,0)
|
||||
,('Объектное хранилище S3 CEPH','Исходящий трафик','paas.s3.ceph.trf-m','ГБайт',1.50,0)
|
||||
,('Объектное хранилище S3 CEPH','Запросы GET, HEAD, OPTIONS','paas.s3.ceph.get-m','10тыс.шт.',0.35,0)
|
||||
,('Объектное хранилище S3 CEPH','Запросы PUT, POST, PATCH','paas.s3.ceph.put-m','10тыс.шт.',4.50,0)
|
||||
('Объектное хранилище S3 CEPH','Дисковое пространство: Объем хранения (HOT)','paas.s3.ceph.vol-m','ГБайт',1.80,0)
|
||||
,('Объектное хранилище S3 CEPH','Исходящий трафик (HOT)','paas.s3.ceph.trf-m','ГБайт',1.50,0)
|
||||
,('Объектное хранилище S3 CEPH','Запросы GET, HEAD, OPTIONS (HOT)','paas.s3.ceph.get-m','10тыс.шт.',0.35,0)
|
||||
,('Объектное хранилище S3 CEPH','Запросы PUT, POST, PATCH (HOT)','paas.s3.ceph.put-m','10тыс.шт.',4.50,0)
|
||||
|
||||
,('Объектное хранилище S3 CEPH','Дисковое пространство: Объем хранения (COLD)','paas.s3.cphc.vol-m','ГБайт',0.84*1.2,0)
|
||||
,('Объектное хранилище S3 CEPH','Исходящий трафик (COLD)','paas.s3.cphc.trf-m','ГБайт',1.25*1.2,0)
|
||||
,('Объектное хранилище S3 CEPH','Запросы GET, HEAD, OPTIONS (COLD)','paas.s3.cphc.get-m','10тыс.шт.',0.8*1.2,0)
|
||||
,('Объектное хранилище S3 CEPH','Запросы PUT, POST, PATCH (COLD)','paas.s3.cphc.put-m','10тыс.шт.',9.7*1.2,0)
|
||||
) as
|
||||
s (svc, component, code, unit, price, discount)
|
||||
order by code
|
||||
@@ -41,7 +47,7 @@
|
||||
from ngcloud_ru.capacity_resource
|
||||
join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id
|
||||
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
|
||||
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#tenant_wz_index#/>
|
||||
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#ATTRIBUTES.tenant_wz_index#/>
|
||||
AND capacity_resource.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_start#/>
|
||||
AND capacity_resource.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_finish#/>
|
||||
group by tenant.wzcode, vdc.name
|
||||
@@ -80,7 +86,7 @@
|
||||
join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id
|
||||
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
|
||||
join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id
|
||||
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#tenant_wz_index#/>
|
||||
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#ATTRIBUTES.tenant_wz_index#/>
|
||||
AND capacity_storage.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_start#/>
|
||||
AND capacity_storage.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_finish#/>
|
||||
group by tenant.wzcode, vdc.name, storage_profile_types.name
|
||||
@@ -118,7 +124,7 @@ HOT_FREE_LIMIT 1 10 1 100
|
||||
join s3billing.placement ON s3billing.placement.id=bucket_stat.placement_id
|
||||
join s3billing.bucket_info ON bucket_info.id=bucket_stat.bucket_id
|
||||
where bucket_stat.placement_id in (1,2)
|
||||
AND bucket_stat.owner = <cfqueryparam cfsqltype="cf_sql_varchar" value=#tenant_wz_index#/>
|
||||
AND bucket_stat.owner = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
|
||||
AND bucket_stat.timestamp_addition >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_start#/>
|
||||
AND bucket_stat.timestamp_addition < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_finish#/>
|
||||
group by bucket_stat.owner, bucket_stat.placement_id
|
||||
@@ -203,7 +209,7 @@ select
|
||||
join s3billing.bucket_info on usage_bucket_by_user.bucketid=bucket_info.id
|
||||
join s3billing.placement on bucket_info.placement_id=placement.id
|
||||
where 1=1
|
||||
AND user_info.name = <cfqueryparam cfsqltype="cf_sql_varchar" value=#tenant_wz_index#/>
|
||||
AND user_info.name = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
|
||||
AND usage_bucket_by_user.time >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_start#/>
|
||||
AND usage_bucket_by_user.time < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_finish#/>
|
||||
group by user_info.name, bucket_info.placement_id
|
||||
@@ -245,6 +251,7 @@ select wz, code || '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric,
|
||||
SELECT
|
||||
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
|
||||
<d:field title="Артикул">s.code</d:field>
|
||||
<d:field title="Артикул">m.code as m_code</d:field>
|
||||
<d:field title="Услуга">s.svc</d:field>
|
||||
<d:field title="Компонент">s.component</d:field>
|
||||
<d:field title="Кол-во (метрика)" cfSqlType="CF_SQL_NUMERIC">m.chargeable_metric</d:field>
|
||||
@@ -266,6 +273,11 @@ WHERE m.wz=<cfqueryparam cfsqltype="cf_sql_varchar" value=#request.auth.wz#/> AN
|
||||
order by s.code
|
||||
</cfquery>
|
||||
|
||||
<cfif (ATTRIBUTES.debug)>
|
||||
<cfdump var=#qCharge#/>
|
||||
<cfflush/>
|
||||
</cfif>
|
||||
|
||||
<cfset var report={}/>
|
||||
|
||||
<cfset report.qComputingAge = qComputingAge/>
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
<cfsilent>
|
||||
<cfimport prefix="m" taglib="lib"/>
|
||||
<cfimport prefix="c" taglib="lib/controls"/>
|
||||
<cfimport prefix="d" taglib="lib/data"/>
|
||||
<cfimport prefix="layout" taglib="layout"/>
|
||||
</cfsilent>
|
||||
|
||||
<m:prepare_ls entity="payg" accessObject="" pageInfoOut="pageInfo" trackOut="tr"/>
|
||||
|
||||
<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 --->
|
||||
<cfset tenant_wz_index="00169"/><!--- cut off WZ --->
|
||||
|
||||
|
||||
<cfif isDefined("output_xls")><cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/><layout:xml qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.xml"/>
|
||||
<cfabort/>
|
||||
</cfif><cfif isDefined("output_json")><cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/><!--- <cfdump var=#report#/><cfabort/> ---><layout:json qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.json"/>
|
||||
<cfabort/>
|
||||
</cfif><!---
|
||||
---><layout:page section="header" pageInfo=#pageInfo#>
|
||||
|
||||
<layout:attribute name="title">
|
||||
<cfoutput><b>Отчет по PAYG</b> #request.auth.wz#</cfoutput>
|
||||
</layout:attribute>
|
||||
<layout:attribute name="controls">
|
||||
<!---skip filter link, filter is not implemented--->
|
||||
<!---<layout:language_switch/>--->
|
||||
</layout:attribute>
|
||||
</layout:page>
|
||||
|
||||
<!--- Внимание! округление вверх при расчете метрик get, put S3. Расчетные суммы могут содержать погрешности округления, поэтому окончательный расчет стоимости (количество * цену) должен выполняться в бухгалтерской программе.
|
||||
<b>Если у клиента больше 1 действующего допника с услугами PAYG, то данные в отчете могут быть замножены, в этом случае считать вручную</b> --->
|
||||
|
||||
<cfoutput>
|
||||
<form method="post" action="">
|
||||
Период с <input type="text" name="dt_start" value="#dateFormat(dt_start,'YYYY-MM-DD')#"/>
|
||||
по <input type="text" name="dt_finish" value="#dateFormat(dt_finish,'YYYY-MM-DD')#"/>
|
||||
<input type="submit" style="cursor:pointer;"/>
|
||||
<!--- <input type="submit" name="DEBUG" value="DEBUG" style="cursor:pointer;"/> --->
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<cfflush/>
|
||||
|
||||
<cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/>
|
||||
|
||||
Часов в периоде: <b>#hours#</b> Строк в отчете: <b>#report.qCharge.recordCount#</b><br/>
|
||||
Актуальность данных:
|
||||
VCD Computing: <b>#dateFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qComputingAge.dt_load,'HH:MM:SS')#</b>
|
||||
VCD Storage: <b>#dateFormat(report.qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qStorageAge.dt_load,'HH:MM:SS')#</b>
|
||||
S3 хранение: <b>#dateFormat(report.qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3VolAge.dt_load,'HH:MM:SS')#</b>
|
||||
S3 операции и трафик: <b>#dateFormat(report.qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3OpsTrfAge.dt_load,'HH:MM:SS')#</b>
|
||||
|
||||
<br/>
|
||||
<a href="#request.thisPage#?output_xls" title="экспорт в Excel" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/xls.gif" style="vertical-align:text-bottom;"/></a>
|
||||
|
||||
<a href="#request.thisPage#?output_json" title="экспорт в json" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/json.svg" style="vertical-align:text-bottom;" width="13" height="13"/></a>
|
||||
</cfoutput>
|
||||
<table class="worktable">
|
||||
<thead>
|
||||
<td width="1%"></td>
|
||||
<!--- <th width="3%">WZ</th> --->
|
||||
<th width="7%">Артикул</th>
|
||||
<th width="7%">Кол-во (метрика)</th>
|
||||
<th width="3%">Ед.изм.</th>
|
||||
<th width="3%">Цена ₽ со скид., с НДС</th>
|
||||
<th width="3%">Стоимость ₽ с НДС</th>
|
||||
|
||||
<td width="3%"> </td>
|
||||
|
||||
<td width="10%">Услуга</td>
|
||||
<td width="10%">Компонент</td>
|
||||
<!--- <td width="10%">Клиентс. назв.</td> --->
|
||||
|
||||
<td width="3%">GPL с НДС</td>
|
||||
<td width="3%">Скидка%</td>
|
||||
|
||||
<!--- <td width="6%">Дата НОУ</td>
|
||||
<td width="6%">Дата оконч.</td>
|
||||
|
||||
<td width="3%">Ключ строки</td> --->
|
||||
<!--- <td width="3%">Сырая метрика</td> --->
|
||||
<td width="3%">Оплачиваемый объем</td>
|
||||
|
||||
<!--- <td width="5%">Договор</td>
|
||||
<td width="10%">Допник</td>
|
||||
<td width="10%">Сделка</td> --->
|
||||
|
||||
</thead>
|
||||
|
||||
<cfoutput query="report.qCharge" group="svc">
|
||||
<cfset var acc=0/>
|
||||
<tr>
|
||||
<td bgcolor="##ccc"></td>
|
||||
<td bgcolor="##ccc" colspan="5" class="b"> #svc#</td>
|
||||
<td></td>
|
||||
<td bgcolor="##ccc" colspan="99"></td>
|
||||
</tr>
|
||||
<cfoutput>
|
||||
<cfset acc=acc+charge/>
|
||||
<tr>
|
||||
<td>#currentRow#</a></td>
|
||||
|
||||
<td>#code#</td>
|
||||
<td class="r" style="font-size:120%; padding:0 1em;">#chargeable_metric#</td>
|
||||
<td class="c" style="font-size:90%;">#unit#</td>
|
||||
<td class="r">#discounted_price#</td>
|
||||
<td class="r">#charge#</td>
|
||||
|
||||
<td></td>
|
||||
|
||||
<td>#svc#</td>
|
||||
<td>#component#</td>
|
||||
<!--- <td>#user_description#</td> --->
|
||||
|
||||
<td class="r">#price#</td>
|
||||
<td class="r">#discount#</td>
|
||||
|
||||
<td class="r">#metric#</td>
|
||||
|
||||
</tr>
|
||||
</cfoutput>
|
||||
<tr>
|
||||
<td bgcolor="##eee" style="border-bottom:2px solid gray;"></td>
|
||||
<td bgcolor="##eee" colspan="4" class="r" style="border-bottom:2px solid gray;"><b>Итого</b> #svc#:</td>
|
||||
<td bgcolor="##eee" colspan="1" class="r b" style="border-bottom:2px solid gray;">#NumFmt(acc,2)#</td>
|
||||
<td colspan="14" style="border-bottom:2px solid gray;"></td>
|
||||
</tr >
|
||||
</cfoutput>
|
||||
|
||||
</table>
|
||||
<!--- <cfdump var=#qCharge#/> --->
|
||||
|
||||
|
||||
<layout:page section="footer"/>
|
||||
|
||||
<!--- select * from s3billing.billing_per_user_fix_pl('2025-05-01 00:00:00','2025-06-01 00:00:00','1395'); --->
|
||||
|
||||
Reference in New Issue
Block a user