33const crypto = require ( "crypto" ) ;
44const { URL , URLSearchParams } = require ( "url" ) ;
55const axios = require ( "axios" ) ;
6- const axiosApi = axios . create ( {
7- timeout : 10000 ,
8- } ) ;
9- axios . defaults . headers . post [ "Content-Type" ] = "application/json" ;
6+ const tough = require ( "tough-cookie" ) ;
7+ const CookieJar = tough . CookieJar ;
8+ const { wrapper } = require ( "axios-cookiejar-support" ) ;
9+ const jar = new CookieJar ( ) ;
10+ const api_client = wrapper ( axios . create ( { withCredentials : true , timeout : 10000 } ) ) ;
11+ api_client . defaults . headers . post [ "Content-Type" ] = "application/json" ;
1012const https = require ( "https" ) ;
1113// rejectUnauthorized needs to be false due to the local machine's certificate cannot be checked properly
1214const agent = new https . Agent ( {
@@ -24,13 +26,16 @@ const LAST_UPDATED = "last updated";
2426// API Endpoints
2527const HOST_SYSTEMS = "https://senec-app-systems-proxy.prod.senec.dev" ;
2628const HOST_MEASUREMENTS = "https://senec-app-measurements-proxy.prod.senec.dev" ;
29+ const SSO_BASE_URL = "https://sso.senec.com/realms/senec/protocol/openid-connect" ;
30+ const SSO_AUTH_URL = `${ SSO_BASE_URL } /auth` ;
31+ const SSO_TOKEN_URL = `${ SSO_BASE_URL } /token` ;
2732
2833const CONFIG = {
29- authUrl : "https://sso.senec.com/realms/senec/protocol/openid-connect/auth" ,
30- tokenUrl : "https://sso.senec.com/realms/senec/protocol/openid-connect/token" ,
34+ authUrl : SSO_AUTH_URL ,
35+ tokenUrl : SSO_TOKEN_URL ,
3136 clientId : "endcustomer-app-frontend" ,
3237 redirectUri : "senec-app-auth://keycloak.prod" ,
33- scope : "roles meinsenec openid " ,
38+ scope : "roles profile meinsenec " ,
3439} ;
3540
3641const apiKnownSystems = new Set ( ) ;
@@ -473,7 +478,7 @@ class Senec extends utils.Adapter {
473478 const codeVerifier = generateCodeVerifier ( ) ;
474479 const codeChallenge = generateCodeChallenge ( codeVerifier ) ;
475480
476- const pageRes = await axiosApi . get (
481+ let pageRes = await api_client . get (
477482 `${ CONFIG . authUrl } ?${ new URLSearchParams ( {
478483 response_type : "code" ,
479484 client_id : CONFIG . clientId ,
@@ -482,26 +487,63 @@ class Senec extends utils.Adapter {
482487 code_challenge : codeChallenge ,
483488 code_challenge_method : "S256" ,
484489 } ) . toString ( ) } `,
490+ { jar } , // attach cookie jar
485491 ) ;
486492
487- const actionUrl = extractFormAction ( pageRes . data ) ;
493+ let actionUrl = extractFormAction ( pageRes . data ) ;
488494 if ( ! actionUrl ) {
489495 throw new Error ( "Login-Formular URL nicht gefunden." ) ;
490496 }
491497
492- const formData = new URLSearchParams ( ) ;
493- formData . append ( "username" , this . config . api_mail ) ;
494- formData . append ( "password" , this . config . api_pwd ) ;
495- formData . append ( "credentialId" , "" ) ;
498+ let loginRes ;
499+ if ( hasUsernameAndPassword ( pageRes . data ) ) {
500+ // worked until 20260228
501+ const formData = new URLSearchParams ( ) ;
502+ formData . append ( "username" , this . config . api_mail ) ;
503+ formData . append ( "password" , this . config . api_pwd ) ;
504+ formData . append ( "credentialId" , "" ) ;
505+
506+ loginRes = await api_client . post ( actionUrl , formData , {
507+ headers : {
508+ "Content-Type" : "application/x-www-form-urlencoded" ,
509+ } ,
510+ maxRedirects : 0 ,
511+ validateStatus : ( s ) => s >= 200 && s < 400 ,
512+ jar,
513+ } ) ;
514+ } else {
515+ if ( ! hasUsername ( pageRes . data ) ) {
516+ throw new Error ( "Expected: Login-Form with username. Got something else." ) ;
517+ }
518+ let formData = new URLSearchParams ( ) ;
519+ formData . append ( "credentialId" , "" ) ;
520+ formData . append ( "username" , this . config . api_mail ) ;
521+ loginRes = await api_client . post ( actionUrl , formData , {
522+ headers : {
523+ "Content-Type" : "application/x-www-form-urlencoded" ,
524+ } ,
525+ maxRedirects : 0 ,
526+ validateStatus : ( s ) => s >= 200 && s < 400 ,
527+ jar,
528+ } ) ;
496529
497- const loginRes = await axiosApi . post ( actionUrl , formData , {
498- headers : {
499- "Content-Type" : "application/x-www-form-urlencoded" ,
500- Cookie : formatCookies ( pageRes . headers ) ,
501- } ,
502- maxRedirects : 0 ,
503- validateStatus : ( s ) => s >= 200 && s < 400 ,
504- } ) ;
530+ if ( ! hasPassword ( loginRes . data ) ) {
531+ throw new Error ( "Expected: Login-Form with password. Got something else." ) ;
532+ }
533+ actionUrl = extractFormAction ( loginRes . data ) ;
534+ formData = new URLSearchParams ( ) ;
535+ //formData.append("credentialId", "");
536+ formData . append ( "username" , this . config . api_mail ) ;
537+ formData . append ( "password" , this . config . api_pwd ) ;
538+ loginRes = await api_client . post ( actionUrl , formData , {
539+ headers : {
540+ "Content-Type" : "application/x-www-form-urlencoded" ,
541+ } ,
542+ maxRedirects : 0 ,
543+ validateStatus : ( s ) => s >= 200 && s < 400 ,
544+ jar,
545+ } ) ;
546+ }
505547
506548 const redirectLocation = loginRes . headers [ "location" ] ;
507549 if ( ! redirectLocation ) {
@@ -518,7 +560,7 @@ class Senec extends utils.Adapter {
518560 throw new Error ( "Authorization code not found in redirect." ) ;
519561 }
520562
521- const tokenRes = await axiosApi . post (
563+ const tokenRes = await api_client . post (
522564 CONFIG . tokenUrl ,
523565 new URLSearchParams ( {
524566 grant_type : "authorization_code" ,
@@ -570,7 +612,7 @@ class Senec extends utils.Adapter {
570612 for ( const anlagenId of apiKnownSystems ) {
571613 this . log . info ( `🔄 Polling API data for system ${ anlagenId } ...` ) ;
572614 // get Dashboard data
573- const dashRes = await axiosApi . get ( `${ HOST_MEASUREMENTS } /v1/systems/${ anlagenId } /dashboard` , {
615+ const dashRes = await api_client . get ( `${ HOST_MEASUREMENTS } /v1/systems/${ anlagenId } /dashboard` , {
574616 headers : { Authorization : `Bearer ${ token } ` } ,
575617 } ) ;
576618 this . log . debug ( `DashRes${ JSON . stringify ( dashRes . data ) } ` ) ;
@@ -653,7 +695,7 @@ class Senec extends utils.Adapter {
653695 async pollSystems ( token ) {
654696 this . log . debug ( "🔄 Reading available systems from API ..." ) ;
655697 // get Systems
656- const sysRes = await axiosApi . get ( `${ HOST_SYSTEMS } /v1/systems` , {
698+ const sysRes = await api_client . get ( `${ HOST_SYSTEMS } /v1/systems` , {
657699 headers : { Authorization : `Bearer ${ token } ` } ,
658700 } ) ;
659701 if ( ! sysRes . data || ! sysRes . data [ 0 ] ) {
@@ -743,7 +785,7 @@ class Senec extends utils.Adapter {
743785 }
744786 const url = `${ HOST_MEASUREMENTS } /v1/systems/${ anlagenId } /measurements?resolution=${ resolution } &from=${ start } &to=${ end } ` ;
745787 this . log . debug ( `🔄 Polling measurements for ${ url } ` ) ;
746- const measurements = await axiosApi . get ( url , {
788+ const measurements = await api_client . get ( url , {
747789 headers : { Authorization : `Bearer ${ token } ` } ,
748790 } ) ;
749791 if ( ! measurements . data . timeSeries || measurements . data . timeSeries . length === 0 ) {
@@ -791,7 +833,7 @@ class Senec extends utils.Adapter {
791833 }
792834 const url = `${ HOST_MEASUREMENTS } /v1/systems/${ anlagenId } /measurements?resolution=${ resolution } &from=${ start } &to=${ end } ` ;
793835 this . log . debug ( `🔄 Polling measurements for ${ url } ` ) ;
794- const measurements = await axiosApi . get ( url , {
836+ const measurements = await api_client . get ( url , {
795837 headers : { Authorization : `Bearer ${ token } ` } ,
796838 } ) ;
797839 await this . doSumMeasurements ( measurements . data , anlagenId , pfx , period ) ;
@@ -835,7 +877,7 @@ class Senec extends utils.Adapter {
835877 }
836878 const url = `${ HOST_MEASUREMENTS } /v1/systems/${ anlagenId } /measurements?resolution=${ resolution } &from=${ start } &to=${ end } ` ;
837879 this . log . debug ( `🔄 Polling measurements for ${ url } ` ) ;
838- const measurements = await axiosApi . get ( url , {
880+ const measurements = await api_client . get ( url , {
839881 headers : { Authorization : `Bearer ${ token } ` } ,
840882 } ) ;
841883 await this . doSumMeasurements ( measurements . data , anlagenId , pfx , period ) ;
@@ -1415,15 +1457,21 @@ function base64UrlEncode(buffer) {
14151457 return buffer . toString ( "base64" ) . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = / g, "" ) ;
14161458}
14171459
1418- function formatCookies ( headers ) {
1419- const setCookie = headers [ "set-cookie" ] ;
1420- if ( ! setCookie ) {
1421- return "" ;
1422- }
1423- return Array . isArray ( setCookie ) ? setCookie . map ( ( c ) => c . split ( ";" ) [ 0 ] ) . join ( "; " ) : setCookie . split ( ";" ) [ 0 ] ;
1424- }
1425-
14261460function extractFormAction ( html ) {
14271461 const match = html . match ( / < f o r m [ ^ > ] * a c t i o n = " ( [ ^ " ] + ) " [ ^ > ] * > / i) ;
14281462 return match && match [ 1 ] ? match [ 1 ] . replace ( / & a m p ; / g, "&" ) : null ;
14291463}
1464+
1465+ function hasUsername ( html ) {
1466+ return html . match ( / < i n p u t \b (? ! [ ^ > ] * \b v a l u e \s * = ) [ ^ > ] * \b (?: n a m e | i d ) \s * = \s * [ " ' ] ? (?: u s e r n a m e | u s e r | e m a i l ) [ " ' ] ? [ ^ > ] * > / i) ;
1467+ }
1468+
1469+ function hasPassword ( html ) {
1470+ return html . match (
1471+ / < i n p u t \b (? = [ ^ > ] * \b t y p e \s * = \s * [ " ' ] ? p a s s w o r d [ " ' ] ? ) (? = [ ^ > ] * \b (?: n a m e | i d ) \s * = \s * [ " ' ] ? p a s s w o r d [ " ' ] ? ) [ ^ > ] * > / i,
1472+ ) ;
1473+ }
1474+
1475+ function hasUsernameAndPassword ( html ) {
1476+ return hasUsername ( html ) && hasPassword ( html ) ;
1477+ }
0 commit comments