nocin.eu

Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[BTP] How to use the refresh_token to get a new valid access_token

https://oauth.net/2/refresh-tokens

https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token

https://docs.cloudfoundry.org/api/uaa/version/4.6.0/index.html#refresh-token

# url from XSUAA Service Key
@xsuaaUrl = {{$dotenv xsuaaUrl}}
# clientid from XSUAA Service Key
@xsuaaClientId = {{$dotenv xsuaaClientId}}
# clientsecret from XSUAA Service Key
@xsuaaClientSecret = {{$dotenv xsuaaClientSecret}}

#==================================================================#

### Get Access Token for Cloud Foundry using Grant Type Password with BTP default IdP 
# @name token_response
POST {{xsuaaUrl}}/oauth/token
Authorization: Basic {{xsuaaClientId}}:{{xsuaaClientSecret}}
Accept: application/json;charset=utf8
Content-Type: application/x-www-form-urlencoded

grant_type=password
&username={{$dotenv btp_username}}
&password={{$dotenv btp_password}}
&response_type=token

### Store access token and refresh token
@access_token = {{token_response.response.body.$.access_token}}
@refresh_token = {{token_response.response.body.$.refresh_token}}


### Use Refresh Token
# @name token_response
POST {{xsuaaUrl}}/oauth/token
Authorization: Basic {{xsuaaClientId}}:{{xsuaaClientSecret}}
Accept: application/json;charset=utf8
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token={{refresh_token}}

### Store access token and refresh token
@access_token = {{token_response.response.body.$.access_token}}
@refresh_token = {{token_response.response.body.$.refresh_token}}

[SAPUI5] securityTokenAvailable

Just noticed, that with UI5 version 1.119.0 the getSecurityToken() function got replaced with securityTokenAvailable().

https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v2.ODataModel%23methods/getSecurityToken

https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v2.ODataModel%23methods/securityTokenAvailable

// Returns the current security token if available; triggers a request to fetch the security token if it is not available.
const token = this.getModel().getSecurityToken() // Deprecated

// Returns a promise, which will resolve with the security token as soon as it is available.
const token = await this.getModel().securityTokenAvailable()

[ABAP] Progress indicator

SELECT * FROM sflight INTO TABLE @DATA(flights).

LOOP AT flights INTO DATA(flight).
  WAIT UP TO 1 SECONDS.
  cl_progress_indicator=>progress_indicate( i_text               = |Processing flight { flight-connid } as { sy-tabix } / { lines( flights ) }|
                                            i_processed          = sy-tabix
                                            i_total              = lines( flights )
                                            i_output_immediately = abap_true ).
ENDLOOP.

[JavaScript] Regex to check if string only contains the newline escape sequence \n

This is the first time ChatGPT actually helped me to solve a problem. So far the answers have not been so helpful with coding problems, but it seems to work very well with regex. I asked it to create me a regex pattern that checks if a string contains only the newline escape sequence \n and the answer was correct.

const test1 = '\n' //true
const test2 = '\n\n\n\n' //true
const test3 = 'test \n test' //false
const test4 = 'abcdefghij' //false
const test5 = ' ' //false

const myRegex = /^(?:\n)+$/

console.log('test1: ' + myRegex.test(test1))
console.log('test2: ' + myRegex.test(test2))
console.log('test3: ' + myRegex.test(test3))
console.log('test4: ' + myRegex.test(test4))
console.log('test5: ' + myRegex.test(test5))

[nodejs] Extract first page of a PDF using pdf-lib

const { PDFDocument } = require('pdf-lib')

// file = { fileName: 'test1.pdf, content: arraybuffer }     

const originalPdf = await PDFDocument.load(file.content, { ignoreEncryption: true })
const newPdf = await PDFDocument.create()
const [firstPage] = await newPdf.copyPages(originalPdf, [0]) // <-- 0 is the first page
newPdf.addPage(firstPage)
const firstPagePdf = await newPdf.save()

file.content = Buffer.from(firstPagePdf)

[CAP] Multitenant Job Scheduler – Request timeout after 15 seconds

For Jobs running longer than 15 seconds, you have to manually inform the Job Scheduler if your operation succeeded or not. Else, your job will only stay in status COMPLETED/UNKNOWN due to the timeout.

Informing the Job Scheduler about your succeeded operation can be done vie REST API Endpoint Update Job Run Log. You can read more about Long-Running (Async) Jobs here. I therefore wrote a function named updateJobStatus, which I call at the end of every long-running endpoint. It checks if the endpoint is called manually or via Job Scheduler service and updates the Job Run Log using the @sap/jobs-client if required.

const cds = require('@sap/cds')
const LOG = cds.log('JobService')
const xsenv = require("@sap/xsenv")
const JobSchedulerClient = require("@sap/jobs-client")

async function fetchAccessToken(url, creds) {
    const response = await fetch(`${url}/oauth/token`, {
        method: 'POST',
        body: 'grant_type=client_credentials&client_id=' + creds.uaa.clientid + '&client_secret=' + creds.uaa.clientsecret,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    })
    return await response.json()
}

async function getJobscheduler(req) {
    xsenv.loadEnv()
    const services = xsenv.getServices({
        jobscheduler: { tags: "jobscheduler" }
    })
    if (!services.jobscheduler) req.reject("no jobscheduler service instance found")

    const subdomain = (process.env.NODE_ENV === 'production') ? req.http.req.authInfo.getSubdomain() : 'customer1' // workaround for local testing
    const domain = `https://${subdomain}.${services.jobscheduler.uaa.uaadomain}`
    const token = await fetchAccessToken(domain, services.jobscheduler)

    const options = {
        baseURL: services.jobscheduler.url,
        token: token.access_token
    }
    return new JobSchedulerClient.Scheduler(options)
}

async function updateJobStatus(req) {
    const jobId = req.headers['x-sap-job-id']
    const scheduleId = req.headers['x-sap-job-schedule-id']
    const runId = req.headers['x-sap-job-run-id']

    if (!jobId || !scheduleId || !runId) return
    LOG.info('Endpoint is called via Job Scheduler')

    const scheduler = await getJobscheduler(req)

    const payload = {
        jobId: jobId,
        scheduleId: scheduleId,
        runId: runId,
        data: { success: true, message: 'The endpoint has successfully executed the long-running job' }
    }

    scheduler.updateJobRunLog(payload, function (err, result) {
        if (err) return LOG.error('Error updating run log: %s', err)
        //Run log updated successfully
        LOG.info('Run log updated successfully')
    })
}

module.exports = {
  updateJobStatus
}

[CAP] Multitenant Job Scheduler – Fixing Scope issue

When I was integrating the Job Scheduler service into my Multitenant Application, I ran into the following JWT Token issue, when the Job Scheduler was calling my CAP action. Means the job creation was already working fine and was also displaying the right tenant for my job, but the Job Scheduler was not able to successfully call the given endpoint. This is the error I got in the logs:

Error: Jwt token with audience: [
'sb-a1e9d3b8-2bee-47db-xxxx-07e5a54aec1e!b180208|sap-jobscheduler!b3',
'uaa'
] is not issued for these clientIds: [
'sb-MyApp-mtdev-App!t180208',
'MyAp-mtdev-App!t180208'
].

After reading some of the great blogs from Carlos Roggan, I noticed that I forgot to grant the Job Scheduler the necessary authority to actual call my CAP action. So I added the following lines to the xs-security.json file

    {
      "name": "$XSAPPNAME.jobscheduler",
      "description": "Scope for Job Scheduler",
      "grant-as-authority-to-apps": [
        "$XSSERVICENAME(job-scheduler)"
      ]
    }

and also annotated my CAP action using the new scope @(requires: ['jobscheduler']).

I redeployed everything, but the issue still persists. 🙁

Turned out, for the standard plan, tokens are cached in Job Scheduler up to 12 hours.

https://help.sap.com/docs/job-scheduling/sap-job-scheduling-service/secure-access?locale=en-US

After waiting 12 hours, the endpoint was successfully called by the Job Scheduler. 🙂

[JavaScript] Check if iterator is undefined when using a for…of loop

Just saw this trick, how you can do a for…of loop which also checks if the iterator is null or undefined. Normally, you would check this by another if statement before starting the for..of loop, like here

const d = undefined

if (d) { 
  for (const x of d ) {
  	console.log(x)
  }
} 

or by using a try...catch

try {
  for (const x of d) {
    console.log(value)
  }
} catch (e) {
  console.error(e) // TypeError: undefined is not iterable
}

But instead of if or try...catch, you could use d || [], to check if d is Falsy, and if it’s false, no iterations are performed and no errors are thrown. The disadvantage of this approach is that you create an unneeded array and the readability may be poor depending on the situation.

for (const x of d || []) {
	console.log(x)
}

Of course, the first and the last snippet can also be done in one line

if (d) for (const x of d ) console.log(x) 

for (const x of d || []) console.log(x)

[CAP] Timeout on long-running endpoint

In my application, I have a function that can take quite a long time to process, depending on the data selected. Two external systems were involved in the processing, so a lot of round trips were made. Of course, I tried to parallelize the calls to the external systems as much as possible, but it still took a long time. During the development in BAS everything worked fine, but during the deployment in BTP I encountered some errors, depending on the amount of data selected.

In the console I could see, that it was a 504 Gateway Timeout.

Luckily, the CAP docs are already explaining the possible reason for this. The approuter has a default timeout of 30 seconds for destinations. This matched my observation, that this issue only occurred when deployed.

https://cap.cloud.sap/docs/get-started/troubleshooting#why-are-long-running-requests-rejected-with-status-504-after-30-seconds-even-though-the-application-continues-processing-the-request

https://www.npmjs.com/package/@sap/approuter#destinations

In my case, the destination for my backend service is configured in the mta.yaml directly on the approuter. By simply adding the timeout property and by increasing the timeout from 30 seconds to 2 minutes, I could get rid of the errors.

  - name: my-approuter
    type: approuter.nodejs
    path: app/approute
    build-parameters:
      builder: npm-ci
      ignore:
        - "node_modules/"
        - "default-env.json"
        - "manifest*.yml"
    requires:
      - name: srv-api
        group: destinations
        properties:
          name: srv-api 
          url: ~{srv-url}
          forwardAuthToken: true
          timeout: 120000 # <--------------------------------- add timeout to your cap service destination
      - name: my-xsuaa
      - name: my-destination
      - name: my-html5-repo-runtime