Initial commit
This commit is contained in:
290
src/components/JmapAuthPanel.vue
Normal file
290
src/components/JmapAuthPanel.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
|
||||
import type { ProviderAuthPanelProps, ProviderAuthPanelEmits } from '@KTXM/MailManager/types/integration'
|
||||
|
||||
const props = defineProps<ProviderAuthPanelProps>()
|
||||
const emit = defineEmits<ProviderAuthPanelEmits>()
|
||||
|
||||
// Auth method selection
|
||||
const authType = ref<'BA' | 'TA' | 'OA'>('BA')
|
||||
|
||||
// Basic auth state
|
||||
const basicIdentity = ref(props.prefilledIdentity || props.emailAddress || '')
|
||||
const basicSecret = ref(props.prefilledSecret || '')
|
||||
|
||||
// Token auth state
|
||||
const bearerToken = ref('')
|
||||
|
||||
// OAuth state
|
||||
const oauthLoading = ref(false)
|
||||
const oauthSuccess = ref(false)
|
||||
const oauthAccessToken = ref('')
|
||||
const oauthRefreshToken = ref('')
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value: any) => !!value || 'This field is required'
|
||||
}
|
||||
|
||||
// Validation
|
||||
const isValid = computed(() => {
|
||||
switch (authType.value) {
|
||||
case 'BA':
|
||||
return !!basicIdentity.value && !!basicSecret.value
|
||||
case 'TA':
|
||||
return !!bearerToken.value
|
||||
case 'OA':
|
||||
return oauthSuccess.value
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// Build ServiceIdentity object
|
||||
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,
|
||||
accessScope: ['mail'],
|
||||
accessExpiry: Date.now() + 3600000
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
// Watch and emit changes
|
||||
watch(
|
||||
currentIdentity,
|
||||
(identity) => {
|
||||
if (identity) {
|
||||
emit('update:modelValue', identity)
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
isValid,
|
||||
(valid) => {
|
||||
emit('valid', valid)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Update local state if modelValue changes externally
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
authType.value = newValue.type as 'BA' | 'TA' | 'OA'
|
||||
|
||||
switch (newValue.type) {
|
||||
case 'BA':
|
||||
basicIdentity.value = newValue.identity || ''
|
||||
basicSecret.value = newValue.secret || ''
|
||||
break
|
||||
case 'TA':
|
||||
bearerToken.value = newValue.token || ''
|
||||
break
|
||||
case 'OA':
|
||||
oauthAccessToken.value = newValue.accessToken || ''
|
||||
oauthRefreshToken.value = newValue.refreshToken || ''
|
||||
oauthSuccess.value = !!newValue.accessToken
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Prefill identity when email address is provided
|
||||
watch(
|
||||
() => props.emailAddress,
|
||||
(email) => {
|
||||
if (email && !basicIdentity.value) {
|
||||
basicIdentity.value = email
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// OAuth flow (stub for now)
|
||||
async function initiateOAuth() {
|
||||
oauthLoading.value = true
|
||||
try {
|
||||
// TODO: Implement OAuth flow when backend is ready
|
||||
emit('error', 'OAuth implementation pending')
|
||||
throw new Error('OAuth implementation pending')
|
||||
} catch (error: any) {
|
||||
emit('error', error.message)
|
||||
} finally {
|
||||
oauthLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user