Password reset (#7494)
* wip * wip * Update well-known.ts * wip * clean up * Update request-reset-password.ts * Update forgot-password.vue * Update reset-password.ts * Update request-reset-password.ts
This commit is contained in:
		
							parent
							
								
									a34d8549d0
								
							
						
					
					
						commit
						6ae642245e
					
				
					 13 changed files with 333 additions and 3 deletions
				
			
		|  | @ -7,6 +7,7 @@ search: "検索" | ||||||
| notifications: "通知" | notifications: "通知" | ||||||
| username: "ユーザー名" | username: "ユーザー名" | ||||||
| password: "パスワード" | password: "パスワード" | ||||||
|  | forgotPassword: "パスワードを忘れた" | ||||||
| fetchingAsApObject: "連合に照会中" | fetchingAsApObject: "連合に照会中" | ||||||
| ok: "OK" | ok: "OK" | ||||||
| gotIt: "わかった" | gotIt: "わかった" | ||||||
|  | @ -748,6 +749,11 @@ recentPosts: "最近の投稿" | ||||||
| popularPosts: "人気の投稿" | popularPosts: "人気の投稿" | ||||||
| shareWithNote: "ノートで共有" | shareWithNote: "ノートで共有" | ||||||
| 
 | 
 | ||||||
|  | _forgotPassword: | ||||||
|  |   enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" | ||||||
|  |   ifNoEmail: "メールアドレスを登録していない場合は、管理者までお問い合わせください。" | ||||||
|  |   contactAdmin: "このインスタンスではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。" | ||||||
|  | 
 | ||||||
| _gallery: | _gallery: | ||||||
|   my: "自分の投稿" |   my: "自分の投稿" | ||||||
|   liked: "いいねした投稿" |   liked: "いいねした投稿" | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								migration/1619942102890-password-reset.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								migration/1619942102890-password-reset.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class passwordReset1619942102890 implements MigrationInterface { | ||||||
|  |     name = 'passwordReset1619942102890' | ||||||
|  | 
 | ||||||
|  |     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"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								src/client/components/forgot-password.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/client/components/forgot-password.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | ||||||
|  | <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="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required> | ||||||
|  | 				<span>{{ $ts.username }}</span> | ||||||
|  | 				<template #prefix>@</template> | ||||||
|  | 			</MkInput> | ||||||
|  | 
 | ||||||
|  | 			<MkInput v-model:value="email" type="email" spellcheck="false" 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 { | ||||||
|  | 			username: '', | ||||||
|  | 			email: '', | ||||||
|  | 			processing: false, | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		async onSubmit() { | ||||||
|  | 			this.processing = true; | ||||||
|  | 			await os.apiWithDialog('request-reset-password', { | ||||||
|  | 				username: this.username, | ||||||
|  | 				email: this.email, | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			this.$emit('done'); | ||||||
|  | 			this.$refs.dialog.close(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
| 			<MkInput v-model:value="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required> | 			<MkInput v-model:value="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required> | ||||||
| 				<span>{{ $ts.password }}</span> | 				<span>{{ $ts.password }}</span> | ||||||
| 				<template #prefix><i class="fas fa-lock"></i></template> | 				<template #prefix><i class="fas fa-lock"></i></template> | ||||||
|  | 				<template #desc><button class="_textButton" @click="resetPassword">{{ $ts.forgotPassword }}</button></template> | ||||||
| 			</MkInput> | 			</MkInput> | ||||||
| 			<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton> | 			<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton> | ||||||
| 		</div> | 		</div> | ||||||
|  | @ -49,8 +50,8 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { toUnicode } from 'punycode/'; | import { toUnicode } from 'punycode/'; | ||||||
| import MkButton from './ui/button.vue'; | import MkButton from '@client/components/ui/button.vue'; | ||||||
| import MkInput from './ui/input.vue'; | import MkInput from '@client/components/ui/input.vue'; | ||||||
| import { apiUrl, host } from '@client/config'; | import { apiUrl, host } from '@client/config'; | ||||||
| import { byteify, hexify } from '@client/scripts/2fa'; | import { byteify, hexify } from '@client/scripts/2fa'; | ||||||
| import * as os from '@client/os'; | import * as os from '@client/os'; | ||||||
|  | @ -197,6 +198,11 @@ export default defineComponent({ | ||||||
| 					this.signing = false; | 					this.signing = false; | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		resetPassword() { | ||||||
|  | 			os.popup(import('@client/components/forgot-password.vue'), {}, { | ||||||
|  | 			}, 'closed'); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
							
								
								
									
										69
									
								
								src/client/pages/reset-password.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/client/pages/reset-password.vue
									
										
									
									
									
										Normal 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> | ||||||
|  | @ -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: '/@: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: '/@: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: '/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: '/announcements', component: page('announcements') }, | ||||||
| 		{ path: '/about', component: page('about') }, | 		{ path: '/about', component: page('about') }, | ||||||
| 		{ path: '/about-misskey', component: page('about-misskey') }, | 		{ path: '/about-misskey', component: page('about-misskey') }, | ||||||
|  |  | ||||||
|  | @ -337,7 +337,7 @@ hr { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ._monolithic_ { | ._monolithic_ { | ||||||
| 	._section { | 	._section:not(:empty) { | ||||||
| 		box-sizing: border-box; | 		box-sizing: border-box; | ||||||
| 		padding: var(--root-margin, 32px); | 		padding: var(--root-margin, 32px); | ||||||
| 	 | 	 | ||||||
|  |  | ||||||
|  | @ -70,6 +70,7 @@ import { Channel } from '../models/entities/channel'; | ||||||
| import { ChannelFollowing } from '../models/entities/channel-following'; | import { ChannelFollowing } from '../models/entities/channel-following'; | ||||||
| import { ChannelNotePining } from '../models/entities/channel-note-pining'; | import { ChannelNotePining } from '../models/entities/channel-note-pining'; | ||||||
| import { RegistryItem } from '../models/entities/registry-item'; | import { RegistryItem } from '../models/entities/registry-item'; | ||||||
|  | import { PasswordResetRequest } from '@/models/entities/password-reset-request'; | ||||||
| 
 | 
 | ||||||
| const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); | const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); | ||||||
| 
 | 
 | ||||||
|  | @ -169,6 +170,7 @@ export const entities = [ | ||||||
| 	ChannelFollowing, | 	ChannelFollowing, | ||||||
| 	ChannelNotePining, | 	ChannelNotePining, | ||||||
| 	RegistryItem, | 	RegistryItem, | ||||||
|  | 	PasswordResetRequest, | ||||||
| 	...charts as any | 	...charts as any | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								src/models/entities/password-reset-request.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/models/entities/password-reset-request.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; | ||||||
|  | import { id } from '../id'; | ||||||
|  | import { User } from './user'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class PasswordResetRequest { | ||||||
|  | 	@PrimaryColumn(id()) | ||||||
|  | 	public id: string; | ||||||
|  | 
 | ||||||
|  | 	@Column('timestamp with time zone') | ||||||
|  | 	public createdAt: Date; | ||||||
|  | 
 | ||||||
|  | 	@Index({ unique: true }) | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 256, | ||||||
|  | 	}) | ||||||
|  | 	public token: string; | ||||||
|  | 
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column({ | ||||||
|  | 		...id(), | ||||||
|  | 	}) | ||||||
|  | 	public userId: User['id']; | ||||||
|  | 
 | ||||||
|  | 	@ManyToOne(type => User, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public user: User | null; | ||||||
|  | } | ||||||
|  | @ -60,6 +60,7 @@ import { MutedNote } from './entities/muted-note'; | ||||||
| import { ChannelFollowing } from './entities/channel-following'; | import { ChannelFollowing } from './entities/channel-following'; | ||||||
| import { ChannelNotePining } from './entities/channel-note-pining'; | import { ChannelNotePining } from './entities/channel-note-pining'; | ||||||
| import { RegistryItem } from './entities/registry-item'; | import { RegistryItem } from './entities/registry-item'; | ||||||
|  | import { PasswordResetRequest } from './entities/password-reset-request'; | ||||||
| 
 | 
 | ||||||
| export const Announcements = getRepository(Announcement); | export const Announcements = getRepository(Announcement); | ||||||
| export const AnnouncementReads = getRepository(AnnouncementRead); | export const AnnouncementReads = getRepository(AnnouncementRead); | ||||||
|  | @ -122,3 +123,4 @@ export const Channels = getCustomRepository(ChannelRepository); | ||||||
| export const ChannelFollowings = getRepository(ChannelFollowing); | export const ChannelFollowings = getRepository(ChannelFollowing); | ||||||
| export const ChannelNotePinings = getRepository(ChannelNotePining); | export const ChannelNotePinings = getRepository(ChannelNotePining); | ||||||
| export const RegistryItems = getRepository(RegistryItem); | export const RegistryItems = getRepository(RegistryItem); | ||||||
|  | export const PasswordResetRequests = getRepository(PasswordResetRequest); | ||||||
|  |  | ||||||
							
								
								
									
										73
									
								
								src/server/api/endpoints/request-reset-password.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/server/api/endpoints/request-reset-password.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | ||||||
|  | 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'; | ||||||
|  | import { IsNull } from 'typeorm'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	requireCredential: false as const, | ||||||
|  | 
 | ||||||
|  | 	limit: { | ||||||
|  | 		duration: ms('1hour'), | ||||||
|  | 		max: 3 | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		username: { | ||||||
|  | 			validator: $.str | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		email: { | ||||||
|  | 			validator: $.str | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps) => { | ||||||
|  | 	const user = await Users.findOne({ | ||||||
|  | 		usernameLower: ps.username.toLowerCase(), | ||||||
|  | 		host: IsNull() | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	// 合致するユーザーが登録されていなかったら無視
 | ||||||
|  | 	if (user == null) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const profile = await UserProfiles.findOneOrFail(user.id); | ||||||
|  | 
 | ||||||
|  | 	// 合致するメアドが登録されていなかったら無視
 | ||||||
|  | 	if (profile.email !== ps.email) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// メアドが認証されていなかったら無視
 | ||||||
|  | 	if (!profile.emailVerified) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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}`); | ||||||
|  | }); | ||||||
							
								
								
									
										45
									
								
								src/server/api/endpoints/reset-password.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/server/api/endpoints/reset-password.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import * as bcrypt from 'bcryptjs'; | ||||||
|  | import { publishMainStream } from '../../../services/stream'; | ||||||
|  | import define from '../define'; | ||||||
|  | import { Users, UserProfiles, PasswordResetRequests } from '../../../models'; | ||||||
|  | import { ApiError } from '../error'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	requireCredential: false as const, | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		token: { | ||||||
|  | 			validator: $.str | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		password: { | ||||||
|  | 			validator: $.str | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user) => { | ||||||
|  | 	const req = await PasswordResetRequests.findOneOrFail({ | ||||||
|  | 		token: ps.token, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	// 発行してから30分以上経過していたら無効
 | ||||||
|  | 	if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) { | ||||||
|  | 		throw new Error(); // TODO
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Generate hash of password
 | ||||||
|  | 	const salt = await bcrypt.genSalt(8); | ||||||
|  | 	const hash = await bcrypt.hash(ps.password, salt); | ||||||
|  | 
 | ||||||
|  | 	await UserProfiles.update(req.userId, { | ||||||
|  | 		password: hash | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	PasswordResetRequests.delete(req.id); | ||||||
|  | }); | ||||||
|  | @ -61,6 +61,11 @@ router.get('/.well-known/nodeinfo', async ctx => { | ||||||
| 	ctx.body = { links }; | 	ctx.body = { links }; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | /* TODO | ||||||
|  | router.get('/.well-known/change-password', async ctx => { | ||||||
|  | }); | ||||||
|  | */ | ||||||
|  | 
 | ||||||
| router.get(webFingerPath, async ctx => { | router.get(webFingerPath, async ctx => { | ||||||
| 	const fromId = (id: User['id']): Record<string, any> => ({ | 	const fromId = (id: User['id']): Record<string, any> => ({ | ||||||
| 		id, | 		id, | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue