Files
authentication_provider_pas…/src/components/AdminSecurityPanel.vue
T
2026-02-10 20:04:26 -05:00

268 lines
7.1 KiB
Vue

<script setup lang="ts">
import { ref, onMounted } from 'vue';
interface User {
uid: string;
identity: string;
label: string;
provider?: string;
}
const props = defineProps<{
user: User;
}>();
const emit = defineEmits<{
update: [];
}>();
const loading = ref(false);
const statusLoading = ref(false);
const error = ref<string | null>(null);
const success = ref<string | null>(null);
const hasPassword = ref(false);
const passwordDialog = ref(false);
const removeDialog = ref(false);
const newPassword = ref('');
const confirmPassword = ref('');
const loadStatus = async () => {
statusLoading.value = true;
try {
const response = await fetch(`/m/authentication_provider_password/admin/status/${props.user.uid}`, {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
hasPassword.value = data.enrolled ?? false;
}
} catch (err) {
console.error('Failed to load password status:', err);
} finally {
statusLoading.value = false;
}
};
const resetPassword = async () => {
if (newPassword.value !== confirmPassword.value) {
error.value = 'Passwords do not match';
return;
}
if (newPassword.value.length < 8) {
error.value = 'Password must be at least 8 characters';
return;
}
loading.value = true;
error.value = null;
try {
const response = await fetch('/m/authentication_provider_password/admin/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
uid: props.user.uid,
password: newPassword.value,
}),
});
if (response.ok) {
success.value = hasPassword.value ? 'Password reset successfully' : 'Password set successfully';
passwordDialog.value = false;
newPassword.value = '';
confirmPassword.value = '';
await loadStatus();
emit('update');
} else {
const data = await response.json();
error.value = data.error || 'Failed to reset password';
}
} catch (err: any) {
error.value = err.message || 'Failed to reset password';
} finally {
loading.value = false;
}
};
const removePassword = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(`/m/authentication_provider_password/admin/remove/${props.user.uid}`, {
method: 'DELETE',
credentials: 'include',
});
if (response.ok) {
success.value = 'Password removed successfully';
removeDialog.value = false;
await loadStatus();
emit('update');
} else {
const data = await response.json();
error.value = data.error || 'Failed to remove password';
}
} catch (err: any) {
error.value = err.message || 'Failed to remove password';
} finally {
loading.value = false;
}
};
onMounted(() => {
loadStatus();
});
</script>
<template>
<VCard variant="outlined">
<VCardTitle class="d-flex align-center">
<VIcon icon="mdi-lock" class="mr-2" />
Password
</VCardTitle>
<VCardText>
<VAlert
v-if="error"
type="error"
closable
class="mb-4"
@click:close="error = null"
>
{{ error }}
</VAlert>
<VAlert
v-if="success"
type="success"
closable
class="mb-4"
@click:close="success = null"
>
{{ success }}
</VAlert>
<div v-if="statusLoading" class="text-center py-4">
<VProgressCircular indeterminate size="small" />
</div>
<template v-else>
<p class="mb-2">
<strong>Status:</strong>
<VChip
:color="hasPassword ? 'success' : 'grey'"
size="small"
class="ml-2"
>
{{ hasPassword ? 'Password Set' : 'No Password' }}
</VChip>
</p>
<p class="mb-4">
{{ hasPassword ? 'Reset the user\'s password. The user will be able to login with the new password immediately.' : 'Set a password for this user to enable password-based authentication.' }}
</p>
<div class="d-flex gap-2">
<VBtn
color="primary"
:disabled="user.provider === 'oidc'"
@click="passwordDialog = true"
>
{{ hasPassword ? 'Reset Password' : 'Set Password' }}
</VBtn>
<VBtn
v-if="hasPassword"
color="error"
variant="outlined"
:disabled="user.provider === 'oidc'"
@click="removeDialog = true"
>
Remove Password
</VBtn>
</div>
<div v-if="user.provider === 'oidc'" class="text-caption text-grey mt-2">
Password authentication not available for externally managed users
</div>
</template>
</VCardText>
<!-- Password Reset Dialog -->
<VDialog v-model="passwordDialog" max-width="500">
<VCard>
<VCardTitle>{{ hasPassword ? 'Reset Password' : 'Set Password' }}</VCardTitle>
<VCardText>
<VForm @submit.prevent="resetPassword">
<VTextField
v-model="newPassword"
label="New Password"
type="password"
variant="outlined"
class="mb-4"
required
hint="Minimum 8 characters"
/>
<VTextField
v-model="confirmPassword"
label="Confirm Password"
type="password"
variant="outlined"
required
:error="confirmPassword.length > 0 && confirmPassword !== newPassword"
:error-messages="confirmPassword.length > 0 && confirmPassword !== newPassword ? ['Passwords do not match'] : []"
/>
</VForm>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn @click="passwordDialog = false">Cancel</VBtn>
<VBtn
color="primary"
:loading="loading"
@click="resetPassword"
>
{{ hasPassword ? 'Reset Password' : 'Set Password' }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- Remove Password Dialog -->
<VDialog v-model="removeDialog" max-width="500">
<VCard>
<VCardTitle class="d-flex align-center">
<VIcon icon="mdi-alert" color="warning" class="mr-2" />
Remove Password
</VCardTitle>
<VCardText>
<VAlert type="warning" class="mb-4">
<strong>Warning:</strong> This will remove the user's password.
They will not be able to login using password authentication.
</VAlert>
<p>
Are you sure you want to remove the password for <strong>{{ user.label }}</strong>?
</p>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn @click="removeDialog = false">Cancel</VBtn>
<VBtn
color="error"
:loading="loading"
@click="removePassword"
>
Remove Password
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</VCard>
</template>