Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: password reset パスワードリマインダ🎉 #799

Merged
merged 2 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ common:
signin: "Login"
signup: "Sign up"
signout: "Logout"
reminder: "Forgot password"
reload-to-apply-the-setting: "You'll need to reload the page to reflect this setting. Do you want to reload it now?"
fetching-as-ap-object: "Inquiring to fediverse"
unfollow-confirm: "Do you want to unfollow {name}?"
Expand Down Expand Up @@ -710,6 +711,11 @@ common/views/components/signup.vue:
tos: "Terms Of Service"
create: "Create an Account"
some-error: "An attempt at account creation has failed for some reason. Please try again."
common/views/components/reminder.vue:
username: "Username"
email: "Email Address"
desc: "If your email address has been verified, you will be sent a link to reset your password."
submit: "Submit"
common/views/components/special-message.vue:
new-year: "Happy New Year!"
christmas: "Merry Christmas!"
Expand Down Expand Up @@ -918,6 +924,9 @@ common/views/pages/follow-requests.vue:
received-follow-requests: "Follow requests"
accept: "Accept"
reject: "Reject"
common/views/pages/reset-password.vue:
password: "New password"
save: "Save"
desktop:
banner-crop-title: "Crop the part that appears as a banner"
banner: "Banner"
Expand Down
11 changes: 11 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ common:
signin: "ログイン"
signup: "新規登録"
signout: "ログアウト"
reminder: "パスワードを忘れた"
reload-to-apply-the-setting: "この設定を反映するにはページをリロードする必要があります。今すぐリロードしますか?"
fetching-as-ap-object: "連合に照会中"
unfollow-confirm: "{name}さんをフォロー解除しますか?"
Expand Down Expand Up @@ -760,6 +761,12 @@ common/views/components/signup.vue:
create: "アカウント作成"
some-error: "何らかの原因によりアカウントの作成に失敗しました。再度お試しください。"

common/views/components/reminder.vue:
username: "ユーザー名"
email: "登録したメールアドレス"
desc: "メールアドレスが認証済みの場合、パスワードリセット用のリンクが送信されます。"
submit: "送信"

common/views/components/special-message.vue:
new-year: "Happy New Year!"
christmas: "Merry Christmas!"
Expand Down Expand Up @@ -997,6 +1004,10 @@ common/views/pages/follow-requests.vue:
accept: "承認"
reject: "拒否"

common/views/pages/reset-password.vue:
password: "新しいパスワード"
save: "保存"

desktop:
banner-crop-title: "バナーとして表示する部分を選択"
banner: "バナー"
Expand Down
20 changes: 20 additions & 0 deletions migration/1662998896687-password-reset-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class passwordResetRequest1662998896687 implements MigrationInterface {
name = 'passwordResetRequest1662998896687'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "password_reset_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_fcf4b02eae1403a2edaf87fd074" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0b575fa9a4cfe638a925949285" ON "password_reset_request" ("token") `);
await queryRunner.query(`CREATE INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac" ON "password_reset_request" ("userId") `);
await queryRunner.query(`ALTER TABLE "password_reset_request" ADD CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "password_reset_request" DROP CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8"`);
await queryRunner.query(`DROP INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac"`);
await queryRunner.query(`DROP INDEX "IDX_0b575fa9a4cfe638a925949285"`);
await queryRunner.query(`DROP TABLE "password_reset_request"`);
}

}
13 changes: 11 additions & 2 deletions src/client/app/common/views/components/dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
@before-close="onBeforeClose">
<div class="main" ref="main" :class="{ round: $store.state.device.roundedCorners }">
<template v-if="type == 'signin'">
<mk-signin/>
<mk-signin @reminder="onReminder"/>
</template>
<template v-if="type == 'reminder'">
<mk-reminder @done="onReminderDone"/>
</template>
<template v-else>
<div class="icon" v-if="icon">
Expand Down Expand Up @@ -195,7 +198,13 @@ export default Vue.extend({
e.stopPropagation();
this.ok();
}
}
},
onReminder() {
this.type = 'reminder';
},
onReminderDone() {
this.close();
},
}
});
</script>
Expand Down
2 changes: 2 additions & 0 deletions src/client/app/common/views/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import noteHeader from './note-header.vue';
import renote from './renote.vue';
import signin from './signin.vue';
import signup from './signup.vue';
import reminder from './reminder.vue';
import forkit from './forkit.vue';
import acct from './acct.vue';
import avatar from './avatar.vue';
Expand Down Expand Up @@ -67,6 +68,7 @@ Vue.component('mk-note-header', noteHeader);
Vue.component('mk-renote', renote);
Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup);
Vue.component('mk-reminder', reminder);
Vue.component('mk-forkit', forkit);
Vue.component('mk-acct', acct);
Vue.component('mk-avatar', avatar);
Expand Down
57 changes: 57 additions & 0 deletions src/client/app/common/views/components/page/reset-password.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<div class="resetpass17">
<ui-input v-model="password" type="password" required>
<span>{{ $t('password') }}</span>
<template #prefix><fa icon="lock"/></template>
</ui-input>
<ui-button type="submit" :disabled="saving" @click="onSave">{{ $t('save') }}</ui-button>
</div>
</template>

<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('common/views/pages/reset-password.vue'),
props: {
token: {
type: String,
required: true
},
},
data() {
return {
saving: false,
password: '',
};
},
methods: {
onSave() {
this.saving = true;
this.$root.api('reset-password', {
token: this.token,
password: this.password,
}).then(res => {
this.$root.dialog({
type: 'success',
splash: true
});
this.$router.push('/');
}).catch(() => {
alert(this.$t('failed'));
this.saving = false;
});
},
}
});
</script>

<style lang="stylus" scoped>
.resetpass17
padding 32px
max-width 500px
margin 0 auto
text-align center
color var(--text)
$bg = var(--face)
</style>
58 changes: 58 additions & 0 deletions src/client/app/common/views/components/reminder.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<form class="mk-reminder" :class="{ reminderg }" @submit.prevent="onSubmit">
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" required>
<span>{{ $t('username') }}</span>
<template #prefix>@</template>
</ui-input>
<ui-input v-model="email" type="text" spellcheck="false" required>
<span>{{ $t('email') }}</span>
</ui-input>
<p class="desc">
{{ $t('desc') }}
</p>
<ui-button type="submit" :disabled="reminderg">{{ $t('submit') }}</ui-button>
</form>
</template>

<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('common/views/components/reminder.vue'),
data() {
return {
reminderg: false,
username: '',
email: '',
};
},
methods: {
onSubmit() {
this.reminderg = true;
this.$root.api('request-reset-password', {
username: this.username,
email: this.email,
}).then(res => {
this.$root.dialog({
type: 'success',
splash: true
});
this.$emit('done');
}).catch(() => {
alert(this.$t('failed'));
this.reminderg = false;
});
},
}
});
</script>

<style lang="stylus" scoped>
.mk-reminder
color #555
&.reminderg
&, *
cursor wait !important
> .desc
font-size small
</style>
5 changes: 5 additions & 0 deletions src/client/app/common/views/components/signin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<template #prefix><fa icon="lock"/></template>
</ui-input>
<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
<p style="margin: 8px 0;"><a @click="onReminder">{{ $t('@.reminder') }}</a></p>
<p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="['fab', 'twitter']"/> {{ $t('signin-with-twitter') }}</a></p>
<p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="['fab', 'github']"/> {{ $t('signin-with-github') }}</a></p>
<p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="['fab', 'discord']"/> {{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p>
Expand Down Expand Up @@ -135,6 +136,10 @@ export default Vue.extend({
});
},

onReminder() {
this.$emit('reminder');
},

onSubmit() {
this.signing = true;

Expand Down
57 changes: 57 additions & 0 deletions src/client/app/common/views/pages/reset-password.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<div class="resetpass17">
<ui-input v-model="password" type="password" required>
<span>{{ $t('password') }}</span>
<template #prefix><fa icon="lock"/></template>
</ui-input>
<ui-button type="submit" :disabled="saving" @click="onSave">{{ $t('save') }}</ui-button>
</div>
</template>

<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('common/views/pages/reset-password.vue'),
props: {
token: {
type: String,
required: true
},
},
data() {
return {
saving: false,
password: '',
};
},
methods: {
onSave() {
this.saving = true;
this.$root.api('reset-password', {
token: this.token,
password: this.password,
}).then(res => {
this.$root.dialog({
type: 'success',
splash: true
});
this.$router.push('/');
}).catch(() => {
alert(this.$t('failed'));
this.saving = false;
});
},
}
});
</script>

<style lang="stylus" scoped>
.resetpass17
padding 32px
max-width 500px
margin 0 auto
text-align center
color var(--text)
$bg = var(--face)
</style>
2 changes: 2 additions & 0 deletions src/client/app/desktop/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkReversi from './views/pages/games/reversi.vue';
import MkShare from '../common/views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue';
import MkResetPassword from '../common/views/pages/reset-password.vue';
import MkNotFound from '../common/views/pages/not-found.vue';
import MkSettings from './views/pages/settings.vue';
import DeckColumn from '../common/views/deck/deck.column-template.vue';
Expand Down Expand Up @@ -191,6 +192,7 @@ init(async (launch, os) => {
{ path: '/share', component: MkShare },
{ path: '/games/reversi/:game?', component: MkReversi },
{ path: '/authorize-follow', component: MkFollow },
{ path: '/reset-password/:token', component: MkResetPassword, props: true },
{ path: '/deck', redirect: '/' },
{ path: '*', component: MkNotFound }
],
Expand Down
16 changes: 15 additions & 1 deletion src/client/app/desktop/views/pages/welcome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,12 @@

<modal name="signin" class="modal" width="450px" height="auto" scrollable>
<header class="formHeader">{{ $t('@.signin') }}</header>
<mk-signin class="form"/>
<mk-signin class="form" @reminder="reminder" />
</modal>

<modal name="reminder" class="modal" width="450px" height="auto" scrollable>
<header class="formHeader">{{ $t('@.reminder') }}</header>
<mk-reminder class="form" @done="doneReminder" />
</modal>
</div>
</template>
Expand Down Expand Up @@ -214,6 +219,15 @@ export default Vue.extend({
this.$modal.show('signin');
},

reminder() {
this.$modal.hide('signin');
this.$modal.show('reminder');
},

doneReminder() {
this.$modal.hide('reminder');
},

dark() {
this.$store.commit('device/set', {
key: 'darkmode',
Expand Down
2 changes: 2 additions & 0 deletions src/client/app/mobile/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import MkReversi from './views/pages/games/reversi.vue';
import MkTag from './views/pages/tag.vue';
import MkShare from '../common/views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue';
import MkResetPassword from '../common/views/pages/reset-password.vue';
import MkNotFound from '../common/views/pages/not-found.vue';
import DeckColumn from '../common/views/deck/deck.column-template.vue';
import PostFormDialog from './views/components/post-form-dialog.vue';
Expand Down Expand Up @@ -175,6 +176,7 @@ init((launch, os) => {
{ path: '/@:acct/room', props: true, component: () => import('../common/views/pages/room/room.vue').then(m => m.default) },
{ path: '/notes/:note', component: MkNote },
{ path: '/authorize-follow', component: MkFollow },
{ path: '/reset-password/:token', component: MkResetPassword, props: true },
{ path: '*', component: MkNotFound }
]
});
Expand Down
2 changes: 2 additions & 0 deletions src/db/postgre.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { ModerationLog } from '../models/entities/moderation-log';
import { UsedUsername } from '../models/entities/used-username';
import { Relay } from '../models/entities/relay';
import { envOption } from '../env';
import { PasswordResetRequest } from '../models/entities/password-reset-request';

const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);

Expand Down Expand Up @@ -135,6 +136,7 @@ export const entities = [
ReversiGame,
ReversiMatching,
Relay,
PasswordResetRequest,
...charts as any
];

Expand Down
Loading