refactor: bunch of improvements
All checks were successful
Build Test / test (pull_request) Successful in 35s
JS Unit Tests / test (pull_request) Successful in 33s
PHP Unit Tests / test (pull_request) Successful in 1m5s

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-04-23 22:04:36 -04:00
parent d0e8406830
commit acc42d09ee
8 changed files with 406 additions and 259 deletions

View File

@@ -1,33 +1,31 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
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>()
// 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 basicIdentity = ref('')
const basicSecret = ref('')
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'
required: (value: unknown) => !!value || 'This field is required'
}
// Validation
const isValid = computed(() => {
switch (authType.value) {
case 'BA':
@@ -35,105 +33,183 @@ const isValid = computed(() => {
case 'TA':
return !!bearerToken.value
case 'OA':
return oauthSuccess.value
return oauthSuccess.value && !!oauthAccessToken.value
default:
return false
}
})
// Build ServiceIdentity object
const currentIdentity = computed((): ServiceIdentity | null => {
if (!isValid.value) return null
if (!isValid.value) {
return null
}
switch (authType.value) {
case 'BA':
return {
type: 'BA',
identity: basicIdentity.value,
secret: basicSecret.value
secret: basicSecret.value,
}
case 'TA':
return {
type: 'TA',
token: bearerToken.value
token: bearerToken.value,
}
case 'OA':
return {
type: 'OA',
accessToken: oauthAccessToken.value,
refreshToken: oauthRefreshToken.value,
refreshToken: oauthRefreshToken.value || undefined,
accessScope: ['mail'],
accessExpiry: Date.now() + 3600000
accessExpiry: Math.floor(Date.now() / 1000) + 3600,
}
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)
() => props.service,
service => {
syncFromService(service)
},
{ 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) {
email => {
if (authType.value === 'BA' && email && !basicIdentity.value) {
basicIdentity.value = email
}
},
{ immediate: true }
)
// OAuth flow (stub for now)
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 {
// 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)
} catch (error) {
console.warn('[JMAP Auth Panel] OAuth implementation pending', error)
} finally {
oauthLoading.value = false
}