From a925572ea8b759550a48de8ce02fe51b6796c675 Mon Sep 17 00:00:00 2001 From: Povo43 Date: Thu, 26 Mar 2026 22:57:56 +0900 Subject: [PATCH 1/8] =?UTF-8?q?EvexAccount=E9=96=A2=E9=80=A3=E3=81=AE?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * a * now * EvexAccount関連の変更はある程度できた. READ: ./document-evexacount/BUILD.md * EvexAccount関連のテキストをi18nに移行 (多言語対応) * fix: 余計な変更を戻す(1) * fix: 余計な変更を戻す(2) * chore: 余計な変更戻す Signed-off-by: Akari Tsuyukusa * chore: 余計な変更戻す2 Signed-off-by: Akari Tsuyukusa --------- Signed-off-by: Akari Tsuyukusa Co-authored-by: Akari Tsuyukusa --- .config/docker_example.env | 3 + locales/ja-JP.yml | 16 + packages/backend/package.json | 1 + .../backend/src/server/EvexAccountService.ts | 326 +++++++++++++ packages/backend/src/server/ServerModule.ts | 2 + .../backend/src/server/api/EndpointsModule.ts | 3 + .../backend/src/server/api/endpoint-list.ts | 2 + .../api/endpoints/evex-account/complete.ts | 33 ++ .../api/endpoints/evex-account/start.ts | 30 ++ packages/frontend/src/_boot_.ts | 2 +- packages/frontend/src/accounts.ts | 26 +- .../frontend/src/components/MkAuthConfirm.vue | 26 +- packages/frontend/src/components/MkSignin.vue | 447 +++--------------- .../src/components/MkSigninDialog.vue | 2 +- .../src/components/MkSignupDialog.form.vue | 312 ++---------- .../src/components/MkSignupDialog.vue | 2 +- .../src/components/MkVisitorDashboard.vue | 4 +- packages/frontend/src/pages/auth.vue | 2 +- packages/frontend/src/pages/callback.vue | 146 ++++++ packages/frontend/src/pages/note.vue | 2 +- .../frontend/src/pages/settings/accounts.vue | 10 +- packages/frontend/src/pages/welcome.setup.vue | 75 +-- packages/frontend/src/router.definition.ts | 3 + packages/frontend/src/ui/visitor.vue | 2 +- packages/frontend/src/utility/evex-account.ts | 93 ++++ packages/frontend/src/utility/please-login.ts | 4 +- pnpm-lock.yaml | 20 +- 27 files changed, 845 insertions(+), 749 deletions(-) create mode 100644 packages/backend/src/server/EvexAccountService.ts create mode 100644 packages/backend/src/server/api/endpoints/evex-account/complete.ts create mode 100644 packages/backend/src/server/api/endpoints/evex-account/start.ts create mode 100644 packages/frontend/src/pages/callback.vue create mode 100644 packages/frontend/src/utility/evex-account.ts diff --git a/.config/docker_example.env b/.config/docker_example.env index c61248da2e4..87deafa17a4 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -9,3 +9,6 @@ POSTGRES_USER=example-misskey-user POSTGRES_DB=misskey # DATABASE_DB=${POSTGRES_DB} DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" + +EVEXACCOUNT_CLIENT_ID= +EVEXACCOUNT_CLIENT_SECRET= diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4af17dd39e7..611fc4993ce 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -35,6 +35,22 @@ login: "ログイン" loggingIn: "ログイン中" logout: "ログアウト" signup: "新規登録" +evexAccount: + title: "EvexAccount" + addWith: "EvexAccountで追加" + signIn: "EvexAccountで続行" + signUp: "EvexAccountで作成" + goToSignup: "EvexAccountへ移動する" + signupDescription: "EvexAccountに登録すると、新しいMisskeyアカウントが作成されます。" + signInDescription: "このアカウントをEvexAccountで連携します。" + authorizeMessage: "このアプリを承認するにはEvexAccountで続行してください。" + signInOrContinue: "EvexAccountでサインインするか、リモートインスタンスで続行してください。" + welcomeCreateIntro: "EvexAccountで初期アカウントを作ってください。" + welcomeCreateButton: "EvexAccountで作る" + pleaseLoginRemote: "EvexAccountを使ってリモートオプションを利用してください。" + pleaseLoginContinue: "続行するにはEvexAccountでサインインしてください。" + errorStart: "EvexAccount認可の開始に失敗しました。" + errorComplete: "EvexAccount認可の完了に失敗しました。" uploading: "アップロード中" save: "保存" users: "ユーザー" diff --git a/packages/backend/package.json b/packages/backend/package.json index 921e89eff97..aad9fe23693 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -189,6 +189,7 @@ "@types/content-disposition": "0.5.9", "@types/fluent-ffmpeg": "2.1.28", "@types/http-link-header": "1.0.7", + "@types/ioredis": "5.0.0", "@types/jest": "29.5.14", "@types/jsonld": "1.5.15", "@types/mime-types": "3.0.1", diff --git a/packages/backend/src/server/EvexAccountService.ts b/packages/backend/src/server/EvexAccountService.ts new file mode 100644 index 00000000000..e1f9d993a41 --- /dev/null +++ b/packages/backend/src/server/EvexAccountService.ts @@ -0,0 +1,326 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { createHash } from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; +import type { MiUserProfile } from '@/models/UserProfile.js'; +import type { MiUser } from '@/models/User.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { SignupService } from '@/core/SignupService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { bindThis } from '@/decorators.js'; +import type { Redis } from 'ioredis'; + +type EvexAccountUserInfo = { + sub: string; + name?: string; + picture?: string; + updated_at?: string; + email?: string; + email_verified?: boolean; + discord_id?: string | null; + discord_roles?: Array<{ + id: string; + name: string; + color: number; + position: number; + }> | null; +}; + +type PendingState = { + codeVerifier: string; + createdAt: number; +}; + +type EvexAccountCompleteResult = { + id: string; + token: string; + username: string; +}; + +@Injectable() +export class EvexAccountService { + private readonly issuer: string; + private readonly authorizationEndpoint: string; + private readonly tokenEndpoint: string; + private readonly userInfoEndpoint: string; + private readonly clientId: string | undefined; + private readonly clientSecret: string | undefined; + private readonly redirectUri: string; + private readonly statePrefix = 'evex-account:state:'; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.redis) + private redis: Redis, + + private httpRequestService: HttpRequestService, + private signupService: SignupService, + private userEntityService: UserEntityService, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + this.issuer = 'https://account.evex.land'; + this.authorizationEndpoint = process.env.EVEXACCOUNT_AUTHORIZATION_ENDPOINT ?? new URL('/api/oauth/authorize', this.issuer).toString(); + this.tokenEndpoint = process.env.EVEXACCOUNT_TOKEN_ENDPOINT ?? new URL('/api/oauth/token', this.issuer).toString(); + this.userInfoEndpoint = process.env.EVEXACCOUNT_USERINFO_ENDPOINT ?? new URL('/api/oauth/userinfo', this.issuer).toString(); + this.clientId = process.env.EVEXACCOUNT_CLIENT_ID; + this.clientSecret = process.env.EVEXACCOUNT_CLIENT_SECRET; + this.redirectUri = new URL('/callback', this.config.url).toString(); + } + + @bindThis + public async createAuthorizationUrl() { + if (!this.clientId) { + throw new Error('EVEXACCOUNT_CLIENT_ID is not configured'); + } + + const state = secureRndstr(64); + const codeVerifier = secureRndstr(96); + const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url'); + + const stateData: PendingState = { + codeVerifier, + createdAt: Date.now(), + }; + + await this.redis.set( + `${this.statePrefix}${state}`, + JSON.stringify(stateData), + 'EX', + 60 * 10, + ); + + const authorizeUrl = new URL(this.authorizationEndpoint); + authorizeUrl.searchParams.set('response_type', 'code'); + authorizeUrl.searchParams.set('client_id', this.clientId); + authorizeUrl.searchParams.set('redirect_uri', this.redirectUri); + authorizeUrl.searchParams.set('scope', 'openid profile email offline_access discord_id'); + authorizeUrl.searchParams.set('state', state); + authorizeUrl.searchParams.set('code_challenge', codeChallenge); + authorizeUrl.searchParams.set('code_challenge_method', 'S256'); + + return { + state, + authorizeUrl: authorizeUrl.toString(), + }; + } + + @bindThis + public async completeAuthorization(code: string, state: string): Promise { + if (!this.clientId) { + throw new Error('EVEXACCOUNT_CLIENT_ID is not configured'); + } + + const rawState = await this.redis.get(`${this.statePrefix}${state}`); + if (!rawState) { + throw new Error('Invalid or expired EvexAccount state'); + } + + await this.redis.del(`${this.statePrefix}${state}`); + + const pendingState = JSON.parse(rawState) as PendingState; + if (!pendingState.codeVerifier || typeof pendingState.codeVerifier !== 'string') { + throw new Error('Invalid EvexAccount state payload'); + } + + const tokenBody = new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: this.redirectUri, + client_id: this.clientId, + code_verifier: pendingState.codeVerifier, + }); + + if (this.clientSecret) { + tokenBody.set('client_secret', this.clientSecret); + } + + const tokenResponse = await this.httpRequestService.send(this.tokenEndpoint, { + method: 'POST', + body: tokenBody.toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + timeout: 10_000, + }, { + throwErrorWhenResponseNotOk: true, + }); + + const tokenJson = await tokenResponse.json() as { + access_token: string; + token_type?: string; + expires_in?: number; + refresh_token?: string; + scope?: string; + id_token?: string; + }; + + if (!tokenJson.access_token) { + throw new Error('EvexAccount did not return an access token'); + } + + const userInfoResponse = await this.httpRequestService.send(this.userInfoEndpoint, { + method: 'GET', + headers: { + Authorization: `Bearer ${tokenJson.access_token}`, + Accept: 'application/json', + }, + timeout: 10_000, + }, { + throwErrorWhenResponseNotOk: true, + }); + + const userInfo = await userInfoResponse.json() as EvexAccountUserInfo; + if (!userInfo.sub) { + throw new Error('EvexAccount userinfo did not include sub'); + } + + const { account, secret } = await this.findOrCreateLocalUser(userInfo); + const me = await this.userEntityService.pack(account, account, { + schema: 'MeDetailed', + includeSecrets: true, + }); + + return { + ...me, + token: secret, + }; + } + + private async findOrCreateLocalUser(userInfo: EvexAccountUserInfo): Promise<{ account: MiUser; secret: string; }> { + const linkedProfile = await this.userProfilesRepository.createQueryBuilder('profile') + .innerJoinAndSelect('profile.user', 'user') + .where("(profile.\"clientData\" -> 'evexAccount' ->> 'sub') = :sub", { sub: userInfo.sub }) + .getOne(); + + if (linkedProfile?.user) { + const account = linkedProfile.user; + await this.updateLinkedProfile(account.id, userInfo, linkedProfile); + await this.usersRepository.update(account.id, { + name: userInfo.name?.slice(0, 128) ?? account.name, + }); + return { + account, + secret: account.token!, + }; + } + + const password = secureRndstr(32); + const usernameCandidates = this.buildUsernameCandidates(userInfo); + let createdAccount: MiUser | null = null; + let secret: string | null = null; + + for (const username of usernameCandidates) { + try { + const result = await this.signupService.signup({ + username, + password, + }); + createdAccount = result.account; + secret = result.secret; + break; + } catch (err) { + const message = typeof err === 'string' ? err : (err as Error).message; + if (![ + 'INVALID_USERNAME', + 'DUPLICATED_USERNAME', + 'USED_USERNAME', + 'DENIED_USERNAME', + ].some(x => message.includes(x))) { + throw err; + } + } + } + + if (createdAccount == null || secret == null) { + throw new Error('Failed to derive a unique username from EvexAccount profile'); + } + + await this.updateLinkedProfile(createdAccount.id, userInfo); + await this.usersRepository.update(createdAccount.id, { + name: userInfo.name?.slice(0, 128) ?? createdAccount.name, + }); + + return { + account: createdAccount, + secret, + }; + } + + private async updateLinkedProfile(accountId: string, userInfo: EvexAccountUserInfo, currentProfile?: MiUserProfile) { + const profile = currentProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: accountId }); + const nextClientData = { + ...(profile.clientData ?? {}), + evexAccount: { + sub: userInfo.sub, + email: userInfo.email ?? null, + name: userInfo.name ?? null, + picture: userInfo.picture ?? null, + updatedAt: userInfo.updated_at ?? null, + }, + }; + + profile.email = userInfo.email ?? null; + profile.emailVerified = userInfo.email_verified === true; + profile.clientData = nextClientData; + await this.userProfilesRepository.save(profile); + } + + private buildUsernameCandidates(userInfo: EvexAccountUserInfo): string[] { + const candidates = new Set(); + const shortHash = createHash('sha256').update(userInfo.sub).digest('hex').slice(0, 10); + + const rawSources = [ + userInfo.email?.split('@')[0], + userInfo.name, + `evex_${shortHash}`, + ]; + + for (const source of rawSources) { + const normalized = this.normalizeUsername(source); + if (normalized) { + candidates.add(normalized); + } + } + + const generated = [...candidates]; + for (const base of generated) { + for (let i = 1; i <= 9; i++) { + candidates.add(this.truncateUsername(`${base}${i}`)); + } + } + + return [...candidates]; + } + + private normalizeUsername(source: string | undefined): string | null { + if (!source) return null; + + const normalized = source + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .replace(/[^a-z0-9_]+/g, '_') + .replace(/^_+|_+$/g, '') + .replace(/_+/g, '_'); + + if (!normalized) return null; + return this.truncateUsername(normalized); + } + + private truncateUsername(username: string): string { + return username.slice(0, 20); + } +} diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index e228d511037..473d71679a1 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -29,6 +29,7 @@ import { FeedService } from './web/FeedService.js'; import { UrlPreviewService } from './web/UrlPreviewService.js'; import { ClientLoggerService } from './web/ClientLoggerService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; +import { EvexAccountService } from './EvexAccountService.js'; import MainStreamConnection from '@/server/api/stream/Connection.js'; import { MainChannel } from './api/stream/channels/main.js'; @@ -102,6 +103,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j NoteStreamingHidingService, OpenApiServerService, OAuth2ProviderService, + EvexAccountService, ], exports: [ ServerService, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 9cfb2f0ac0a..7bf2b772719 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -10,6 +10,7 @@ import * as endpointsObject from './endpoint-list.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; +import { EvexAccountService } from '../EvexAccountService.js'; const endpoints = Object.entries(endpointsObject); const endpointProviders = endpoints.map(([path, endpoint]): Provider => ({ provide: `ep:${path}`, useClass: endpoint.default })); @@ -19,11 +20,13 @@ const endpointProviders = endpoints.map(([path, endpoint]): Provider => ({ provi CoreModule, ], providers: [ + EvexAccountService, GetterService, ApiLoggerService, ...endpointProviders, ], exports: [ + EvexAccountService, ...endpointProviders, ], }) diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 6679005c3c4..a63b7ebf00f 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -194,6 +194,8 @@ export * as 'emoji' from './endpoints/emoji.js'; export * as 'emojis' from './endpoints/emojis.js'; export * as 'endpoint' from './endpoints/endpoint.js'; export * as 'endpoints' from './endpoints/endpoints.js'; +export * as 'evex-account/complete' from './endpoints/evex-account/complete.js'; +export * as 'evex-account/start' from './endpoints/evex-account/start.js'; export * as 'export-custom-emojis' from './endpoints/export-custom-emojis.js'; export * as 'federation/followers' from './endpoints/federation/followers.js'; export * as 'federation/following' from './endpoints/federation/following.js'; diff --git a/packages/backend/src/server/api/endpoints/evex-account/complete.ts b/packages/backend/src/server/api/endpoints/evex-account/complete.ts new file mode 100644 index 00000000000..2bdd0e07568 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/evex-account/complete.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EvexAccountService } from '@/server/EvexAccountService.js'; + +export const meta = { + tags: ['auth'], + requireCredential: false, +} as const; + +export const paramDef = { + type: 'object', + properties: { + code: { type: 'string' }, + state: { type: 'string' }, + }, + required: ['code', 'state'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private evexAccountService: EvexAccountService, + ) { + super(meta, paramDef, async (ps) => { + return await this.evexAccountService.completeAuthorization(ps.code, ps.state); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/evex-account/start.ts b/packages/backend/src/server/api/endpoints/evex-account/start.ts new file mode 100644 index 00000000000..f5094db9cf8 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/evex-account/start.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EvexAccountService } from '@/server/EvexAccountService.js'; + +export const meta = { + tags: ['auth'], + requireCredential: false, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private evexAccountService: EvexAccountService, + ) { + super(meta, paramDef, async () => { + return await this.evexAccountService.createAuthorizationUrl(); + }); + } +} diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index 111a4abbfdf..f175e8f3bbc 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', '/verify-email', '/install-extensions']; +const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/callback', '/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/accounts.ts b/packages/frontend/src/accounts.ts index 862ef4e113d..eb77095d608 100644 --- a/packages/frontend/src/accounts.ts +++ b/packages/frontend/src/accounts.ts @@ -301,23 +301,23 @@ export async function getAccountMenu(opts: { menuItems.push(...accountItems); - menuItems.push({ - type: 'parent', - icon: 'ti ti-plus', - text: i18n.ts.addAccount, - children: [{ - text: i18n.ts.existingAccount, - action: () => { - getAccountWithSigninDialog().then(res => { - if (res != null) { + menuItems.push({ + type: 'parent', + icon: 'ti ti-plus', + text: i18n.ts.addAccount, + children: [{ + text: i18n.ts.evexAccount.signIn, + action: () => { + getAccountWithSigninDialog().then(res => { + if (res != null) { success(); } }); }, - }, { - text: i18n.ts.createAccount, - action: () => { - getAccountWithSignupDialog().then(res => { + }, { + text: i18n.ts.evexAccount.signUp, + action: () => { + getAccountWithSignupDialog().then(res => { if (res != null) { switchAccount(host, res.id); } diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue index b1a29660adb..dcccf8cde64 100644 --- a/packages/frontend/src/components/MkAuthConfirm.vue +++ b/packages/frontend/src/components/MkAuthConfirm.vue @@ -34,12 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -186,11 +186,11 @@ init(); function clickAddAccount(ev: PointerEvent) { selectedUser.value = null; - os.popupMenu([{ - text: i18n.ts.existingAccount, - action: () => { - getAccountWithSigninDialog().then(async (res) => { - if (res != null) { + os.popupMenu([{ + text: i18n.ts.evexAccount.signIn, + action: () => { + getAccountWithSigninDialog().then(async (res) => { + if (res != null) { os.success(); await init(); if (users.value.has(res.id)) { @@ -199,8 +199,8 @@ function clickAddAccount(ev: PointerEvent) { } }); }, - }, { - text: i18n.ts.createAccount, + }, { + text: i18n.ts.evexAccount.signUp, action: () => { getAccountWithSignupDialog().then(async (res) => { if (res != null) { diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index b2f0f4ddcf1..c6b8a6b2ec4 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -4,83 +4,39 @@ SPDX-License-Identifier: AGPL-3.0-only --> -async function onTotpSubmitted(token: string) { - waiting.value = true; - - if (userInfo.value == null) { - await os.alert({ - type: 'error', - title: i18n.ts.noSuchUser, - text: i18n.ts.signinFailed, - }); - waiting.value = false; - return; - } else { - await tryLogin({ - username: userInfo.value.username, - password: password.value, - token, - }); - } + diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 77141f17141..3f76681b860 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only >
-
{{ i18n.ts.login }}
+
{{ i18n.ts.evexAccount.title }}
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 68ba09980a1..697b4027ee8 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -6,92 +6,35 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -328,7 +84,7 @@ function onSignupApiError() { color: var(--MI_THEME-accent); } -.captcha { - margin: 16px 0; +.text { + text-align: center; } diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index 5c9047dd43f..2993459f998 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only @close="onClose" @closed="emit('closed')" > - +
{{ i18n.ts.federationDisabled }}
- {{ i18n.ts.joinThisServer }} + {{ i18n.ts.evexAccount.signUp }} {{ i18n.ts.exploreOtherServers }} - {{ i18n.ts.login }} + {{ i18n.ts.evexAccount.signIn }}
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index 14b13e511aa..7a3b8a5458a 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-

{{ i18n.ts._auth.pleaseLogin }}

+

{{ i18n.ts.evexAccount.authorizeMessage }}

diff --git a/packages/frontend/src/pages/callback.vue b/packages/frontend/src/pages/callback.vue new file mode 100644 index 00000000000..9fe115c0446 --- /dev/null +++ b/packages/frontend/src/pages/callback.vue @@ -0,0 +1,146 @@ + + + + + + + diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 2bbd7b25115..7e05dec77bf 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -139,7 +139,7 @@ function fetchNote() { if (['fbcc002d-37d9-4944-a6b0-d9e29f2d33ab', '145f88d2-b03d-4087-8143-a78928883c4b'].includes(err.id)) { pleaseLogin({ path: '/', - message: err.id === 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab' ? i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor : i18n.ts.signinOrContinueOnRemote, + message: err.id === 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab' ? i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor : i18n.ts.evexAccount.signInOrContinue, openOnRemote: { type: 'lookup', url: `https://${host}/notes/${props.noteId}`, diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index 55a81bbf386..9934561ef4d 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -19,18 +19,14 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 697b4027ee8..d81f950c5c1 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -6,35 +6,110 @@ SPDX-License-Identifier: AGPL-3.0-only