enhance(frontend): パスワード変更時にHIBPで流出パスワードをチェックするように (MisskeyIO#625)
This commit is contained in:
parent
86e15db338
commit
5f8d4cf7b4
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
@ -7904,10 +7904,6 @@ export interface Locale extends ILocale {
|
||||
* バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。
|
||||
*/
|
||||
"backupCodesExhaustedWarning": string;
|
||||
/**
|
||||
* 詳細なガイドはこちら
|
||||
*/
|
||||
"moreDetailedGuideHere": string;
|
||||
/**
|
||||
* バックアップコードを保存しましたか?
|
||||
*/
|
||||
|
161
packages/frontend/src/components/MkNewPassword.vue
Normal file
161
packages/frontend/src/components/MkNewPassword.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<MkInput v-model="password" :debounce="true" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
|
||||
<template #label>
|
||||
{{ label }} <a href="https://haveibeenpwned.com/Passwords" target="_blank" rel="nofollow noopener"><span :class="$style.hibpLogo">leak checked by <span>';--hibp?</span></span></a>
|
||||
</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordStrength == 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-if="passwordStrength == 'low' && isLeaked" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.tsx.leakedPassword({ n: leakedCount }) }}</span>
|
||||
<span v-else-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
|
||||
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
|
||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||
<template #label>{{ label }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
|
||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
||||
defineProps<{
|
||||
label: string;
|
||||
}>();
|
||||
|
||||
const password = ref<string>('');
|
||||
const retypedPassword = ref<string>('');
|
||||
const passwordStrength = ref<'' | 'wait' | 'low' | 'medium' | 'high'>('');
|
||||
const isLeaked = ref(false);
|
||||
const leakedCount = ref(0);
|
||||
const passwordRetypeState = ref<null | 'match' | 'not-match'>(null);
|
||||
const passwordAbortController = ref<null | AbortController>(null);
|
||||
|
||||
const isValid = computed((): boolean => {
|
||||
return passwordRetypeState.value === 'match'
|
||||
&& (passwordStrength.value === 'medium' || passwordStrength.value === 'high');
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
isValid,
|
||||
password,
|
||||
});
|
||||
|
||||
async function getPasswordStrength(source: string): Promise<number> {
|
||||
let strength = 0;
|
||||
let power = 0.018;
|
||||
|
||||
// 英数字
|
||||
if (/[a-zA-Z]/.test(source) && /[0-9]/.test(source)) {
|
||||
power += 0.020;
|
||||
}
|
||||
|
||||
// 大文字と小文字が混ざってたら
|
||||
if (/[a-z]/.test(source) && /[A-Z]/.test(source)) {
|
||||
power += 0.015;
|
||||
}
|
||||
|
||||
// 記号が混ざってたら
|
||||
if (/[!\x22\#$%&@'()*+,-./_]/.test(source)) {
|
||||
power += 0.02;
|
||||
}
|
||||
|
||||
strength = power * source.length;
|
||||
|
||||
// check HIBP 3 chars or more
|
||||
if (passwordAbortController.value != null) {
|
||||
passwordAbortController.value.abort();
|
||||
}
|
||||
|
||||
if (source.length >= 3) {
|
||||
passwordStrength.value = 'wait';
|
||||
passwordAbortController.value = new AbortController();
|
||||
|
||||
const hash = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(source));
|
||||
const hashHex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
const hashPrefix = hashHex.slice(0, 5).toUpperCase();
|
||||
const hashSuffix = hashHex.slice(5).toUpperCase();
|
||||
await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`, { signal: passwordAbortController.value.signal })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(text => {
|
||||
const lines = text.split('\n');
|
||||
const line = lines.find(l => l.startsWith(hashSuffix));
|
||||
if (line) {
|
||||
leakedCount.value = parseInt(line.split(':')[1]);
|
||||
isLeaked.value = true;
|
||||
strength = 0;
|
||||
} else {
|
||||
isLeaked.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
leakedCount.value = 0;
|
||||
isLeaked.value = false;
|
||||
});
|
||||
} else {
|
||||
leakedCount.value = 0;
|
||||
isLeaked.value = false;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(1, strength));
|
||||
}
|
||||
|
||||
async function onChangePassword(): Promise<void> {
|
||||
if (password.value === '') {
|
||||
passwordStrength.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const strength = await getPasswordStrength(password.value);
|
||||
passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||
|
||||
onChangePasswordRetype();
|
||||
}
|
||||
|
||||
function onChangePasswordRetype(): void {
|
||||
if (retypedPassword.value === '') {
|
||||
passwordRetypeState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.hibpLogo {
|
||||
background: linear-gradient(45deg, #616c70, #626262);
|
||||
color: #fefefe;
|
||||
display: inline-flex;
|
||||
padding-left: 8px;
|
||||
margin-left: 4px;
|
||||
font-size: 0.8em;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
transform: translateY(-1px);
|
||||
|
||||
span {
|
||||
background: linear-gradient(45deg, #255e81, #338cac);
|
||||
font-size: 1.4em;
|
||||
padding: 2px 8px;
|
||||
height: auto;
|
||||
margin-left: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -45,27 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" :debounce="true" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
|
||||
<template #label>
|
||||
{{ i18n.ts.password }} <a href="https://haveibeenpwned.com/Passwords" target="_blank" rel="nofollow noopener"><span :class="$style.hibpLogo">leak checked by <span>';--hibp?</span></span></a>
|
||||
</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordStrength == 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-if="passwordStrength == 'low' && isLeaked" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.tsx.leakedPassword({ n: leakedCount }) }}</span>
|
||||
<span v-else-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
|
||||
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
|
||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
|
||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkNewPassword ref="password" :label="i18n.ts.password"/>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
@ -82,11 +62,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, shallowRef } from 'vue';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkNewPassword from '@/components/MkNewPassword.vue';
|
||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||
import * as config from '@/config.js';
|
||||
import * as os from '@/os.js';
|
||||
@ -113,16 +94,11 @@ const recaptcha = ref<Captcha | undefined>();
|
||||
const turnstile = ref<Captcha | undefined>();
|
||||
|
||||
const username = ref<string>('');
|
||||
const password = ref<string>('');
|
||||
const retypedPassword = ref<string>('');
|
||||
const password = shallowRef<InstanceType<typeof MkNewPassword> | null>(null);
|
||||
const invitationCode = ref<string>('');
|
||||
const email = ref('');
|
||||
const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null);
|
||||
const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:banned' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
|
||||
const passwordStrength = ref<'' | 'wait' | 'low' | 'medium' | 'high'>('');
|
||||
const isLeaked = ref(false);
|
||||
const leakedCount = ref(0);
|
||||
const passwordRetypeState = ref<null | 'match' | 'not-match'>(null);
|
||||
const submitting = ref<boolean>(false);
|
||||
const hCaptchaResponse = ref<string | null>(null);
|
||||
const mCaptchaResponse = ref<string | null>(null);
|
||||
@ -130,7 +106,6 @@ const reCaptchaResponse = ref<string | null>(null);
|
||||
const turnstileResponse = ref<string | null>(null);
|
||||
const usernameAbortController = ref<null | AbortController>(null);
|
||||
const emailAbortController = ref<null | AbortController>(null);
|
||||
const passwordAbortController = ref<null | AbortController>(null);
|
||||
|
||||
const shouldDisableSubmitting = computed((): boolean => {
|
||||
return submitting.value ||
|
||||
@ -139,75 +114,10 @@ const shouldDisableSubmitting = computed((): boolean => {
|
||||
instance.enableRecaptcha && !reCaptchaResponse.value ||
|
||||
instance.enableTurnstile && !turnstileResponse.value ||
|
||||
instance.emailRequiredForSignup && emailState.value !== 'ok' ||
|
||||
(passwordStrength.value !== 'medium' && passwordStrength.value !== 'high') ||
|
||||
usernameState.value !== 'ok' ||
|
||||
passwordRetypeState.value !== 'match';
|
||||
!password.value?.isValid;
|
||||
});
|
||||
|
||||
async function getPasswordStrength(source: string): Promise<number> {
|
||||
let strength = 0;
|
||||
let power = 0.018;
|
||||
|
||||
// 英数字
|
||||
if (/[a-zA-Z]/.test(source) && /[0-9]/.test(source)) {
|
||||
power += 0.020;
|
||||
}
|
||||
|
||||
// 大文字と小文字が混ざってたら
|
||||
if (/[a-z]/.test(source) && /[A-Z]/.test(source)) {
|
||||
power += 0.015;
|
||||
}
|
||||
|
||||
// 記号が混ざってたら
|
||||
if (/[!\x22\#$%&@'()*+,-./_]/.test(source)) {
|
||||
power += 0.02;
|
||||
}
|
||||
|
||||
strength = power * source.length;
|
||||
|
||||
// check HIBP 3 chars or more
|
||||
if (passwordAbortController.value != null) {
|
||||
passwordAbortController.value.abort();
|
||||
}
|
||||
|
||||
if (source.length >= 3) {
|
||||
passwordStrength.value = 'wait';
|
||||
passwordAbortController.value = new AbortController();
|
||||
|
||||
const hash = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(source));
|
||||
const hashHex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
const hashPrefix = hashHex.slice(0, 5).toUpperCase();
|
||||
const hashSuffix = hashHex.slice(5).toUpperCase();
|
||||
await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`, { signal: passwordAbortController.value.signal })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(text => {
|
||||
const lines = text.split('\n');
|
||||
const line = lines.find(l => l.startsWith(hashSuffix));
|
||||
if (line) {
|
||||
leakedCount.value = parseInt(line.split(':')[1]);
|
||||
isLeaked.value = true;
|
||||
strength = 0;
|
||||
} else {
|
||||
isLeaked.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
leakedCount.value = 0;
|
||||
isLeaked.value = false;
|
||||
});
|
||||
} else {
|
||||
leakedCount.value = 0;
|
||||
isLeaked.value = false;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(1, strength));
|
||||
}
|
||||
|
||||
function onChangeUsername(): void {
|
||||
if (username.value === '') {
|
||||
usernameState.value = null;
|
||||
@ -274,35 +184,14 @@ function onChangeEmail(): void {
|
||||
});
|
||||
}
|
||||
|
||||
async function onChangePassword(): Promise<void> {
|
||||
if (password.value === '') {
|
||||
passwordStrength.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const strength = await getPasswordStrength(password.value);
|
||||
passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||
|
||||
if (passwordRetypeState.value === 'match') onChangePasswordRetype();
|
||||
}
|
||||
|
||||
function onChangePasswordRetype(): void {
|
||||
if (retypedPassword.value === '') {
|
||||
passwordRetypeState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match';
|
||||
}
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
if (submitting.value) return;
|
||||
if (!password.value?.isValid || submitting.value) return;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
await misskeyApi('signup', {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
password: password.value.password,
|
||||
emailAddress: email.value,
|
||||
invitationCode: invitationCode.value,
|
||||
'hcaptcha-response': hCaptchaResponse.value,
|
||||
@ -320,7 +209,7 @@ async function onSubmit(): Promise<void> {
|
||||
} else {
|
||||
const res = await misskeyApi('signin', {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
password: password.value.password,
|
||||
});
|
||||
emit('signup', res);
|
||||
|
||||
@ -354,26 +243,4 @@ async function onSubmit(): Promise<void> {
|
||||
.captcha {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.hibpLogo {
|
||||
background: linear-gradient(45deg, #616c70, #626262);
|
||||
color: #fefefe;
|
||||
display: inline-flex;
|
||||
padding-left: 8px;
|
||||
margin-left: 4px;
|
||||
font-size: 0.8em;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
transform: translateY(-1px);
|
||||
|
||||
span {
|
||||
background: linear-gradient(45deg, #255e81, #338cac);
|
||||
font-size: 1.4em;
|
||||
padding: 2px 8px;
|
||||
height: auto;
|
||||
margin-left: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -8,20 +8,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="token" :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="password" type="password">
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #label>{{ i18n.ts.newPassword }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton primary @click="save">{{ i18n.ts.save }}</MkButton>
|
||||
<MkNewPassword ref="newPassword" :label="i18n.ts.newPassword"/>
|
||||
<MkButton primary :disabled="shouldDisableSubmitting" @click="save">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref, computed } from 'vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { defineAsyncComponent, onMounted, ref, computed, shallowRef } from 'vue';
|
||||
import MkNewPassword from '@/components/MkNewPassword.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@ -32,12 +28,20 @@ const props = defineProps<{
|
||||
token?: string;
|
||||
}>();
|
||||
|
||||
const password = ref('');
|
||||
const newPassword = shallowRef<InstanceType<typeof MkNewPassword> | null>(null);
|
||||
const submitting = ref<boolean>(false);
|
||||
|
||||
const shouldDisableSubmitting = computed((): boolean => {
|
||||
return submitting.value || !newPassword.value?.isValid;
|
||||
});
|
||||
|
||||
async function save() {
|
||||
if (!newPassword.value?.isValid || submitting.value) return;
|
||||
submitting.value = true;
|
||||
|
||||
await os.apiWithDialog('reset-password', {
|
||||
token: props.token,
|
||||
password: password.value,
|
||||
password: newPassword.value.password,
|
||||
});
|
||||
mainRouter.push('/');
|
||||
}
|
||||
|
@ -7,7 +7,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_m">
|
||||
<FormSection first>
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<MkButton primary @click="change()">{{ i18n.ts.changePassword }}</MkButton>
|
||||
<MkFolder key="changePasswordKey">
|
||||
<template #icon><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.changePassword }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkNewPassword ref="newPassword" :label="i18n.ts.newPassword"/>
|
||||
<MkButton primary :disabled="!newPassword?.isValid" @click="changePassword">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</FormSection>
|
||||
|
||||
<X2fa/>
|
||||
@ -40,10 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
import X2fa from './2fa.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkNewPassword from '@/components/MkNewPassword.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os.js';
|
||||
@ -51,33 +60,16 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const changePasswordKey = ref(Date.now());
|
||||
const newPassword = shallowRef<InstanceType<typeof MkNewPassword> | null>(null);
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'i/signin-history' as const,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
async function change() {
|
||||
const { canceled: canceled2, result: newPassword } = await os.inputText({
|
||||
title: i18n.ts.newPassword,
|
||||
type: 'password',
|
||||
autocomplete: 'new-password',
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
const { canceled: canceled3, result: newPassword2 } = await os.inputText({
|
||||
title: i18n.ts.newPasswordRetype,
|
||||
type: 'password',
|
||||
autocomplete: 'new-password',
|
||||
});
|
||||
if (canceled3) return;
|
||||
|
||||
if (newPassword !== newPassword2) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.retypedNotMatch,
|
||||
});
|
||||
return;
|
||||
}
|
||||
async function changePassword() {
|
||||
if (!newPassword.value?.isValid) return;
|
||||
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
@ -85,7 +77,9 @@ async function change() {
|
||||
os.apiWithDialog('i/change-password', {
|
||||
currentPassword: auth.result.password,
|
||||
token: auth.result.token,
|
||||
newPassword,
|
||||
newPassword: newPassword.value.password,
|
||||
}).then(() => {
|
||||
changePasswordKey.value = Date.now();
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user