"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateAuthenticationService = generateAuthenticationService;
function generateAuthenticationService(context) {
    return /* ts */ `
// We import Fastify cookie here to ensure that the cookie plugin and its types are registered.
import '@fastify/cookie'

import * as z from 'zod'
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'

import { FastifyRequest } from 'fastify'
import jwt from 'jsonwebtoken'
import { JwksClient } from 'jwks-rsa'
import { OidcClient } from 'oidc-client-ts'

import type { AuthenticationConfig_EnabledKeycloak } from './authentication.config'
import { authenticationCookieName, UserInfo, UserRoles, zUserInfo, zUserRoles } from './authentication.types'
import { unknownToErrorMessage } from './utils'

/**
 * State object for the authentication service.
 */
const zState = z.object({
  redirectTo: z.string().optional(),
})

/**
 * State object for the authentication service.
 */
export type State = z.infer<typeof zState>

@Injectable()
export class ${context.authentication.service.name} implements OnModuleInit {
  private readonly logger = new Logger(${context.authentication.service.name}.name)

  private readonly oidc: OidcClient

  private readonly jwks: JwksClient

  // collects subscriptions of users, with the key being the user's email and the value being the last check date
  // this is used to cache the subscription check for a certain amount of time
  private readonly subscriptions: Map<string, Date> = new Map()

  private readonly _cookieDomain: string
  public get cookieDomain(): string {
    return this._cookieDomain
  }

  constructor(public readonly config: AuthenticationConfig_EnabledKeycloak) {
    
    this.oidc = new OidcClient({
      authority: this.config.keycloakUrl.toString(),
      client_id: this.config.clientId,
      redirect_uri: this.config.redirectUrl.toString(),
      client_authentication: 'client_secret_basic',
      client_secret: this.config.clientSecret,
      response_type: 'code',
      // prettier-ignore
      metadataUrl: \`\${this.config.keycloakUrl.toString()}/realms/\${this.config.realm}/.well-known/openid-configuration\`,
    })

    this.jwks = new JwksClient({
      jwksUri: \`\${this.config.keycloakUrl.toString()}/realms/\${this.config.realm}/protocol/openid-connect/certs\`,
      timeout: 30_000,
    })

    // Setting  cookie domain from the URL by removing the subdomain unless the URL
    // is already a top-level domain.
    // Background: The cookie domain shall match the server domain without subdomain so
    //             that browser forwards the cookie to the server.
    const parts = this.config.url.hostname.split('.')

    if (parts.length > 2) {
      this._cookieDomain = parts.slice(1).join('.')
    } else {
     this._cookieDomain = this.config.url.hostname
    }  
  }

  public async onModuleInit() {
    // Clear Keycloak stale state to check if Keycloak is running
    try {
      await this.oidc.clearStaleState()
    } catch (error) {
      this.logger.error('Error clearing stale state', error)
    }
  }

  /**
   * Refreshes the access token via the refresh token.
   */
  public async refreshAccessToken(req: FastifyRequest): Promise<
    | {
        ok: true
        accessToken: string
        idToken: string
        refreshToken?: string
        state: State | null
      }
    | {
        ok: false
        error: unknown
      }
  > {
    const refreshToken = req.cookies[\`\${authenticationCookieName}-refresh\`]
    if (!refreshToken) {
      return { ok: false, error: new Error('No refresh token provided') }
    }
    const url = \`\${this.config.keycloakUrl.toString()}/realms/\${this.config.realm}/protocol/openid-connect/token\`
    const params = new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: this.config.clientId,
      client_secret: this.config.clientSecret,
    })
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: params.toString(),
      })

      if (!response.ok) {
        return { ok: false, error: new Error(\`Failed to refresh access token: \${response.statusText}\`) }
      }

      const data = (await response.json()) as {
        access_token: string
        id_token: string
        refresh_token?: string
        state?: string
      }

      return {
        ok: true,
        accessToken: data.access_token,
        idToken: data.id_token,
        refreshToken: data.refresh_token,
        state: null,
      }
    } catch (error) {
      return { ok: false, error: error }
    }
  }

  /**
   * Extracts the code from URL and exchanges it for an access token.
   */
  public async getAccessTokenFromURL(url: string): Promise<
    | {
        ok: true
        accessToken: string
        refreshToken?: string
        state: State | null
      }
    | {
        ok: false
        error: unknown
      }
  > {
    try {
      this.logger.log(\`Getting access token from URL \${url}\`)
      const res = await this.oidc.processSigninResponse(url)
      const state = this.decodeState(res.userState)

      return { ok: true, accessToken: res.access_token, refreshToken: res.refresh_token, state }
    } catch (error) {
      return { ok: false, error: error }
    }
  }

  /**
   * Returns the URL that the user may use to perform authentication.
   */
  public async getAuthenticationUrl({ redirectTo }: { redirectTo?: string }): Promise<string> {
    const request = await this.oidc.createSigninRequest({
      scope: 'email profile openid',
      state: this.encodeState({ redirectTo }),
    })

    return replaceDockerHost(request.url)
  }

  /**
   * Obtains the user associated with the request.
   */
  public async getUserInfoFromRequest(req: FastifyRequest): Promise<{ ok: true; userInfo: UserInfo } | { ok: false }> {
    const accessToken = this.getAccessTokenFromRequest(req)

    if (!accessToken) {
      return { ok: false }
    }

    const info = await this.getUserInfo(accessToken)

    return info
  }

  /**
   * Extracts access token from request headers
   */
  private getAccessTokenFromRequest(req: FastifyRequest): string | undefined {
    const bearer = req.cookies[authenticationCookieName] ?? req.headers.authorization

    if (!bearer) {
      return
    }

    return bearer.replace('Bearer', '').trim()
  }

  /**
   * Extracts the user from the API token.
   *
   * If the app configuration does not enforce authentication or is configured for E2E testing,
   * the system user will be returned, **even if the token is invalid!**.
   */
  public async getUserInfo(accessToken: string): Promise<{ ok: true; userInfo: UserInfo } | { ok: false }> {
    // use access token to retrieve user info if still valid
    const userInfo = this.accessTokenToUserInfo(accessToken)
    if (userInfo?.exp && userInfo.exp > new Date()) {
      return { ok: true, userInfo }
    }

    try {
      const endpoint = await this.oidc.metadataService.getUserInfoEndpoint()
      const response = await fetch(endpoint, {
        method: 'GET',
        headers: { Authorization: \`Bearer \${accessToken}\` },
      })

      const raw = await response.text()

      try {
        const parsed = await zUserInfo.safeParseAsync(JSON.parse(raw), {})
        if (parsed.success) {
          return { ok: true, userInfo: parsed.data }
        }

        this.logger.error(\`Failed to validate user info: \${JSON.stringify(parsed.error)}\`)

        return { ok: false }
      } catch (error) {
        this.logger.error(unknownToErrorMessage(error, 'Failed to parse user info'))
        return { ok: false }
      }
    } catch (error) {
      this.logger.error(unknownToErrorMessage(error, 'Failed to fetch user info'))
      return { ok: false }
    }
  }

  public async getUserRolesFromRequest(
    req: FastifyRequest,
  ): Promise<{ ok: true; userRoles: UserRoles } | { ok: false }> {
    const accessToken = req.cookies[authenticationCookieName] ?? req.headers.authorization

    if (!accessToken) {
      return { ok: false }
    }

    const roles = await this.getUserRoles(accessToken)

    return roles
  }

  public async getUserRoles(accessToken: string): Promise<{ ok: true; userRoles: UserRoles } | { ok: false }> {
    try {
      const getKey = (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => {
        this.jwks.getSigningKey(header.kid, (err, key) => {
          if (err || !key) {
            callback(err)
          } else {
            callback(null, key.getPublicKey())
          }
        })
      }

      const decoded = await _jwtVerify(accessToken, getKey, { algorithms: ['RS256'] })

      if (typeof decoded === 'object' && decoded !== null) {
        const rawUserRoles = decoded.realm_access?.roles || []

        const userRoles = zUserRoles.safeParse(rawUserRoles)
        if (userRoles.success) {
          return { ok: true, userRoles: userRoles.data }
        }

        return { ok: false }
      }

      return { ok: true, userRoles: [] }
    } catch (error) {
      console.error(error)

      return { ok: false }
    }
  }

  /**
   * Retrieves Keycloak logout url
   */
  public async getLogoutUrl(req: FastifyRequest): Promise<string> {
    // fetch id token by refreshing the access token
    const res = await this.refreshAccessToken(req)
    const signoutRequest = await this.oidc.createSignoutRequest({
      post_logout_redirect_uri: this.config.postLogoutRedirectUrl?.toString() ?? this.config.frontendUrl?.toString(),
      id_token_hint: res.ok ? res.idToken : undefined,
    })
    return signoutRequest.url
  }

  /**
   * Retrieves UserInfo from Access Token
   */
  private accessTokenToUserInfo(accessToken: string): UserInfo | undefined {
    const decodedToken = jwt.decode(accessToken, { complete: true })
    if (!decodedToken || typeof decodedToken !== 'object') {
      return undefined
    }
    const parsedToken = zUserInfo.safeParse(decodedToken.payload)
    if (!parsedToken.success) {
      return undefined
    }

    return parsedToken.data
  }

  private encodeState(state: State) {
    return Buffer.from(JSON.stringify(state)).toString('base64')
  }

  /**
   * Decodes the state string from the URL.
   */
  private decodeState(state?: unknown): State | null {
    if (!state) {
      return null
    }

    if (typeof state !== 'string') {
      return null
    }

    try {
      const raw = JSON.parse(Buffer.from(state, 'base64').toString()) as unknown

      return zState.parse(raw)
    } catch (_error) {
      return null
    }
  }
}

/**
 * Verifies a JWT token.
 */
function _jwtVerify(
  token: string,
  key: jwt.Secret | jwt.GetPublicKeyOrSecret,
  options?: jwt.VerifyOptions,
): Promise<string | jwt.JwtPayload | undefined> {
  return new Promise((resolve, reject) => {
    jwt.verify(token, key, options, (error, decoded) => {
      if (error) {
        reject(error)
      } else {
        resolve(decoded)
      }
    })
  })
}

/**
 * Replace Docker internal hostname with localhost for local development.
 *
 * SECURITY NOTE: These HTTP URLs are for local development only.
 * In production, use HTTPS by configuring the keycloakUrl in the authentication config
 * to use https:// protocol. The URLs here are only used for Docker Compose development.
 */
function replaceDockerHost(url: string): string {
  // Development-only HTTP URLs for Docker Compose
  // prettier-ignore
  if (url.startsWith('http://keycloak:8080')) { // NOSONAR - this is only for execution in test env
    return url.replace('http://keycloak:8080', 'http://localhost:3000/auth') // NOSONAR - this is only for execution in test env
  }
  return url
}`;
}
//# sourceMappingURL=authentication-service.generator.js.map