<!--
Copyright (c) 2026 Sidero Labs, Inc.

Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<route lang="json">
{
  "meta": {
    "guard": "auth0"
  }
}
</route>

<script setup lang="ts">
import type { User } from '@auth0/auth0-spa-js'
import type { Auth0VueClient } from '@auth0/auth0-vue'
import { useAuth0 } from '@auth0/auth0-vue'
import { jwtDecode } from 'jwt-decode'
import { computed, nextTick, onBeforeMount, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'

import { b64Encode, type fetchOption, RequestError } from '@/api/fetch.pb'
import { Code } from '@/api/google/rpc/code.pb'
import { AuthService } from '@/api/omni/auth/auth.pb'
import { withMetadata } from '@/api/options'
import {
  authBearerHeaderPrefix,
  AuthFlowQueryParam,
  authHeader,
  authPublicKeyIDQueryParam,
  CLIAuthFlow,
  FrontendAuthFlow,
  RedirectQueryParam,
  samlSessionHeader,
  SignedRedirect,
  WorkloadProxyAuthFlow,
  workloadProxyPublicKeyIdSignatureBase64Cookie,
} from '@/api/resources'
import TButton from '@/components/Button/TButton.vue'
import TIcon from '@/components/Icon/TIcon.vue'
import PageContainer from '@/components/PageContainer/PageContainer.vue'
import TSpinner from '@/components/Spinner/TSpinner.vue'
import UserInfo from '@/components/UserInfo/UserInfo.vue'
import { AuthType, authType } from '@/methods'
import { useLogout } from '@/methods/auth'
import { useIdentity } from '@/methods/identity'
import { createKeys, signDetached, useKeys } from '@/methods/key'
import { showError } from '@/notification'

definePage({
  name: 'Authenticate',
})

const user = ref<User | undefined>(undefined)
let idToken = ''

/** @description Redirect the user's top-most window to the given URL.
 *
 * This makes sure that the redirect works correctly when the call comes from inside an iframe.
 *
 * @param url The URL to redirect to.
 */
const redirectToURL = (url: string) => {
  if (window.top) {
    window.top.location.href = url
  } else {
    window.location.href = url
  }
}

let auth0: Auth0VueClient | undefined

onBeforeMount(async () => {
  switch (authType.value) {
    case AuthType.Auth0:
      auth0 = useAuth0()

      user.value = auth0.user.value
      idToken = auth0.idTokenClaims.value!.__raw

      break
    case AuthType.OIDC:
    case AuthType.SAML:
      const navigateToLogin = () => {
        redirectToURL(`/login${window.location.search}`)
      }

      if (!identity.value) {
        navigateToLogin()
      }
  }
})

const identity = computed(() => {
  switch (authType.value) {
    case AuthType.Auth0:
      return user.value?.email
    case AuthType.SAML:
      return route.query.identity as string
    case AuthType.OIDC:
      return tokenData.value?.email
  }

  return undefined
})

const route = useRoute()

const tokenData = ref<{
  email?: string
  picture?: string
  name?: string
}>(route.query.token ? jwtDecode(route.query.token as string) : {})

const name = computed(() => {
  switch (authType.value) {
    case AuthType.Auth0:
      return user.value?.name
    case AuthType.OIDC:
      return tokenData.value.name
    case AuthType.SAML:
      return (route.query.fullname ?? route.query.identity) as string
  }

  return ''
})

const picture = computed(() => {
  switch (authType.value) {
    case AuthType.Auth0:
      return user?.value?.picture
    case AuthType.OIDC:
      return tokenData.value.picture
  }

  return undefined
})

const router = useRouter()

const publicKeyId = computed(() => route.query[authPublicKeyIDQueryParam]?.toString())

const confirmed = ref(false)

const logout = useLogout()
const keys = useKeys()
const identityStorage = useIdentity()
const keysGenerating = ref(false)
const keysAutoGenerated = ref(false)

const generatePublicKey = async (identity: string) => {
  if (keysGenerating.value) return

  keysAutoGenerated.value = true
  keysGenerating.value = true

  let res: Awaited<ReturnType<typeof createKeys>>

  try {
    res = await createKeys(identity)
  } catch (e) {
    showError('Failed to generate keys', e instanceof Error ? e.message : String(e))
    keysGenerating.value = false
    return
  }

  await confirmPublicKey(res.publicKeyId, res.keyPair)

  if (!confirmed.value) {
    keysGenerating.value = false
    return
  }

  keys.keyPair.value = res.keyPair
  keys.keyExpirationTime.value = res.keyExpirationTime
  keys.publicKeyID.value = res.publicKeyId

  identityStorage.identity.value = identity.toLowerCase()
  identityStorage.fullname.value = name.value ?? ''
  identityStorage.avatar.value = picture.value ?? ''

  // Wait for storages to be set
  await nextTick()

  const redirect = route.query[RedirectQueryParam]?.toString()

  if (!redirect) {
    return
  }

  if (redirect.indexOf(SignedRedirect) === 0) {
    redirectToURL(`/exposed/service?${RedirectQueryParam}=${encodeURIComponent(redirect)}`)

    return
  }

  await router.replace({ path: !redirect.startsWith('/') ? '/' : redirect })
}

const confirmPublicKey = async (publicKeyId: string, keyPair?: CryptoKeyPair) => {
  try {
    const options: fetchOption[] = []

    const metadata: Record<string, string> = {}

    if (keyPair) {
      const array = new Uint8Array(await signDetached(publicKeyId, keyPair))
      const publicKeyIdSignatureBase64 = b64Encode(array, 0, array.length)

      metadata[workloadProxyPublicKeyIdSignatureBase64Cookie] = publicKeyIdSignatureBase64
    }

    if (authType.value === AuthType.Auth0) {
      metadata[authHeader] = authBearerHeaderPrefix + idToken
    } else if (authType.value === AuthType.OIDC) {
      metadata[authHeader] = authBearerHeaderPrefix + route.query.token
    } else if (authType.value === AuthType.SAML) {
      if (!route.query.session) {
        throw new Error('no session')
      }

      metadata[samlSessionHeader] = route.query.session as string
    }

    options.push(withMetadata(metadata))

    await AuthService.ConfirmPublicKey(
      {
        public_key_id: publicKeyId,
      },
      ...options,
    )

    confirmed.value = true
  } catch (e) {
    if (e instanceof RequestError && e.code === Code.UNAUTHENTICATED && auth0) {
      // Force re-authentication
      return await auth0.loginWithRedirect({ authorizationParams: { max_age: 0 } })
    }

    showError('Failed to confirm public key', e.message)
  }
}

const Auth = {
  CLI: CLIAuthFlow,
  Frontend: FrontendAuthFlow,
  WorkloadProxy: WorkloadProxyAuthFlow,
} as const

const authFlow = computed(
  () => route.query[AuthFlowQueryParam] as (typeof Auth)[keyof typeof Auth] | undefined,
)

// For the frontend flow, we automatically generate keys for the user
// Will only attempt this once, however, then fallback to the form
watch(
  [authFlow, identity],
  () => {
    if (
      authFlow.value === Auth.Frontend &&
      identity.value &&
      !keysAutoGenerated.value &&
      !keysGenerating.value
    ) {
      generatePublicKey(identity.value)
    }
  },
  { immediate: true },
)
</script>

<template>
  <PageContainer class="flex h-full items-center justify-center">
    <div class="flex flex-col gap-2 rounded-md bg-naturals-n3 px-8 py-8 drop-shadow-md">
      <div class="flex items-center gap-4">
        <TIcon icon="key" class="fill-color h-6 w-6" />
        <div class="text-xl font-bold text-naturals-n13">
          <div v-if="authFlow === Auth.CLI">Authenticate CLI Access</div>
          <div v-else-if="authFlow === Auth.Frontend">Authenticate UI Access</div>
          <div v-else-if="authFlow === Auth.WorkloadProxy">Authenticate Workload Proxy Access</div>
        </div>
      </div>

      <div v-if="!publicKeyId && authFlow === Auth.CLI" class="mx-12">
        Public key ID parameter is missing...
      </div>
      <div v-else-if="!identity">Redirecting to the authentication provider...</div>
      <div v-else-if="confirmed" id="confirmed">
        Successfully logged in as {{ identity }}, you can return to the application...
      </div>
      <div v-else class="flex w-full flex-col gap-4">
        <div>The keys are going to be issued for the user:</div>
        <UserInfo
          user="user"
          class="rounded-md bg-naturals-n6 px-6 py-2"
          :email="identity"
          :avatar="picture"
          :fullname="name"
        />
        <div class="flex w-full flex-col gap-3">
          <TButton variant="secondary" class="w-full" @click="logout">Switch User</TButton>
          <TButton
            v-if="authFlow === Auth.CLI"
            id="confirm"
            class="w-full"
            variant="highlighted"
            @click="confirmPublicKey(publicKeyId!)"
          >
            Grant Access
          </TButton>
          <TButton
            v-else
            id="login"
            class="w-full"
            variant="highlighted"
            :disabled="keysGenerating"
            @click="generatePublicKey(identity)"
          >
            <TSpinner v-if="keysGenerating" class="size-4" />
            <template v-else>Log In</template>
          </TButton>
        </div>
      </div>
    </div>
  </PageContainer>
</template>
