summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-08-21 16:52:30 +0900
committerGitHub <noreply@github.com>2025-08-21 16:52:30 +0900
commit7f6ba2e50128182258b92a8572700d14a05a9f8b (patch)
tree03739b4016dd893d38e0c66c39511aae6c3b68ef /packages
parentBump version to 2025.8.0-beta.2 (diff)
downloadmisskey-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.ts24
-rw-r--r--packages/backend/src/server/api/endpoint-list.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/verify-email.ts66
-rw-r--r--packages/frontend/src/_boot_.ts2
-rw-r--r--packages/frontend/src/components/MkButton.vue3
-rw-r--r--packages/frontend/src/pages/signup-complete.vue2
-rw-r--r--packages/frontend/src/pages/verify-email.vue122
-rw-r--r--packages/frontend/src/router.definition.ts3
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md4
-rw-r--r--packages/misskey-js/src/autogen/apiClientJSDoc.ts11
-rw-r--r--packages/misskey-js/src/autogen/endpoint.ts2
-rw-r--r--packages/misskey-js/src/autogen/entities.ts1
-rw-r--r--packages/misskey-js/src/autogen/types.ts71
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'];
+ };
+ };
+ };
+ };
}