367 lines
9.3 KiB
Vue
367 lines
9.3 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import {
|
|
IdentityBasic,
|
|
IdentityOAuth,
|
|
IdentityToken,
|
|
} from '@KTXM/MailManager/models/identity'
|
|
import type { ServiceObject } from '@KTXM/MailManager/models/service'
|
|
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
|
|
import type { ProviderAuthPanelProps, ProviderAuthPanelEmits } from '@KTXM/MailManager/types/integration'
|
|
import { JmapServiceObject } from '@/models/JmapServiceObject'
|
|
|
|
const props = defineProps<ProviderAuthPanelProps>()
|
|
const emit = defineEmits<ProviderAuthPanelEmits>()
|
|
|
|
const authType = ref<'BA' | 'TA' | 'OA'>('BA')
|
|
const basicIdentity = ref('')
|
|
const basicSecret = ref('')
|
|
const bearerToken = ref('')
|
|
const oauthLoading = ref(false)
|
|
const oauthSuccess = ref(false)
|
|
const oauthAccessToken = ref('')
|
|
const oauthRefreshToken = ref('')
|
|
|
|
const rules = {
|
|
required: (value: unknown) => !!value || 'This field is required'
|
|
}
|
|
|
|
const isValid = computed(() => {
|
|
switch (authType.value) {
|
|
case 'BA':
|
|
return !!basicIdentity.value && !!basicSecret.value
|
|
case 'TA':
|
|
return !!bearerToken.value
|
|
case 'OA':
|
|
return oauthSuccess.value && !!oauthAccessToken.value
|
|
default:
|
|
return false
|
|
}
|
|
})
|
|
|
|
const currentIdentity = computed((): ServiceIdentity | null => {
|
|
if (!isValid.value) {
|
|
return null
|
|
}
|
|
|
|
switch (authType.value) {
|
|
case 'BA':
|
|
return {
|
|
type: 'BA',
|
|
identity: basicIdentity.value,
|
|
secret: basicSecret.value,
|
|
}
|
|
case 'TA':
|
|
return {
|
|
type: 'TA',
|
|
token: bearerToken.value,
|
|
}
|
|
case 'OA':
|
|
return {
|
|
type: 'OA',
|
|
accessToken: oauthAccessToken.value,
|
|
refreshToken: oauthRefreshToken.value || undefined,
|
|
accessScope: ['mail'],
|
|
accessExpiry: Math.floor(Date.now() / 1000) + 3600,
|
|
}
|
|
default:
|
|
return null
|
|
}
|
|
})
|
|
|
|
watch(
|
|
() => props.service,
|
|
service => {
|
|
syncFromService(service)
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
watch(
|
|
() => props.emailAddress,
|
|
email => {
|
|
if (authType.value === 'BA' && email && !basicIdentity.value) {
|
|
basicIdentity.value = email
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
watch(
|
|
currentIdentity,
|
|
identity => {
|
|
const existingIdentity = props.service?.identity?.toJson() ?? null
|
|
if (sameIdentity(existingIdentity, identity)) {
|
|
return
|
|
}
|
|
|
|
const nextService = createServiceObject(props.service)
|
|
nextService.identity = createIdentityModel(identity)
|
|
emit('update:service', nextService)
|
|
},
|
|
{ immediate: true, deep: true }
|
|
)
|
|
|
|
function syncFromService(service?: ServiceObject) {
|
|
const identity = service?.identity?.toJson() ?? null
|
|
|
|
if (!identity) {
|
|
authType.value = 'BA'
|
|
basicIdentity.value = props.prefilledIdentity || props.emailAddress || ''
|
|
basicSecret.value = props.prefilledSecret || ''
|
|
bearerToken.value = ''
|
|
oauthAccessToken.value = ''
|
|
oauthRefreshToken.value = ''
|
|
oauthSuccess.value = false
|
|
return
|
|
}
|
|
|
|
authType.value = identity.type as 'BA' | 'TA' | 'OA'
|
|
|
|
switch (identity.type) {
|
|
case 'BA':
|
|
basicIdentity.value = identity.identity || props.prefilledIdentity || props.emailAddress || ''
|
|
basicSecret.value = identity.secret || props.prefilledSecret || ''
|
|
bearerToken.value = ''
|
|
oauthAccessToken.value = ''
|
|
oauthRefreshToken.value = ''
|
|
oauthSuccess.value = false
|
|
break
|
|
case 'TA':
|
|
basicIdentity.value = props.prefilledIdentity || props.emailAddress || ''
|
|
basicSecret.value = props.prefilledSecret || ''
|
|
bearerToken.value = identity.token || ''
|
|
oauthAccessToken.value = ''
|
|
oauthRefreshToken.value = ''
|
|
oauthSuccess.value = false
|
|
break
|
|
case 'OA':
|
|
basicIdentity.value = props.prefilledIdentity || props.emailAddress || ''
|
|
basicSecret.value = props.prefilledSecret || ''
|
|
bearerToken.value = ''
|
|
oauthAccessToken.value = identity.accessToken || ''
|
|
oauthRefreshToken.value = identity.refreshToken || ''
|
|
oauthSuccess.value = !!identity.accessToken
|
|
break
|
|
}
|
|
}
|
|
|
|
function createServiceObject(service?: ServiceObject): JmapServiceObject {
|
|
const nextService = new JmapServiceObject()
|
|
|
|
if (service) {
|
|
nextService.fromJson(service.toJson())
|
|
}
|
|
|
|
return nextService
|
|
}
|
|
|
|
function createIdentityModel(identity: ServiceIdentity | null) {
|
|
if (identity === null) {
|
|
return null
|
|
}
|
|
|
|
switch (identity.type) {
|
|
case 'BA':
|
|
return new IdentityBasic(identity.identity, identity.secret)
|
|
case 'TA':
|
|
return new IdentityToken(identity.token)
|
|
case 'OA':
|
|
return new IdentityOAuth(
|
|
identity.accessToken,
|
|
identity.accessScope,
|
|
identity.accessExpiry,
|
|
identity.refreshToken,
|
|
identity.refreshLocation
|
|
)
|
|
}
|
|
}
|
|
|
|
function sameIdentity(a: ServiceIdentity | null, b: ServiceIdentity | null): boolean {
|
|
if (a === null || b === null) {
|
|
return a === b
|
|
}
|
|
|
|
if (a.type !== b.type) {
|
|
return false
|
|
}
|
|
|
|
switch (a.type) {
|
|
case 'BA':
|
|
return b.type === 'BA'
|
|
&& a.identity === b.identity
|
|
&& a.secret === b.secret
|
|
case 'TA':
|
|
return b.type === 'TA'
|
|
&& a.token === b.token
|
|
case 'OA':
|
|
return b.type === 'OA'
|
|
&& a.accessToken === b.accessToken
|
|
&& a.refreshToken === b.refreshToken
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function initiateOAuth() {
|
|
oauthLoading.value = true
|
|
|
|
try {
|
|
throw new Error('OAuth implementation pending')
|
|
} catch (error) {
|
|
console.warn('[JMAP Auth Panel] OAuth implementation pending', error)
|
|
} finally {
|
|
oauthLoading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="jmap-auth-panel">
|
|
<h3 class="text-h6 mb-4">Authentication</h3>
|
|
<p class="text-body-2 mb-6">Choose your authentication method and enter your credentials.</p>
|
|
|
|
<v-alert type="info" variant="tonal" class="mb-4">
|
|
<template #prepend>
|
|
<v-icon>mdi-information</v-icon>
|
|
</template>
|
|
<div class="text-caption">
|
|
JMAP supports multiple authentication methods. Choose the one your server uses.
|
|
</div>
|
|
</v-alert>
|
|
|
|
<!-- Authentication Type Selection -->
|
|
<div class="mb-4">
|
|
<label class="text-subtitle-2 mb-2 d-block">Authentication Method</label>
|
|
<v-btn-toggle
|
|
v-model="authType"
|
|
color="primary"
|
|
variant="outlined"
|
|
mandatory
|
|
divided
|
|
class="mb-4"
|
|
>
|
|
<v-btn value="BA">
|
|
<v-icon start>mdi-account-key</v-icon>
|
|
Basic Auth
|
|
</v-btn>
|
|
<v-btn value="TA">
|
|
<v-icon start>mdi-key</v-icon>
|
|
Bearer Token
|
|
</v-btn>
|
|
<v-btn value="OA">
|
|
<v-icon start>mdi-shield-account</v-icon>
|
|
OAuth 2.0
|
|
</v-btn>
|
|
</v-btn-toggle>
|
|
</div>
|
|
|
|
<!-- Basic Authentication -->
|
|
<template v-if="authType === 'BA'">
|
|
<v-text-field
|
|
v-model="basicIdentity"
|
|
label="Username / Email"
|
|
hint="Your account username or email address"
|
|
persistent-hint
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-account"
|
|
class="mb-4"
|
|
autocomplete="username"
|
|
autocorrect="off"
|
|
autocapitalize="none"
|
|
:rules="[rules.required]"
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="basicSecret"
|
|
type="password"
|
|
label="Password"
|
|
hint="Your account password"
|
|
persistent-hint
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-lock"
|
|
class="mb-4"
|
|
autocomplete="current-password"
|
|
:rules="[rules.required]"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Bearer Token Authentication -->
|
|
<template v-else-if="authType === 'TA'">
|
|
<v-textarea
|
|
v-model="bearerToken"
|
|
label="Bearer Token"
|
|
hint="Enter your API token or bearer token"
|
|
persistent-hint
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-key"
|
|
class="mb-4"
|
|
rows="3"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="none"
|
|
:rules="[rules.required]"
|
|
/>
|
|
</template>
|
|
|
|
<!-- OAuth 2.0 -->
|
|
<template v-else-if="authType === 'OA'">
|
|
<v-alert type="warning" variant="tonal" class="mb-4">
|
|
<template #prepend>
|
|
<v-icon>mdi-alert</v-icon>
|
|
</template>
|
|
<div class="text-caption">
|
|
OAuth 2.0 implementation is pending. This will launch a browser window
|
|
for secure authentication with your JMAP provider.
|
|
</div>
|
|
</v-alert>
|
|
|
|
<v-btn
|
|
v-if="!oauthSuccess"
|
|
color="primary"
|
|
size="large"
|
|
block
|
|
@click="initiateOAuth"
|
|
:loading="oauthLoading"
|
|
:disabled="true"
|
|
>
|
|
<v-icon start>mdi-login</v-icon>
|
|
Authorize with OAuth 2.0
|
|
</v-btn>
|
|
|
|
<div v-else class="text-center py-4">
|
|
<v-icon color="success" size="64">mdi-check-circle</v-icon>
|
|
<p class="text-subtitle-1 mt-2">OAuth Authorized Successfully</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.jmap-auth-panel {
|
|
max-width: 800px;
|
|
}
|
|
|
|
.text-h6 {
|
|
font-size: 1.25rem;
|
|
font-weight: 500;
|
|
line-height: 2rem;
|
|
letter-spacing: 0.0125em;
|
|
}
|
|
|
|
.text-subtitle-2 {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
line-height: 1.375rem;
|
|
letter-spacing: 0.00714em;
|
|
}
|
|
|
|
.text-body-2 {
|
|
font-size: 0.875rem;
|
|
font-weight: 400;
|
|
line-height: 1.25rem;
|
|
letter-spacing: 0.0178571429em;
|
|
color: rgba(var(--v-theme-on-surface), 0.7);
|
|
}
|
|
</style>
|