This commit is contained in:
syuilo 2021-05-03 00:39:34 +09:00
parent 98e45b7112
commit 1d55ba9edb
8 changed files with 214 additions and 28 deletions

View file

@ -7,6 +7,7 @@ search: "検索"
notifications: "通知"
username: "ユーザー名"
password: "パスワード"
forgotPassword: "パスワードを忘れた"
fetchingAsApObject: "連合に照会中"
ok: "OK"
gotIt: "わかった"
@ -748,6 +749,11 @@ recentPosts: "最近の投稿"
popularPosts: "人気の投稿"
shareWithNote: "ノートで共有"
_forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
ifNoEmail: "メールアドレスを登録していない場合は、管理者までお問い合わせください。"
contactAdmin: "このインスタンスではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。"
_gallery:
my: "自分の投稿"
liked: "いいねした投稿"

View file

@ -0,0 +1,64 @@
<template>
<XModalWindow ref="dialog"
:width="370"
:height="400"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
>
<template #header>{{ $ts.forgotPassword }}</template>
<form class="_monolithic_" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
<div class="_section">
<MkInput v-model:value="email" type="email" spellcheck="false" autofocus required>
<span>{{ $ts.emailAddress }}</span>
<template #desc>{{ $ts._forgotPassword.enterEmail }}</template>
</MkInput>
<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
</div>
<div class="_section">
<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
</div>
</form>
<div v-else>
{{ $ts._forgotPassword.contactAdmin }}
</div>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
import * as os from '@client/os';
export default defineComponent({
components: {
XModalWindow,
MkButton,
MkInput,
},
emits: ['done', 'closed'],
data() {
return {
email: '',
processing: false,
};
},
methods: {
async onSubmit() {
this.processing = true;
await os.apiWithDialog('request-reset-password', {
email: this.email
});
this.$emit('done');
this.$refs.dialog.close();
}
}
});
</script>

View file

@ -11,6 +11,7 @@
<MkInput v-model:value="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
<span>{{ $ts.password }}</span>
<template #prefix><i class="fas fa-lock"></i></template>
<template #desc><button class="_textButton" @click="resetPassword">{{ $ts.forgotPassword }}</button></template>
</MkInput>
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
</div>
@ -49,8 +50,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { toUnicode } from 'punycode/';
import MkButton from './ui/button.vue';
import MkInput from './ui/input.vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
import { apiUrl, host } from '@client/config';
import { byteify, hexify } from '@client/scripts/2fa';
import * as os from '@client/os';
@ -197,6 +198,11 @@ export default defineComponent({
this.signing = false;
});
}
},
resetPassword() {
os.popup(import('@client/components/forgot-password.vue'), {}, {
}, 'closed');
}
}
});

View file

@ -0,0 +1,69 @@
<template>
<FormBase v-if="token">
<FormInput v-model:value="password" type="password">
<template #prefix><i class="fas fa-lock"></i></template>
<span>{{ $ts.newPassword }}</span>
</FormInput>
<FormButton primary @click="save">{{ $ts.save }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FormLink from '@client/components/form/link.vue';
import FormBase from '@client/components/form/base.vue';
import FormGroup from '@client/components/form/group.vue';
import FormInput from '@client/components/form/input.vue';
import FormButton from '@client/components/form/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
FormBase,
FormGroup,
FormLink,
FormInput,
FormButton,
},
props: {
token: {
type: String,
required: false
}
},
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.resetPassword,
icon: 'fas fa-lock'
},
password: '',
}
},
mounted() {
if (this.token == null) {
os.popup(import('@client/components/forgot-password.vue'), {}, {}, 'closed');
this.$router.push('/');
}
},
methods: {
async save() {
await os.apiWithDialog('reset-password', {
token: this.token,
password: this.password,
});
this.$router.push('/');
}
}
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -23,6 +23,7 @@ export const router = createRouter({
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
{ path: '/@:acct/room', props: true, component: page('room/room') },
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
{ path: '/announcements', component: page('announcements') },
{ path: '/about', component: page('about') },
{ path: '/about-misskey', component: page('about-misskey') },

View file

@ -337,7 +337,7 @@ hr {
}
._monolithic_ {
._section {
._section:not(:empty) {
box-sizing: border-box;
padding: var(--root-margin, 32px);

View file

@ -0,0 +1,51 @@
import $ from 'cafy';
import { publishMainStream } from '../../../services/stream';
import define from '../define';
import rndstr from 'rndstr';
import config from '@/config';
import * as ms from 'ms';
import { Users, UserProfiles, PasswordResetRequests } from '../../../models';
import { sendEmail } from '../../../services/send-email';
import { ApiError } from '../error';
import { genId } from '@/misc/gen-id';
export const meta = {
requireCredential: false as const,
limit: {
duration: ms('1hour'),
max: 3
},
params: {
email: {
validator: $.str
},
},
errors: {
}
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOneOrFail({
email: ps.email,
emailVerified: true
});
const token = rndstr('a-z0-9', 64);
await PasswordResetRequests.insert({
id: genId(),
createdAt: new Date(),
userId: profile.userId,
token
});
const link = `${config.url}/reset-password/${token}`;
sendEmail(ps.email, 'Password reset requested',
`To reset password, please click this link:<br><a href="${link}">${link}</a>`,
`To reset password, please click this link: ${link}`);
});

View file

@ -1,26 +1,21 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import { publishMainStream } from '../../../services/stream';
import define from '../define';
import rndstr from 'rndstr';
import config from '@/config';
import * as ms from 'ms';
import { Users, UserProfiles, PasswordResetRequests } from '../../../models';
import { sendEmail } from '../../../services/send-email';
import { ApiError } from '../error';
import { genId } from '@/misc/gen-id';
export const meta = {
requireCredential: false as const,
limit: {
duration: ms('1hour'),
max: 3
},
params: {
email: {
token: {
validator: $.str
},
password: {
validator: $.str
}
},
errors: {
@ -29,23 +24,17 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOneOrFail({
email: ps.email,
emailVerified: true
const req = await PasswordResetRequests.findOneOrFail({
token: ps.token,
});
const token = rndstr('a-z0-9', 128);
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(ps.password, salt);
await PasswordResetRequests.insert({
id: genId(),
createdAt: new Date(),
userId: profile.userId,
token
await UserProfiles.update(req.userId, {
password: hash
});
const link = `${config.url}/reset-password/${token}`;
sendEmail(ps.email, 'Password reset requested',
`To reset password, please click this link:<br><a href="${link}">${link}</a>`,
`To reset password, please click this link: ${link}`);
PasswordResetRequests.delete(req.id);
});