From 01aa56c60201aed3e278193a0aee7c13b1f0aef7 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:50:01 +0900 Subject: enhance(backend/oauth): Support client information discovery in the IndieAuth 11 July 2024 spec (#17030) * enhance(backend): Support client information discovery in the IndieAuth 11 July 2024 spec * add tests * Update Changelog * Update Changelog * fix tests * fix test describe to align with the other describe format --- .../src/server/oauth/OAuth2ProviderService.ts | 89 ++++++++++++++++------ 1 file changed, 67 insertions(+), 22 deletions(-) (limited to 'packages/backend/src/server/oauth') diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index d2391c43ab..47f4bf947d 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -123,41 +123,84 @@ function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: str return { name, logo }; } -// https://indieauth.spec.indieweb.org/#client-information-discovery -// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id, -// and if there is an [h-app] with a url property matching the client_id URL, -// then it should use the name and icon and display them on the authorization prompt." -// (But we don't display any icon for now) -// https://indieauth.spec.indieweb.org/#redirect-url -// "The client SHOULD publish one or more tags or Link HTTP headers with a rel attribute -// of redirect_uri at the client_id URL. -// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST -// look for an exact match of the given redirect_uri in the request against the list of -// redirect_uris discovered after resolving any relative URLs." async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise { try { const res = await httpRequestService.send(id); + const redirectUris: string[] = []; + let name = id; + let logo: string | null = null; + // https://indieauth.spec.indieweb.org/#redirect-url + // "The client SHOULD publish one or more tags or Link HTTP headers with a rel attribute + // of redirect_uri at the client_id URL. + // Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST + // look for an exact match of the given redirect_uri in the request against the list of + // redirect_uris discovered after resolving any relative URLs." const linkHeader = res.headers.get('link'); if (linkHeader) { redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri)); } - const text = await res.text(); - const doc = htmlParser.parse(`
${text}
`); + if (res.headers.get('content-type')?.includes('application/json')) { + // Client discovery via JSON document (11 July 2024 spec) + // https://indieauth.spec.indieweb.org/#client-metadata + // "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing + // client metadata defined in [RFC7591], the minimum properties for an IndieAuth + // client defined below." + + const json = await res.json() as { + client_id: string; + client_name?: string; + client_uri: string; + logo_uri?: string; + redirect_uris?: string[]; + }; + + // https://indieauth.spec.indieweb.org/#client-metadata-li-1 + // "The authorization server MUST verify that the client_id in the document matches the + // client_id of the URL where the document was retrieved." + if (json.client_id !== id) { + throw new AuthorizationError('client_id in the document does not match the client_id URL', 'invalid_request'); + } - redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href)); + // https://indieauth.spec.indieweb.org/#client-metadata-li-1 + // "The client_uri MUST be a prefix of the client_id." + if (!json.client_uri || !id.startsWith(json.client_uri)) { + throw new AuthorizationError('client_uri is not a prefix of client_id', 'invalid_request'); + } - let name = id; - let logo: string | null = null; - if (text) { - const microformats = parseMicroformats(doc, res.url, id); - if (typeof microformats.name === 'string') { - name = microformats.name; + if (typeof json.client_name === 'string') { + name = json.client_name; } - if (typeof microformats.logo === 'string') { - logo = microformats.logo; + + if (typeof json.logo_uri === 'string') { + // Since uri can be relative, resolve it against the document URL + logo = new URL(json.logo_uri, res.url).toString(); + } + + if (Array.isArray(json.redirect_uris)) { + redirectUris.push(...json.redirect_uris.filter((uri): uri is string => typeof uri === 'string')); + } + } else { + // Client discovery via HTML microformats (12 February 2022 spec) + // https://indieauth.spec.indieweb.org/20220212/#client-information-discovery + // "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id, + // and if there is an [h-app] with a url property matching the client_id URL, + // then it should use the name and icon and display them on the authorization prompt." + const text = await res.text(); + const doc = htmlParser.parse(`
${text}
`); + + redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href)); + + if (text) { + const microformats = parseMicroformats(doc, res.url, id); + if (typeof microformats.name === 'string') { + name = microformats.name; + } + if (typeof microformats.logo === 'string') { + logo = microformats.logo; + } } } @@ -172,6 +215,8 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt logger.error('Error while fetching client information', { err }); if (err instanceof StatusError) { throw new AuthorizationError('Failed to fetch client information', 'invalid_request'); + } else if (err instanceof AuthorizationError) { + throw err; } else { throw new AuthorizationError('Failed to parse client information', 'server_error'); } -- cgit v1.2.3-freya From 3980b2ca55cdf6a694ef8cad911e161672c0444f Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:24:30 +0900 Subject: fix: review fixes (#17208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: OAuthのContent-Typeを正しく判定するように * fix(frontend): fix outdated comments * fix: storagePersistenceのtop-level awaitを解消 * fix * fix(frontend): add comment Co-Authored-By: anatawa12 * fix * fix: rename `users/get-following-users-by-birthday` * fix: fix types * Update MkForm.vue * refactor utility/storage.ts --------- Co-authored-by: anatawa12 Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 2 +- packages/backend/src/server/api/endpoint-list.ts | 2 +- .../src/server/api/endpoints/admin/update-meta.ts | 2 +- .../src/server/api/endpoints/users/following.ts | 4 +- .../users/get-following-birthday-users.ts | 167 --------------------- .../users/get-following-users-by-birthday.ts | 167 +++++++++++++++++++++ .../src/server/oauth/OAuth2ProviderService.ts | 4 +- packages/frontend/src/components/MkDraggable.vue | 1 + packages/frontend/src/components/MkForm.vue | 2 +- .../src/pages/admin/RolesEditorFormula.vue | 2 +- .../src/pages/page-editor/page-editor.blocks.vue | 2 +- packages/frontend/src/pages/settings/index.vue | 4 +- packages/frontend/src/pages/settings/other.vue | 4 +- packages/frontend/src/utility/storage.ts | 12 +- .../src/widgets/WidgetBirthdayFollowings.user.vue | 2 +- .../src/widgets/WidgetBirthdayFollowings.vue | 2 +- packages/misskey-js/etc/misskey-js.api.md | 8 +- packages/misskey-js/src/autogen/apiClientJSDoc.ts | 4 +- packages/misskey-js/src/autogen/endpoint.ts | 6 +- packages/misskey-js/src/autogen/entities.ts | 4 +- packages/misskey-js/src/autogen/types.ts | 12 +- 21 files changed, 214 insertions(+), 199 deletions(-) delete mode 100644 packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts create mode 100644 packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts (limited to 'packages/backend/src/server/oauth') diff --git a/CHANGELOG.md b/CHANGELOG.md index 24271d7fa0..520b202f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 2026.2.0 ### Note -- `users/following` の `birthday` プロパティは非推奨になりました。代わりに `users/get-following-birthday-users` をご利用ください。 +- `users/following` の `birthday` プロパティは非推奨になりました。代わりに `users/get-following-users-by-birthday` をご利用ください。 ### General - Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 9311c80eaa..6679005c3c 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -391,7 +391,7 @@ export * as 'users/featured-notes' from './endpoints/users/featured-notes.js'; export * as 'users/flashs' from './endpoints/users/flashs.js'; export * as 'users/followers' from './endpoints/users/followers.js'; export * as 'users/following' from './endpoints/users/following.js'; -export * as 'users/get-following-birthday-users' from './endpoints/users/get-following-birthday-users.js'; +export * as 'users/get-following-users-by-birthday' from './endpoints/users/get-following-users-by-birthday.js'; export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js'; export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js'; export * as 'users/lists/create' from './endpoints/users/lists/create.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 7a8dfc4555..372fe3a25f 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -341,7 +341,7 @@ export default class extends Endpoint { // eslint- if (ps.clientOptions !== undefined) { set.clientOptions = { - ...serverSettings.clientOptions, + ...this.serverSettings.clientOptions, ...ps.clientOptions, }; } diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 3b3352858f..4defcc9dcf 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -86,7 +86,7 @@ export const paramDef = { sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - birthday: { ...birthdaySchema, nullable: true, description: '@deprecated use get-following-birthday-users instead.' }, + birthday: { ...birthdaySchema, nullable: true, description: '@deprecated use get-following-users-by-birthday instead.' }, }, }, ], @@ -146,7 +146,7 @@ export default class extends Endpoint { // eslint- .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); - // @deprecated use get-following-birthday-users instead. + // @deprecated use get-following-users-by-birthday instead. if (ps.birthday) { query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId'); diff --git a/packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts b/packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts deleted file mode 100644 index 124114244e..0000000000 --- a/packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Brackets } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { - FollowingsRepository, - UserProfilesRepository, -} from '@/models/_.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { Packed } from '@/misc/json-schema.js'; - -export const meta = { - tags: ['users'], - - requireCredential: true, - kind: 'read:account', - - description: 'Find users who have a birthday on the specified range.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'misskey:id', - }, - birthday: { - type: 'string', - optional: false, nullable: false, - }, - user: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - offset: { type: 'integer', default: 0 }, - birthday: { - oneOf: [{ - type: 'object', - properties: { - month: { type: 'integer', minimum: 1, maximum: 12 }, - day: { type: 'integer', minimum: 1, maximum: 31 }, - }, - required: ['month', 'day'], - }, { - type: 'object', - properties: { - begin: { - type: 'object', - properties: { - month: { type: 'integer', minimum: 1, maximum: 12 }, - day: { type: 'integer', minimum: 1, maximum: 31 }, - }, - required: ['month', 'day'], - }, - end: { - type: 'object', - properties: { - month: { type: 'integer', minimum: 1, maximum: 12 }, - day: { type: 'integer', minimum: 1, maximum: 31 }, - }, - required: ['month', 'day'], - }, - }, - required: ['begin', 'end'], - }], - }, - }, - required: ['birthday'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - private userEntityService: UserEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.followingsRepository - .createQueryBuilder('following') - .andWhere('following.followerId = :userId', { userId: me.id }) - .innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId'); - - if (Object.hasOwn(ps.birthday, 'begin') && Object.hasOwn(ps.birthday, 'end')) { - const range = ps.birthday as { begin: { month: number; day: number }; end: { month: number; day: number }; }; - - // 誕生日は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)でインデックスが効くようになっているので、その形式に変換 - const begin = range.begin.month * 100 + range.begin.day; - const end = range.end.month * 100 + range.end.day; - - if (begin <= end) { - query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND :end', { begin, end }); - } else { - // 12/31 から 1/1 の範囲を取得するために OR で対応 - query.andWhere(new Brackets(qb => { - qb.where('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND 1231', { begin }); - qb.orWhere('get_birthday_date(followeeProfile.birthday) BETWEEN 101 AND :end', { end }); - })); - } - } else { - const { month, day } = ps.birthday as { month: number; day: number }; - // なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応 - query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: month * 100 + day }); - } - - query.select('following.followeeId', 'user_id'); - query.addSelect('get_birthday_date(followeeProfile.birthday)', 'birthday_date'); - query.orderBy('birthday_date', 'ASC'); - - const birthdayUsers = await query - .offset(ps.offset).limit(ps.limit) - .getRawMany<{ birthday_date: number; user_id: string }>(); - - const users = new Map>(( - await this.userEntityService.packMany( - birthdayUsers.map(u => u.user_id), - me, - { schema: 'UserLite' }, - ) - ).map(u => [u.id, u])); - - return birthdayUsers - .map(item => { - const birthday = new Date(); - birthday.setHours(0, 0, 0, 0); - // item.birthday_date は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)で出力されるので、日付に戻してDateオブジェクトに設定 - birthday.setMonth(Math.floor(item.birthday_date / 100) - 1, item.birthday_date % 100); - - if (birthday.getTime() < new Date().setHours(0, 0, 0, 0)) { - birthday.setFullYear(new Date().getFullYear() + 1); - } - - const birthdayStr = `${birthday.getFullYear()}-${(birthday.getMonth() + 1).toString().padStart(2, '0')}-${(birthday.getDate()).toString().padStart(2, '0')}`; - return { - id: item.user_id, - birthday: birthdayStr, - user: users.get(item.user_id), - }; - }) - .filter(item => item.user != null) - .map(item => item as { id: string; birthday: string; user: Packed<'UserLite'> }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts b/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts new file mode 100644 index 0000000000..947c19d81e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts @@ -0,0 +1,167 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { + FollowingsRepository, + UserProfilesRepository, +} from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import type { Packed } from '@/misc/json-schema.js'; + +export const meta = { + tags: ['users'], + + requireCredential: true, + kind: 'read:account', + + description: 'Retrieve users who have a birthday on the specified range.', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'misskey:id', + }, + birthday: { + type: 'string', + optional: false, nullable: false, + }, + user: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + offset: { type: 'integer', default: 0 }, + birthday: { + oneOf: [{ + type: 'object', + properties: { + month: { type: 'integer', minimum: 1, maximum: 12 }, + day: { type: 'integer', minimum: 1, maximum: 31 }, + }, + required: ['month', 'day'], + }, { + type: 'object', + properties: { + begin: { + type: 'object', + properties: { + month: { type: 'integer', minimum: 1, maximum: 12 }, + day: { type: 'integer', minimum: 1, maximum: 31 }, + }, + required: ['month', 'day'], + }, + end: { + type: 'object', + properties: { + month: { type: 'integer', minimum: 1, maximum: 12 }, + day: { type: 'integer', minimum: 1, maximum: 31 }, + }, + required: ['month', 'day'], + }, + }, + required: ['begin', 'end'], + }], + }, + }, + required: ['birthday'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.followingsRepository + .createQueryBuilder('following') + .andWhere('following.followerId = :userId', { userId: me.id }) + .innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId'); + + if (Object.hasOwn(ps.birthday, 'begin') && Object.hasOwn(ps.birthday, 'end')) { + const range = ps.birthday as { begin: { month: number; day: number }; end: { month: number; day: number }; }; + + // 誕生日は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)でインデックスが効くようになっているので、その形式に変換 + const begin = range.begin.month * 100 + range.begin.day; + const end = range.end.month * 100 + range.end.day; + + if (begin <= end) { + query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND :end', { begin, end }); + } else { + // 12/31 から 1/1 の範囲を取得するために OR で対応 + query.andWhere(new Brackets(qb => { + qb.where('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND 1231', { begin }); + qb.orWhere('get_birthday_date(followeeProfile.birthday) BETWEEN 101 AND :end', { end }); + })); + } + } else { + const { month, day } = ps.birthday as { month: number; day: number }; + // なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応 + query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: month * 100 + day }); + } + + query.select('following.followeeId', 'user_id'); + query.addSelect('get_birthday_date(followeeProfile.birthday)', 'birthday_date'); + query.orderBy('birthday_date', 'ASC'); + + const birthdayUsers = await query + .offset(ps.offset).limit(ps.limit) + .getRawMany<{ birthday_date: number; user_id: string }>(); + + const users = new Map>(( + await this.userEntityService.packMany( + birthdayUsers.map(u => u.user_id), + me, + { schema: 'UserLite' }, + ) + ).map(u => [u.id, u])); + + return birthdayUsers + .map(item => { + const birthday = new Date(); + birthday.setHours(0, 0, 0, 0); + // item.birthday_date は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)で出力されるので、日付に戻してDateオブジェクトに設定 + birthday.setMonth(Math.floor(item.birthday_date / 100) - 1, item.birthday_date % 100); + + if (birthday.getTime() < new Date().setHours(0, 0, 0, 0)) { + birthday.setFullYear(new Date().getFullYear() + 1); + } + + const birthdayStr = `${birthday.getFullYear()}-${(birthday.getMonth() + 1).toString().padStart(2, '0')}-${(birthday.getDate()).toString().padStart(2, '0')}`; + return { + id: item.user_id, + birthday: birthdayStr, + user: users.get(item.user_id), + }; + }) + .filter(item => item.user != null) + .map(item => item as { id: string; birthday: string; user: Packed<'UserLite'> }); + }); + } +} diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index 47f4bf947d..840c34b806 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -142,7 +142,9 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri)); } - if (res.headers.get('content-type')?.includes('application/json')) { + const contentType = res.headers.get('content-type'); + const mediaType = contentType ? contentType.split(';')[0].trim() : null; + if (mediaType === 'application/json') { // Client discovery via JSON document (11 July 2024 spec) // https://indieauth.spec.indieweb.org/#client-metadata // "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing diff --git a/packages/frontend/src/components/MkDraggable.vue b/packages/frontend/src/components/MkDraggable.vue index 07fa1045b9..6e2e038f87 100644 --- a/packages/frontend/src/components/MkDraggable.vue +++ b/packages/frontend/src/components/MkDraggable.vue @@ -109,6 +109,7 @@ function onDragstart(ev: DragEvent, item: T) { // Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう // SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately + // SEE: https://issues.chromium.org/issues/41150279 window.setTimeout(() => { dragging.value = true; }, 10); diff --git a/packages/frontend/src/components/MkForm.vue b/packages/frontend/src/components/MkForm.vue index 1ece0ad4c3..f2360e8cdd 100644 --- a/packages/frontend/src/components/MkForm.vue +++ b/packages/frontend/src/components/MkForm.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->