All files / src/stores openstack-credentials.store.ts

0% Statements 0/126
0% Branches 0/1
0% Functions 0/1
0% Lines 0/126

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156                                                                                                                                                                                                                                                                                                                       
import { defineStore } from 'pinia'
import axios from 'axios'
import { credentialsApi } from '@/api/credentials.api'
import type {
  OpenStackCredentialFromYaml,
  OpenStackCredentialResponse,
  OpenStackCredentialUpsert,
} from '@/types/openstack-credential'
 
interface State {
  status: OpenStackCredentialResponse | null
  loading: boolean
  error: string | null
}
 
const LOCKED_REASON = 'openstack_credentials_locked'
 
function extractError(err: unknown, fallback: string): string {
  if (axios.isAxiosError(err)) {
    const detail = err.response?.data?.detail
    if (typeof detail === 'string') return detail
    if (detail && typeof detail === 'object') {
      const reason = (detail as { reason?: string }).reason
      if (reason === LOCKED_REASON) {
        const n = (detail as { active_deployments?: number }).active_deployments ?? 0
        return `Credentials gesperrt — ${n} aktive(s) Deployment(s)`
      }
      if (reason) return String(reason)
    }
  }
  return fallback
}
 
function isLockedError(err: unknown): boolean {
  if (!axios.isAxiosError(err)) return false
  if (err.response?.status !== 409) return false
  const detail = err.response.data?.detail
  return !!(detail && typeof detail === 'object' && (detail as { reason?: string }).reason === LOCKED_REASON)
}
 
// Dedupe concurrent fetch() calls — DashboardView mount, auth store
// post-login, and route guards can all kick this off at the same time
// on a cold load. Without this, each trigger hits the backend.
let fetchPromise: Promise<OpenStackCredentialResponse | null> | null = null
 
export const useOpenStackCredentialsStore = defineStore('openstack-credentials', {
  state: (): State => ({
    status: null,
    loading: false,
    error: null,
  }),
 
  getters: {
    hasCredential: (s) => !!s.status?.has_credential,
    isValidated: (s) =>
      !!s.status?.last_validated_at && !s.status?.last_validation_error,
    lastError: (s) => s.status?.last_validation_error || null,
    isLocked: (s) => !!s.status?.is_locked,
    activeDeployments: (s) => s.status?.active_deployments ?? 0,
    // True once the GET /me/openstack-credentials has resolved at least
    // once. Banners and disabled states should gate on this to avoid the
    // "fehlen" message flashing during the initial fetch.
    isResolved: (s) => s.status !== null,
  },
 
  actions: {
    async fetch() {
      if (fetchPromise) return fetchPromise
      fetchPromise = (async () => {
        this.loading = true
        this.error = null
        try {
          const res = await credentialsApi.get()
          this.status = res.data
          return res.data
        } catch (err) {
          this.error = extractError(err, 'Failed to load OpenStack credentials')
          return null
        } finally {
          this.loading = false
          fetchPromise = null
        }
      })()
      return fetchPromise
    },
 
    async save(payload: OpenStackCredentialUpsert) {
      this.loading = true
      this.error = null
      try {
        const res = await credentialsApi.put(payload)
        this.status = res.data
        return res.data
      } catch (err) {
        this.error = extractError(err, 'Failed to save OpenStack credentials')
        if (isLockedError(err)) await this.fetch()
        throw err
      } finally {
        this.loading = false
      }
    },
 
    async saveFromYaml(body: OpenStackCredentialFromYaml) {
      this.loading = true
      this.error = null
      try {
        const res = await credentialsApi.putFromYaml(body)
        this.status = res.data
        return res.data
      } catch (err) {
        this.error = extractError(err, 'Failed to parse clouds.yaml')
        if (isLockedError(err)) await this.fetch()
        throw err
      } finally {
        this.loading = false
      }
    },
 
    async remove() {
      this.loading = true
      this.error = null
      try {
        await credentialsApi.remove()
        await this.fetch()
      } catch (err) {
        this.error = extractError(err, 'Failed to delete OpenStack credentials')
        if (isLockedError(err)) await this.fetch()
        throw err
      } finally {
        this.loading = false
      }
    },
 
    async test() {
      this.loading = true
      this.error = null
      try {
        const res = await credentialsApi.test()
        this.status = res.data
        return res.data
      } catch (err) {
        this.error = extractError(err, 'Failed to validate OpenStack credentials')
        throw err
      } finally {
        this.loading = false
      }
    },
 
    reset() {
      this.status = null
      this.error = null
      this.loading = false
    },
  },
})