evcc-io/assets/js/components/Top/AuthProviderModal.vue

205 lines
5 KiB
Vue

<template>
<GenericModal
id="authProviderModal"
ref="modal"
:title="modalTitle"
data-testid="auth-provider-modal"
@closed="handleClosed"
>
<div class="container mx-0 px-0">
<!-- Success message after authentication -->
<template v-if="showAuthenticationSuccess">
<p class="mb-4 text-success">
{{ $t("authProviders.success", { title: providerTitle }) }}<br />
{{ $t("authProviders.successCloseModal") }}
</p>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">
{{ $t("config.general.close") }}
</button>
</div>
</template>
<!-- Login flow -->
<template v-else-if="showAuthentication">
<p class="mb-4">
{{
$t("authProviders.modalDescriptionLogin", {
provider: providerTitle,
})
}}
</p>
<!-- Auth code display (device flow) -->
<div v-if="auth.code">
<hr class="my-4" />
<AuthCodeDisplay
id="authProviderCode"
:code="auth.code"
:expiry="auth.expiry"
/>
</div>
<!-- Error display -->
<p v-if="auth.error" class="text-danger mt-3">{{ auth.error }}</p>
<!-- Action buttons -->
<div
class="my-4 d-flex align-items-stretch justify-content-sm-between align-items-sm-baseline flex-column-reverse flex-sm-row gap-2"
>
<button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">
{{ $t("config.general.cancel") }}
</button>
<!-- Authentication buttons -->
<AuthConnectButton
:provider-url="auth.providerUrl ?? undefined"
:loading="auth.loading"
@prepare="prepareAuthentication"
@external-click="waitingForAuthentication = true"
/>
</div>
</template>
<!-- Logout flow -->
<template v-else>
<p class="mb-4">
{{
$t("authProviders.modalDescriptionLogout", {
provider: providerTitle,
})
}}
</p>
<!-- Error display -->
<p v-if="logoutError" class="text-danger mt-3">{{ logoutError }}</p>
<!-- Action buttons -->
<div
class="my-4 d-flex align-items-stretch justify-content-sm-between align-items-sm-baseline flex-column-reverse flex-sm-row gap-2"
>
<button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">
{{ $t("config.general.cancel") }}
</button>
<button
type="button"
class="btn btn-danger"
:disabled="logoutLoading"
@click="performLogout"
>
<span
v-if="logoutLoading"
class="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
{{ $t("authProviders.buttonDisconnect") }}
</button>
</div>
</template>
</div>
</GenericModal>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import GenericModal from "../Helper/GenericModal.vue";
import AuthCodeDisplay from "../Config/AuthCodeDisplay.vue";
import AuthConnectButton from "../Config/AuthConnectButton.vue";
import {
initialAuthState,
prepareAuthLogin,
performAuthLogout,
} from "../Config/utils/authProvider";
import type { Provider } from "./types";
export default defineComponent({
name: "AuthProviderModal",
components: {
GenericModal,
AuthCodeDisplay,
AuthConnectButton,
},
props: {
provider: {
type: Object as PropType<Provider | null>,
default: null,
},
},
data() {
return {
logoutLoading: false,
logoutError: null as string | null,
auth: initialAuthState(),
waitingForAuthentication: false,
};
},
computed: {
isAuthenticated(): boolean {
return this.provider?.authenticated || false;
},
showAuthentication(): boolean {
return !this.isAuthenticated;
},
showAuthenticationSuccess(): boolean {
return this.isAuthenticated && this.waitingForAuthentication;
},
modalTitle(): string {
return this.providerTitle;
},
providerTitle(): string {
return this.provider?.title || "Unknown";
},
providerId(): string {
return this.provider?.id || "";
},
},
watch: {
providerId(newId) {
if (newId) {
this.reset();
// auto-run the prepare step. no user input needed
this.prepareAuthentication();
}
},
},
methods: {
reset() {
this.auth = initialAuthState();
this.logoutLoading = false;
this.logoutError = null;
this.waitingForAuthentication = false;
},
handleClosed() {
this.reset();
},
async prepareAuthentication() {
if (!this.providerId || this.isAuthenticated) return;
await prepareAuthLogin(this.auth, this.providerId);
},
async performLogout() {
if (!this.providerId) return;
this.logoutLoading = true;
this.logoutError = null;
const result = await performAuthLogout(this.providerId);
if (result.success) {
(this.$refs["modal"] as any)?.close();
} else {
this.logoutError = result.error || this.$t("authProviders.logoutFailed");
}
this.logoutLoading = false;
},
},
});
</script>
<style scoped>
.container {
margin-left: calc(var(--bs-gutter-x) * -0.5);
margin-right: calc(var(--bs-gutter-x) * -0.5);
padding-right: 0;
}
</style>