Compare commits
No commits in common. "34206c66fd1ac806c50796f299af2567f6d90f3b" and "50c33eb172190291dfe66f0c3f5476184910ce10" have entirely different histories.
34206c66fd
...
50c33eb172
|
@ -2,9 +2,9 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="./src/web/assets/favicon.png" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Trocar minha senha</title>
|
<title>Vuetify 3</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
function encodePassword(password: string): string {
|
export function encodePassword(password: string): string {
|
||||||
let encodedPassword = ''
|
let newPassword = ''
|
||||||
password = '"' + password + '"'
|
password = '"' + password + '"'
|
||||||
|
for (let i = 0; i < password.length; i++) {
|
||||||
for (let i = 0; i < password.length; i++)
|
newPassword += String.fromCharCode(
|
||||||
encodedPassword += String.fromCharCode(
|
|
||||||
password.charCodeAt(i) & 0xff,
|
password.charCodeAt(i) & 0xff,
|
||||||
(password.charCodeAt(i) >>> 8) & 0xff
|
(password.charCodeAt(i) >>> 8) & 0xff
|
||||||
)
|
)
|
||||||
|
}
|
||||||
return encodedPassword
|
return newPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
export { encodePassword }
|
|
||||||
|
|
|
@ -8,28 +8,26 @@ import {
|
||||||
import { encodePassword } from './encodePassword'
|
import { encodePassword } from './encodePassword'
|
||||||
|
|
||||||
const ldapClient = new Client({
|
const ldapClient = new Client({
|
||||||
url: process.env.AD_URL || 'ldaps://10.1.0.16',
|
url: process.env.LDAP_URL || 'ldap://10.1.0.16'
|
||||||
tlsOptions: {
|
|
||||||
requestCert: true
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const adminUser = process.env.AD_BIND_USER || ''
|
const bindUser = process.env.AD_BIND_USER || ''
|
||||||
const adminPassword = process.env.AD_BIND_PASSWORD || ''
|
const bindPassword = process.env.AD_BIND_PASSWORD || ''
|
||||||
const baseDN = process.env.AD_BASE_DN || ''
|
const baseDN = process.env.AD_BASE_DN || ''
|
||||||
|
|
||||||
async function getUserDN(username: string): Promise<string> {
|
async function getUserDN(username: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
await ldapClient.bind(adminUser, adminPassword)
|
await ldapClient.bind(bindUser, bindPassword)
|
||||||
|
|
||||||
const { searchEntries } = await ldapClient.search(baseDN, {
|
const { searchEntries } = await ldapClient.search(baseDN, {
|
||||||
|
scope: 'sub',
|
||||||
attributes: ['dn'],
|
attributes: ['dn'],
|
||||||
filter: `(sAMAccountName=${username})`
|
filter: `(sAMAccountName=${username})`
|
||||||
})
|
})
|
||||||
|
|
||||||
return searchEntries[0]?.dn
|
return searchEntries[0]?.dn
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error finding user:', err)
|
console.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
await ldapClient.unbind()
|
await ldapClient.unbind()
|
||||||
}
|
}
|
||||||
|
@ -39,50 +37,41 @@ async function getUserDN(username: string): Promise<string> {
|
||||||
|
|
||||||
export async function updatePassword({
|
export async function updatePassword({
|
||||||
username,
|
username,
|
||||||
currentPassword,
|
password,
|
||||||
newPassword
|
newPassword
|
||||||
}: {
|
}: {
|
||||||
username: string
|
username: string
|
||||||
currentPassword: string
|
password: string
|
||||||
newPassword: string
|
newPassword: string
|
||||||
}): Promise<'SUCCESS' | 'FAIL'> {
|
}): Promise<'SUCCESS' | 'FAIL'> {
|
||||||
try {
|
try {
|
||||||
const userDN = await getUserDN(username)
|
const userDN = await getUserDN(username)
|
||||||
|
|
||||||
// Check if user can bind with current password
|
await ldapClient.bind(userDN, password)
|
||||||
await ldapClient.bind(userDN, currentPassword)
|
|
||||||
await ldapClient.unbind()
|
|
||||||
|
|
||||||
// Bind with admin user to change password
|
console.log('binded')
|
||||||
await ldapClient.bind(adminUser, adminPassword)
|
|
||||||
|
|
||||||
await ldapClient.modify(userDN, [
|
await ldapClient.modify(userDN, [
|
||||||
new Change({
|
new Change({
|
||||||
operation: 'replace',
|
operation: 'delete',
|
||||||
|
modification: new Attribute({
|
||||||
|
type: 'unicodePwd',
|
||||||
|
values: [encodePassword(password)]
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
new Change({
|
||||||
|
operation: 'add',
|
||||||
modification: new Attribute({
|
modification: new Attribute({
|
||||||
type: 'unicodePwd',
|
type: 'unicodePwd',
|
||||||
values: [encodePassword(newPassword)]
|
values: [encodePassword(newPassword)]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// new Change({
|
|
||||||
// operation: 'delete',
|
|
||||||
// modification: new Attribute({
|
|
||||||
// type: 'unicodePwd',
|
|
||||||
// values: [encodePassword(currentPassword)]
|
|
||||||
// })
|
|
||||||
// }),
|
|
||||||
|
|
||||||
// new Change({
|
|
||||||
// operation: 'add',
|
|
||||||
// modification: new Attribute({
|
|
||||||
// type: 'unicodePwd',
|
|
||||||
// values: [encodePassword(newPassword)]
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return 'SUCCESS'
|
return 'SUCCESS'
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log(err)
|
||||||
|
|
||||||
if (err instanceof InvalidCredentialsError) {
|
if (err instanceof InvalidCredentialsError) {
|
||||||
throw new Error('Usuário ou senha atual incorreta.')
|
throw new Error('Usuário ou senha atual incorreta.')
|
||||||
}
|
}
|
||||||
|
@ -91,11 +80,10 @@ export async function updatePassword({
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'A senha atual está correta, mas o servidor recusou a alteração. Verifique se a nova senha atende aos requisitos de complexidade.'
|
'A senha atual está correta, mas o servidor recusou a alteração. Verifique se a nova senha atende aos requisitos de complexidade.'
|
||||||
)
|
)
|
||||||
}
|
} else throw err
|
||||||
|
|
||||||
console.log('Unexpected error:', err)
|
|
||||||
throw err
|
|
||||||
} finally {
|
} finally {
|
||||||
await ldapClient.unbind()
|
await ldapClient.unbind()
|
||||||
|
console.log('unbinded')
|
||||||
}
|
}
|
||||||
|
return 'FAIL'
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,20 +16,19 @@ export const appRouter = router({
|
||||||
updatePassword: input(
|
updatePassword: input(
|
||||||
z.object({
|
z.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
currentPassword: z.string(),
|
password: z.string(),
|
||||||
newPassword: z.string().min(8)
|
newPassword: z.string().min(8)
|
||||||
})
|
})
|
||||||
).mutation(async ({ input }) => {
|
).mutation(async ({ input }) => {
|
||||||
const { username, currentPassword, newPassword } = input
|
console.log('input', input)
|
||||||
|
|
||||||
|
const { username, password, newPassword } = input
|
||||||
try {
|
try {
|
||||||
await updatePassword({
|
await updatePassword({
|
||||||
username,
|
username,
|
||||||
currentPassword,
|
password,
|
||||||
newPassword
|
newPassword
|
||||||
})
|
})
|
||||||
|
|
||||||
return 'SUCCESS'
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<v-main>
|
<v-main>
|
||||||
<router-view />
|
{{ data }}
|
||||||
<v-footer app class="text-grey">
|
|
||||||
<span class="mr-2">v{{ version }}</span>
|
|
||||||
<v-spacer />
|
|
||||||
<span class="mr-2">SERTI-PP</span>
|
|
||||||
</v-footer>
|
|
||||||
</v-main>
|
</v-main>
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import packageInfo from '../../package.json'
|
import { ref, onMounted } from "vue";
|
||||||
|
|
||||||
const version = packageInfo.version
|
import { trpc } from "./trpc";
|
||||||
|
|
||||||
|
const data = ref<string>("");
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
data.value = await trpc.hello.query();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 12 KiB |
6
src/web/assets/logo.svg
Normal file
6
src/web/assets/logo.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
|
||||||
|
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
|
||||||
|
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
|
||||||
|
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 526 B |
75
src/web/components/HelloWorld.vue
Normal file
75
src/web/components/HelloWorld.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<v-container class="fill-height">
|
||||||
|
<v-responsive class="align-center text-center fill-height">
|
||||||
|
<v-img height="300" src="@/assets/logo.svg" />
|
||||||
|
|
||||||
|
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||||
|
|
||||||
|
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
|
||||||
|
|
||||||
|
<div class="py-14" />
|
||||||
|
|
||||||
|
<v-row class="d-flex align-center justify-center">
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn
|
||||||
|
href="https://vuetifyjs.com/components/all/"
|
||||||
|
min-width="164"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-view-dashboard"
|
||||||
|
size="large"
|
||||||
|
start
|
||||||
|
/>
|
||||||
|
|
||||||
|
Components
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||||
|
min-width="228"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
size="x-large"
|
||||||
|
target="_blank"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-speedometer"
|
||||||
|
size="large"
|
||||||
|
start
|
||||||
|
/>
|
||||||
|
|
||||||
|
Get Started
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn
|
||||||
|
href="https://community.vuetifyjs.com/"
|
||||||
|
min-width="164"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-account-group"
|
||||||
|
size="large"
|
||||||
|
start
|
||||||
|
/>
|
||||||
|
|
||||||
|
Community
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-responsive>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
//
|
||||||
|
</script>
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-img src="@/assets/logo.png" contain />
|
|
||||||
</template>
|
|
|
@ -1,131 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-form ref="form" @submit.prevent="submit" validate-on="input">
|
|
||||||
<v-card :elevation="2" :loading="loading" :disabled="loading">
|
|
||||||
<v-card-title class="mb-6 pa-6 text-center">
|
|
||||||
<span class="headline font-weight-light text-h4">Trocar senha</span>
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<v-text-field
|
|
||||||
class="mb-4"
|
|
||||||
v-model="username"
|
|
||||||
label="Usuário"
|
|
||||||
:variant="'outlined'"
|
|
||||||
autocomplete="username"
|
|
||||||
hint="SIAPE para servidores, CPF para alunos."
|
|
||||||
prepend-inner-icon="mdi-account"
|
|
||||||
:rules="[v => !!v || 'O usuário é obrigatório']"
|
|
||||||
required
|
|
||||||
density="compact"
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
class="mb-4"
|
|
||||||
v-model="password"
|
|
||||||
label="Senha atual"
|
|
||||||
:type="showCurrent ? 'text' : 'password'"
|
|
||||||
hint="Sua senha atual"
|
|
||||||
:variant="'outlined'"
|
|
||||||
prepend-inner-icon="mdi-form-textbox-password"
|
|
||||||
:append-inner-icon="showCurrent ? 'mdi-eye' : 'mdi-eye-off'"
|
|
||||||
@click:append-inner="showCurrent = !showCurrent"
|
|
||||||
:rules="[v => !!v || 'A senha atual é obrigatória']"
|
|
||||||
required
|
|
||||||
density="compact"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-text-field
|
|
||||||
class="mb-4"
|
|
||||||
v-model="newPassword"
|
|
||||||
label="Nova senha"
|
|
||||||
:type="showNew ? 'text' : 'password'"
|
|
||||||
hint="A nova senha que você deseja usar"
|
|
||||||
:variant="'outlined'"
|
|
||||||
prepend-inner-icon="mdi-lock-check"
|
|
||||||
:append-inner-icon="showNew ? 'mdi-eye' : 'mdi-eye-off'"
|
|
||||||
@click:append-inner="showNew = !showNew"
|
|
||||||
:rules="newPasswordRules"
|
|
||||||
required
|
|
||||||
density="compact"
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-model="confirmPassword"
|
|
||||||
label="Confirme a nova senha"
|
|
||||||
:type="showConfirm ? 'text' : 'password'"
|
|
||||||
hint="Digite a nova senha mais uma vez"
|
|
||||||
:variant="'outlined'"
|
|
||||||
prepend-inner-icon="mdi-lock-check"
|
|
||||||
:append-inner-icon="showConfirm ? 'mdi-eye' : 'mdi-eye-off'"
|
|
||||||
@click:append-inner="showConfirm = !showConfirm"
|
|
||||||
:rules="[
|
|
||||||
v => !!v || 'A confirmação da senha é obrigatória',
|
|
||||||
v => v === newPassword || 'As senhas não coincidem'
|
|
||||||
]"
|
|
||||||
required
|
|
||||||
density="compact"
|
|
||||||
/>
|
|
||||||
<password-checker class="mt-4" :password="newPassword" />
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
type="submit"
|
|
||||||
color="primary"
|
|
||||||
variant="flat"
|
|
||||||
size="large"
|
|
||||||
:disabled="!valid || loading"
|
|
||||||
>
|
|
||||||
Trocar senha
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-form>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import PasswordChecker from './PasswordChecker.vue'
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
submit: [{ username: string; currentPassword: string; newPassword: string }]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
loading: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const username = ref('')
|
|
||||||
const password = ref('')
|
|
||||||
const newPassword = ref('')
|
|
||||||
const confirmPassword = ref('')
|
|
||||||
|
|
||||||
const showCurrent = ref(false)
|
|
||||||
const showNew = ref(false)
|
|
||||||
const showConfirm = ref(false)
|
|
||||||
|
|
||||||
// use form refs to validate
|
|
||||||
const form = ref<HTMLFormElement | null>(null)
|
|
||||||
|
|
||||||
const valid = computed(() => {
|
|
||||||
return form.value?.isValid || false
|
|
||||||
})
|
|
||||||
|
|
||||||
const newPasswordRules = [
|
|
||||||
(v: string) => !!v || 'A nova senha é obrigatória',
|
|
||||||
(v: string) =>
|
|
||||||
/[a-z]/.test(v) || 'A nova senha deve ter pelo menos uma letra minúscula',
|
|
||||||
(v: string) =>
|
|
||||||
/[A-Z]/.test(v) || 'A nova senha deve ter pelo menos uma letra maiúscula',
|
|
||||||
(v: string) =>
|
|
||||||
v.length >= 8 || 'A nova senha deve ter pelo menos 8 caracteres',
|
|
||||||
(v: string) =>
|
|
||||||
/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(v) ||
|
|
||||||
'A nova senha deve ter pelo menos um caractere especial'
|
|
||||||
]
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
if (await form.value?.validate())
|
|
||||||
emit('submit', {
|
|
||||||
username: username.value,
|
|
||||||
currentPassword: password.value,
|
|
||||||
newPassword: newPassword.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,52 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="font-weight-light mb-2">Regras para a nova senha:</div>
|
|
||||||
<v-alert
|
|
||||||
v-for="alert in alerts"
|
|
||||||
:key="alert.text"
|
|
||||||
:type="alertType(password, alert.rule)"
|
|
||||||
class="mb-2 rule"
|
|
||||||
variant="tonal"
|
|
||||||
density="compact"
|
|
||||||
:text="alert.text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
defineProps<{
|
|
||||||
password: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
function alertType(password: string, rule: (password: string) => boolean) {
|
|
||||||
if (password.length === 0) return 'info'
|
|
||||||
|
|
||||||
return rule(password) ? 'success' : 'error'
|
|
||||||
}
|
|
||||||
|
|
||||||
const alerts = [
|
|
||||||
{
|
|
||||||
text: 'Uma ou mais letras minúsculas.',
|
|
||||||
rule: (password: string) => /[a-z]/.test(password)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Uma ou mais letras maiúsculas.',
|
|
||||||
rule: (password: string) => /[A-Z]/.test(password)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '8 ou mais caracteres.',
|
|
||||||
rule: (password: string) => password.length >= 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: `Um ou mais caracteres especiais (!"#$%&'()*+,\\-./:;<=>?@[]^_\`{}|~).`,
|
|
||||||
rule: (password: string) =>
|
|
||||||
/[●!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(password)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.rule {
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,42 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<HelloWorld />
|
||||||
<v-row justify="center">
|
|
||||||
<v-col xl="5" lg="6" md="7" sm="10">
|
|
||||||
<logo class="mx-auto my-3" :style="{ maxWidth: '128px' }" />
|
|
||||||
<main-form @submit="handleSubmit" :loading="loading" />
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import HelloWorld from '@/components/HelloWorld.vue'
|
||||||
import Logo from '../components/Logo.vue'
|
|
||||||
import MainForm from '../components/MainForm.vue'
|
|
||||||
import { trpc } from '../trpc'
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
async function handleSubmit({
|
|
||||||
username,
|
|
||||||
currentPassword,
|
|
||||||
newPassword
|
|
||||||
}: {
|
|
||||||
username: string
|
|
||||||
currentPassword: string
|
|
||||||
newPassword: string
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
await trpc.updatePassword.mutate({
|
|
||||||
username,
|
|
||||||
currentPassword,
|
|
||||||
newPassword
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user