diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-08-21 16:52:30 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-21 16:52:30 +0900 |
| commit | 7f6ba2e50128182258b92a8572700d14a05a9f8b (patch) | |
| tree | 03739b4016dd893d38e0c66c39511aae6c3b68ef /packages | |
| parent | Bump version to 2025.8.0-beta.2 (diff) | |
| download | misskey-7f6ba2e50128182258b92a8572700d14a05a9f8b.tar.gz misskey-7f6ba2e50128182258b92a8572700d14a05a9f8b.tar.bz2 misskey-7f6ba2e50128182258b92a8572700d14a05a9f8b.zip | |
enhance: verify-emailにフロントエンドUIを実装 (#16431)
* enhance: メールのverifyをAPIに変更
* enhance(frontend): メールのVerifyページを追加
* fix
* :art:
* :art:
* Update Changelog
* lint
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/backend/src/server/ServerService.ts | 24 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoint-list.ts | 1 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/verify-email.ts | 66 | ||||
| -rw-r--r-- | packages/frontend/src/_boot_.ts | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkButton.vue | 3 | ||||
| -rw-r--r-- | packages/frontend/src/pages/signup-complete.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/pages/verify-email.vue | 122 | ||||
| -rw-r--r-- | packages/frontend/src/router.definition.ts | 3 | ||||
| -rw-r--r-- | packages/misskey-js/etc/misskey-js.api.md | 4 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/apiClientJSDoc.ts | 11 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/endpoint.ts | 2 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/entities.ts | 1 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/types.ts | 71 |
13 files changed, 285 insertions, 27 deletions
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 23c085ee27..7325c53df0 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -238,30 +238,6 @@ export class ServerService implements OnApplicationShutdown { } }); - fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => { - const profile = await this.userProfilesRepository.findOneBy({ - emailVerifyCode: request.params.code, - }); - - if (profile != null) { - await this.userProfilesRepository.update({ userId: profile.userId }, { - emailVerified: true, - emailVerifyCode: null, - }); - - this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, { - schema: 'MeDetailed', - includeSecrets: true, - })); - - reply.code(200).send('Verification succeeded! メールアドレスの認証に成功しました。'); - return; - } else { - reply.code(404).send('Verification failed. Please try again. メールアドレスの認証に失敗しました。もう一度お試しください'); - return; - } - }); - fastify.register(this.clientServerService.createServer); this.streamingApiServerService.attach(fastify.server); diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index c0c43dd5c9..f4aa07875d 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -412,6 +412,7 @@ export * as 'users/search' from './endpoints/users/search.js'; export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js'; export * as 'users/show' from './endpoints/users/show.js'; export * as 'users/update-memo' from './endpoints/users/update-memo.js'; +export * as 'verify-email' from './endpoints/verify-email.js'; export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js'; export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js'; export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js'; diff --git a/packages/backend/src/server/api/endpoints/verify-email.ts b/packages/backend/src/server/api/endpoints/verify-email.ts new file mode 100644 index 0000000000..e069ed59f2 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/verify-email.ts @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserProfilesRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ApiError } from '../error.js'; + +export const meta = { + requireCredential: false, + + tags: ['account'], + + errors: { + noSuchCode: { + message: 'No such code.', + code: 'NO_SUCH_CODE', + id: '97c1f576-e4b8-4b8a-a6dc-9cb65e7f6f85', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + code: { type: 'string' }, + }, + required: ['code'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps) => { + const profile = await this.userProfilesRepository.findOneBy({ + emailVerifyCode: ps.code, + }); + + if (profile == null) { + throw new ApiError(meta.errors.noSuchCode); + } + + await this.userProfilesRepository.update({ userId: profile.userId }, { + emailVerified: true, + emailVerifyCode: null, + }); + + this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, { + schema: 'MeDetailed', + includeSecrets: true, + })); + }); + } +} + diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index 354fb95544..111a4abbfd 100644 --- a/packages/frontend/src/_boot_.ts +++ b/packages/frontend/src/_boot_.ts @@ -16,7 +16,7 @@ import '@/style.scss'; import { mainBoot } from '@/boot/main-boot.js'; import { subBoot } from '@/boot/sub-boot.js'; -const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete', '/install-extensions']; +const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete', '/verify-email', '/install-extensions']; if (subBootPaths.some(i => window.location.pathname === i || window.location.pathname.startsWith(i + '/'))) { subBoot(); diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index a77ebd6ac5..b729128a21 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { nextTick, onMounted, useTemplateRef } from 'vue'; +import type { MkABehavior } from '@/components/global/MkA.vue'; const props = defineProps<{ type?: 'button' | 'submit' | 'reset'; @@ -45,7 +46,7 @@ const props = defineProps<{ inline?: boolean; link?: boolean; to?: string; - linkBehavior?: null | 'window' | 'browser'; + linkBehavior?: MkABehavior; autofocus?: boolean; wait?: boolean; danger?: boolean; diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 15954ccc82..8a907c9066 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -51,7 +51,7 @@ function submit() { os.alert({ type: 'error', title: i18n.ts.somethingHappened, - text: i18n.ts.signupPendingError, + text: i18n.ts.emailVerificationFailedError, }); }); } diff --git a/packages/frontend/src/pages/verify-email.vue b/packages/frontend/src/pages/verify-email.vue new file mode 100644 index 0000000000..daf3a0c4c6 --- /dev/null +++ b/packages/frontend/src/pages/verify-email.vue @@ -0,0 +1,122 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<PageWithAnimBg> + <div :class="$style.formContainer"> + <form :class="$style.form" class="_panel" @submit.prevent="submit()"> + <div :class="$style.banner"> + <i class="ti ti-mail-check"></i> + </div> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_enterActive" + :leaveActiveClass="$style.transition_leaveActive" + :enterFromClass="$style.transition_enterFrom" + :leaveToClass="$style.transition_leaveTo" + > + <div v-if="!succeeded" key="input" class="_gaps_m" style="padding: 32px;"> + <div :class="$style.mainText">{{ i18n.tsx.clickToFinishEmailVerification({ ok: i18n.ts.gotIt }) }}</div> + <div> + <MkButton gradate large rounded type="submit" :disabled="submitting" style="margin: 0 auto;"> + {{ submitting ? i18n.ts.processing : i18n.ts.gotIt }}<MkEllipsis v-if="submitting"/> + </MkButton> + </div> + </div> + <div v-else key="success" class="_gaps_m" style="padding: 32px;"> + <div :class="$style.mainText">{{ i18n.ts.emailVerified }}</div> + <div> + <MkButton large rounded link to="/" linkBehavior="browser" style="margin: 0 auto;"> + {{ i18n.ts.goToMisskey }} + </MkButton> + </div> + </div> + </Transition> + </form> + </div> +</PageWithAnimBg> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; + +const submitting = ref(false); +const succeeded = ref(false); + +const props = defineProps<{ + code: string; +}>(); + +function submit() { + if (submitting.value) return; + submitting.value = true; + + misskeyApi('verify-email', { + code: props.code, + }).then(() => { + succeeded.value = true; + submitting.value = false; + }).catch(() => { + submitting.value = false; + + os.alert({ + type: 'error', + title: i18n.ts.somethingHappened, + text: i18n.ts.emailVerificationFailedError, + }); + }); +} +</script> + +<style lang="scss" module> +.transition_enterActive, +.transition_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_leaveTo { + opacity: 0; + transform: translateX(-50px); +} + +.formContainer { + min-height: 100svh; + padding: 32px 32px 64px 32px; + box-sizing: border-box; + display: flex; + align-items: center; +} + +.form { + position: relative; + display: block; + margin: 0 auto; + z-index: 10; + border-radius: var(--MI-radius); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + overflow: clip; + width: 100%; + max-width: 500px; +} + +.banner { + padding: 16px; + text-align: center; + font-size: 26px; + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); +} + +.mainText { + text-align: center; +} +</style> diff --git a/packages/frontend/src/router.definition.ts b/packages/frontend/src/router.definition.ts index 57d9a860d6..e25e0fe161 100644 --- a/packages/frontend/src/router.definition.ts +++ b/packages/frontend/src/router.definition.ts @@ -203,6 +203,9 @@ export const ROUTE_DEF = [{ path: '/signup-complete/:code', component: page(() => import('@/pages/signup-complete.vue')), }, { + path: '/verify-email/:code', + component: page(() => import('@/pages/verify-email.vue')), +}, { path: '/announcements', component: page(() => import('@/pages/announcements.vue')), }, { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index ae12547f35..0416e46cdc 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2129,6 +2129,7 @@ declare namespace entities { UsersUpdateMemoRequest, V2AdminEmojiListRequest, V2AdminEmojiListResponse, + VerifyEmailRequest, Error_2 as Error, UserLite, UserDetailedNotMeOnly, @@ -3807,6 +3808,9 @@ type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestB // @public (undocumented) type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['responses']['200']['content']['application/json']; +// @public (undocumented) +type VerifyEmailRequest = operations['verify-email']['requestBody']['content']['application/json']; + // Warnings were encountered during analysis: // // src/entities.ts:55:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 5407b7a653..c4428efcc2 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -4762,5 +4762,16 @@ declare module '../api.js' { params: P, credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'verify-email', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; } } diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index d7cb2a46eb..5e9fc936b5 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -652,6 +652,7 @@ import type { UsersUpdateMemoRequest, V2AdminEmojiListRequest, V2AdminEmojiListResponse, + VerifyEmailRequest, } from './entities.js'; export type Endpoints = { @@ -1083,6 +1084,7 @@ export type Endpoints = { 'users/show': { req: UsersShowRequest; res: UsersShowResponse }; 'users/update-memo': { req: UsersUpdateMemoRequest; res: EmptyResponse }; 'v2/admin/emoji/list': { req: V2AdminEmojiListRequest; res: V2AdminEmojiListResponse }; + 'verify-email': { req: VerifyEmailRequest; res: EmptyResponse }; }; /** diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index a14febb6e6..73e460c50a 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -655,3 +655,4 @@ export type UsersShowResponse = operations['users___show']['responses']['200'][' export type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json']; export type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestBody']['content']['application/json']; export type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['responses']['200']['content']['application/json']; +export type VerifyEmailRequest = operations['verify-email']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 05ac143762..472b2f9c9e 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3906,6 +3906,15 @@ export type paths = { */ post: operations['v2___admin___emoji___list']; }; + '/verify-email': { + /** + * verify-email + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['verify-email']; + }; }; export type webhooks = Record<string, never>; export type components = { @@ -36387,5 +36396,67 @@ export interface operations { }; }; }; + 'verify-email': { + requestBody: { + content: { + 'application/json': { + code: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; } |