Compare commits

..

26 Commits

Author SHA1 Message Date
github-actions[bot]
254c063455 Bump version to 2024.10.0-beta.5 2024-10-05 07:31:13 +00:00
かっこかり
9d026975bc fix(backend/test): #14558 以降e2eテストがたまに失敗する問題を修正 (#14709)
* fix(backend/test): MisskeyIO#727 以降e2eテストがたまに失敗する問題を修正 (MisskeyIO#735)

* ✌️

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-10-05 16:20:44 +09:00
syuilo
d8cb7305ef feat: 通報の強化 (#14704)
* wip

* Update CHANGELOG.md

* lint

* Update types.ts

* wip

* ✌️

* Update MkAbuseReport.vue

* tweak
2024-10-05 16:20:15 +09:00
syuilo
043fef9fdf 🎨 2024-10-05 15:19:07 +09:00
おさむのひと
0d7d1091c8 enhance: 人気のPlayを10件以上表示できるように (#14443)
Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
2024-10-05 14:37:52 +09:00
かっこかり
d8bf1ff7e9 #14675 レビューの修正 (#14705) 2024-10-05 13:47:50 +09:00
おさむのひと
88698462a9 feat(backend): 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるようにする (#14698)
* feat(backend): 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるようにする

* テスト送信もペイロード形式を合わせる

* add spaces

* fix test
2024-10-05 12:51:46 +09:00
かっこかり
ae3c155490 fix: signin の資格情報が足りないだけの場合はエラーにせず200を返すように (#14700)
* fix: signin の資格情報が足りないだけの場合はエラーにせず200を返すように

* run api extractor

* fix

* fix

* fix test

* /signin -> /signin-flow

* fix

* fix lint

* rename

* fix

* fix
2024-10-05 12:03:47 +09:00
syuilo
fa06c59eae 🎨 2024-10-04 19:09:46 +09:00
かっこかり
b36d13d90c fix(frontend): ログイン画面でキャプチャが表示されない問題を修正 (#14694)
* fix(frontend): ログイン画面でキャプチャが表示されない問題を修正

* rename
2024-10-04 18:45:03 +09:00
github-actions[bot]
3d637af65b Bump version to 2024.10.0-beta.4 2024-10-04 08:41:30 +00:00
syuilo
2340de035b New Crowdin updates (#14677)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)
2024-10-04 17:32:36 +09:00
かっこかり
d8f30fb793 fix(frontend): canvas-confettiの型定義を追加 (#14692) 2024-10-04 17:32:18 +09:00
syuilo
708ffaef5c 🎨 2024-10-04 17:29:10 +09:00
syuilo
2639e92e18 🎨 2024-10-04 17:07:27 +09:00
syuilo
ea2675eaab fix(frontend): リンク動作のオーバーライドが動作していないのを修正 2024-10-04 16:41:08 +09:00
github-actions[bot]
3b0b4f83dd Bump version to 2024.10.0-beta.3 2024-10-04 06:28:36 +00:00
かっこかり
975c2e7bc5 enhance(frontend): サインイン画面の改善 (#14658)
* wip

* Update MkSignin.vue

* Update MkSignin.vue

* wip

* Update CHANGELOG.md

* enhance(frontend): サインイン画面の改善

* Update Changelog

* 14655の変更取り込み

* spdx

* fix

* fix

* fix

* 🎨

* 🎨

* 🎨

* 🎨

* Captchaがリセットされない問題を修正

* 次の処理をsignin apiから読み取るように

* Add Comments

* fix

* fix test

* attempt to fix test

* fix test

* fix test

* fix test

* fix

* fix test

* fix: 一部のエラーがちゃんと出るように

* Update Changelog

* 🎨

* 🎨

* remove border

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-04 15:23:33 +09:00
syuilo
e344650278 Update generate.tsx 2024-10-04 13:40:49 +09:00
syuilo
1aee260398 fix test 2024-10-04 12:23:24 +09:00
syuilo
2fa805b8f7 🎨 2024-10-04 11:55:46 +09:00
syuilo
ed71b0b7d4 🎨 2024-10-04 11:27:08 +09:00
syuilo
864327b4a7 update deps 2024-10-04 11:20:56 +09:00
syuilo
c1597be458 🎨 2024-10-04 10:18:36 +09:00
かっこかり
a08a38c29a fix(test): 初期セットアップで初期パスワードを入力していないのを修正 (#14685) 2024-10-04 07:54:19 +09:00
github-actions[bot]
650e22c90d Bump version to 2024.10.0-beta.2 2024-10-03 12:47:03 +00:00
84 changed files with 2790 additions and 1240 deletions

View File

@@ -4,20 +4,28 @@
- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)
- ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
- なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能ですUI上で初期パスワードの入力欄を空欄にすると続行できます
- ユーザーデータを読み込む際の型が一部変更されました。
- `twoFactorEnabled`, `usePasswordLessLogin`, `securityKeys`: 自分とモデレーター以外のユーザーからは取得できなくなりました
### General
- Feat: サーバー初期設定時に初期パスワードを設定できるように
- Feat: 通報にモデレーションノートを残せるように
- Feat: 通報の解決種別を設定できるように
- Enhance: 通報の解決と転送を個別に行えるように
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
- Enhance: 依存関係の更新
- Enhance: l10nの更新
- Enhance: Playの「人気」タブで10件以上表示可能に #14399
- Fix: 連合のホワイトリストが正常に登録されない問題を修正
### Client
- Enhance: デザインの調整
- Enhance: ログイン画面の認証フローを改善
### Server
- Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように
- Enhance: 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるように ( #14697 )
## 2024.9.0

View File

@@ -120,11 +120,16 @@ describe('After user signup', () => {
it('signin', () => {
cy.visitHome();
cy.intercept('POST', '/api/signin').as('signin');
cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice');
// Enterキーでサインインできるかの確認も兼ねる
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
// Enterキーで続行できるかの確認も兼ねる
cy.get('[data-cy-signin-username] input').type('alice{enter}');
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
// Enterキーで続行できるかの確認も兼ねる
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.wait('@signin');
@@ -139,8 +144,9 @@ describe('After user signup', () => {
cy.visitHome();
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice');
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
cy.get('[data-cy-signin-username] input').type('alice{enter}');
// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);

View File

@@ -48,16 +48,19 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
cy.request('POST', route, {
username: username,
password: password,
...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}),
}).its('body').as(username);
});
Cypress.Commands.add('login', (username, password) => {
cy.visitHome();
cy.intercept('POST', '/api/signin').as('signin');
cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type(username);
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
cy.get('[data-cy-signin-username] input').type(`${username}{enter}`);
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
cy.wait('@signin').as('signedIn');

View File

@@ -7,8 +7,8 @@
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js';
import { commonHandlers } from '../packages/frontend/.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue';
export const Default = {
render(args) {

View File

@@ -8,6 +8,9 @@ search: "Search"
notifications: "Notifications"
username: "Username"
password: "Password"
initialPasswordForSetup: "Initial password for setup"
initialPasswordIsIncorrect: "Initial password for setup is incorrect"
initialPasswordForSetupDescription: "Use the password you entered in the configuration file if you installed Misskey yourself.\n If you are using a Misskey hosting service, use the password provided.\n If you have not set a password, leave it blank to continue."
forgotPassword: "Forgot password"
fetchingAsApObject: "Fetching from the Fediverse..."
ok: "OK"
@@ -1283,6 +1286,7 @@ signinWithPasskey: "Sign in with Passkey"
unknownWebAuthnKey: "Unknown Passkey"
passkeyVerificationFailed: "Passkey verification has failed."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
messageToFollower: "Message to followers"
_delivery:
status: "Delivery status"
stop: "Suspended"
@@ -2392,6 +2396,7 @@ _notification:
followedBySomeUsers: "Followed by {n} users"
flushNotification: "Clear notifications"
exportOfXCompleted: "Export of {x} has been completed"
login: "Someone logged in"
_types:
all: "All"
note: "New notes"

59
locales/index.d.ts vendored
View File

@@ -1834,6 +1834,10 @@ export interface Locale extends ILocale {
* モデレーションノート
*/
"moderationNote": string;
/**
* モデレーター間でだけ共有されるメモを記入することができます。
*/
"moderationNoteDescription": string;
/**
* モデレーションノートを追加する
*/
@@ -2894,22 +2898,10 @@ export interface Locale extends ILocale {
* 通報元
*/
"reporterOrigin": string;
/**
* リモートサーバーに通報を転送する
*/
"forwardReport": string;
/**
* リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。
*/
"forwardReportIsAnonymous": string;
/**
* 送信
*/
"send": string;
/**
* 対応済みにする
*/
"abuseMarkAsResolved": string;
/**
* 新しいタブで開く
*/
@@ -3714,6 +3706,10 @@ export interface Locale extends ILocale {
* パスワードが間違っています。
*/
"incorrectPassword": string;
/**
* ワンタイムパスワードが間違っているか、期限切れになっています。
*/
"incorrectTotp": string;
/**
* 「{choice}」に投票しますか?
*/
@@ -5166,6 +5162,37 @@ export interface Locale extends ILocale {
* フォロワーへのメッセージ
*/
"messageToFollower": string;
/**
* 対象
*/
"target": string;
"_abuseUserReport": {
/**
* 転送
*/
"forward": string;
/**
* 匿名のシステムアカウントとして、リモートサーバーに通報を転送します。
*/
"forwardDescription": string;
/**
* 解決
*/
"resolve": string;
/**
* 是認
*/
"accept": string;
/**
* 否認
*/
"reject": string;
/**
* 内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。
* 内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。
*/
"resolveTutorial": string;
};
"_delivery": {
/**
* 配信状態
@@ -9781,6 +9808,14 @@ export interface Locale extends ILocale {
* 通報を解決
*/
"resolveAbuseReport": string;
/**
* 通報を転送
*/
"forwardAbuseReport": string;
/**
* 通報のモデレーションノート更新
*/
"updateAbuseReportNote": string;
/**
* 招待コードを作成
*/

View File

@@ -454,6 +454,7 @@ totpDescription: "認証アプリを使ってワンタイムパスワードを
moderator: "モデレーター"
moderation: "モデレーション"
moderationNote: "モデレーションノート"
moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。"
addModerationNote: "モデレーションノートを追加する"
moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿"
@@ -719,10 +720,7 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ
reporter: "通報者"
reporteeOrigin: "通報先"
reporterOrigin: "通報元"
forwardReport: "リモートサーバーに通報を転送する"
forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
send: "送信"
abuseMarkAsResolved: "対応済みにする"
openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション"
@@ -924,6 +922,7 @@ followersVisibility: "フォロワーの公開範囲"
continueThread: "さらにスレッドを見る"
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
incorrectPassword: "パスワードが間違っています。"
incorrectTotp: "ワンタイムパスワードが間違っているか、期限切れになっています。"
voteConfirm: "「{choice}」に投票しますか?"
hide: "隠す"
useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示"
@@ -1287,6 +1286,15 @@ unknownWebAuthnKey: "登録されていないパスキーです。"
passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ"
target: "対象"
_abuseUserReport:
forward: "転送"
forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。"
resolve: "解決"
accept: "是認"
reject: "否認"
resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。"
_delivery:
status: "配信状態"
@@ -2592,6 +2600,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決"
forwardAbuseReport: "通報を転送"
updateAbuseReportNote: "通報のモデレーションノート更新"
createInvitation: "招待コードを作成"
createAd: "広告を作成"
deleteAd: "広告を削除"

View File

@@ -8,6 +8,9 @@ search: "검색"
notifications: "알림"
username: "유저명"
password: "비밀번호"
initialPasswordForSetup: "초기 설정용 비밀번호"
initialPasswordIsIncorrect: "초기 설정용 비밀번호가 올바르지 않습니다."
initialPasswordForSetupDescription: "Misskey를 직접 설치하는 경우, 설정 파일에 입력해둔 비밀번호를 사용하세요.\nMisskey 설치를 도와주는 호스팅 서비스 등을 사용하는 경우, 서비스 제공자로부터 받은 비밀번호를 사용하세요.\n비밀번호를 따로 설정하지 않은 경우, 아무것도 입력하지 않아도 됩니다."
forgotPassword: "비밀번호 재설정"
fetchingAsApObject: "연합에서 찾아보는 중"
ok: "확인"
@@ -2393,6 +2396,7 @@ _notification:
followedBySomeUsers: "{n}명에게 팔로우됨"
flushNotification: "알림 이력을 초기화"
exportOfXCompleted: "{x} 추출에 성공했습니다."
login: "로그인 알림이 있습니다"
_types:
all: "전부"
note: "사용자의 새 글"

View File

@@ -8,6 +8,9 @@ search: "搜索"
notifications: "通知"
username: "用户名"
password: "密码"
initialPasswordForSetup: "初始化密码"
initialPasswordIsIncorrect: "初始化密码不正确"
initialPasswordForSetupDescription: "如果是自己安装的 Misskey请输入配置文件里设好的密码。\n如果使用的是 Misskey 的托管服务等,请输入服务商提供的密码。\n如果没有设置密码请留空并继续。"
forgotPassword: "忘记密码"
fetchingAsApObject: "在联邦宇宙查询中..."
ok: "OK"
@@ -921,6 +924,7 @@ followersVisibility: "关注者的公开范围"
continueThread: "查看更多帖子"
deleteAccountConfirm: "将要删除账户。是否确认?"
incorrectPassword: "密码错误"
incorrectTotp: "一次性密码不正确或已过期"
voteConfirm: "确定投给 “{choice}” "
hide: "隐藏"
useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示"
@@ -2393,6 +2397,7 @@ _notification:
followedBySomeUsers: "被 {n} 人关注"
flushNotification: "重置通知历史"
exportOfXCompleted: "已完成 {x} 个导出"
login: "有新的登录"
_types:
all: "全部"
note: "用户的新帖子"

View File

@@ -8,6 +8,9 @@ search: "搜尋"
notifications: "通知"
username: "使用者名稱"
password: "密碼"
initialPasswordForSetup: "初始設定用的密碼"
initialPasswordIsIncorrect: "初始設定用的密碼錯誤。"
initialPasswordForSetupDescription: "如果您自己安裝了 Misskey請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼請將其留空並繼續。"
forgotPassword: "忘記密碼"
fetchingAsApObject: "從聯邦宇宙取得中..."
ok: "OK"
@@ -2393,6 +2396,7 @@ _notification:
followedBySomeUsers: "被{n}人追隨了"
flushNotification: "重置通知歷史紀錄"
exportOfXCompleted: "{x} 的匯出已完成。"
login: "已登入"
_types:
all: "全部 "
note: "使用者的最新貼文"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.10.0-alpha.1",
"version": "2024.10.0-beta.5",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RefineAbuseUserReport1728085812127 {
name = 'RefineAbuseUserReport1728085812127'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`);
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`);
}
}

View File

@@ -101,7 +101,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.13.2",
"bullmq": "5.15.0",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@@ -166,7 +166,7 @@
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",
"sanitize-html": "2.13.0",
"sanitize-html": "2.13.1",
"secure-json-parse": "2.7.0",
"sharp": "0.33.5",
"slacc": "0.0.10",
@@ -194,7 +194,7 @@
"@types/archiver": "6.0.2",
"@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.5",
"@types/color-convert": "2.0.3",
"@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.26",
"@types/htmlescape": "1.1.3",

View File

@@ -22,6 +22,7 @@ import { RoleService } from '@/core/RoleService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdService } from './IdService.js';
@Injectable()
@@ -42,6 +43,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
private emailService: EmailService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
private userEntityService: UserEntityService,
) {
this.redisForSub.on('message', this.onMessage);
}
@@ -135,6 +137,26 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
return;
}
const usersMap = await this.userEntityService.packMany(
[
...new Set([
...abuseReports.map(it => it.reporter ?? it.reporterId),
...abuseReports.map(it => it.targetUser ?? it.targetUserId),
...abuseReports.map(it => it.assignee ?? it.assigneeId),
].filter(x => x != null)),
],
null,
{ schema: 'UserLite' },
).then(it => new Map(it.map(it => [it.id, it])));
const convertedReports = abuseReports.map(it => {
return {
...it,
reporter: usersMap.get(it.reporterId),
targetUser: usersMap.get(it.targetUserId),
assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null,
};
});
const recipientWebhookIds = await this.fetchWebhookRecipients()
.then(it => it
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
@@ -142,7 +164,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.filter(x => x != null));
for (const webhookId of recipientWebhookIds) {
await Promise.all(
abuseReports.map(it => {
convertedReports.map(it => {
return this.systemWebhookService.enqueueSystemWebhook(
webhookId,
type,

View File

@@ -20,8 +20,10 @@ export class AbuseReportService {
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService,
@@ -77,16 +79,16 @@ export class AbuseReportService {
* - SystemWebhook
*
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
* @param operator 通報を処理したユーザ
* @param moderator 通報を処理したユーザ
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async resolve(
params: {
reportId: string;
forward: boolean;
resolvedAs: MiAbuseUserReport['resolvedAs'];
}[],
operator: MiUser,
moderator: MiUser,
) {
const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({
@@ -99,25 +101,15 @@ export class AbuseReportService {
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
assigneeId: operator.id,
forwarded: ps.forward && report.targetUserHost !== null,
assigneeId: moderator.id,
resolvedAs: ps.resolvedAs,
});
if (ps.forward && report.targetUserHost != null) {
const actor = await this.instanceActorService.getInstanceActor();
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
// eslint-disable-next-line
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
const contextAssignedFlag = this.apRendererService.addContext(flag);
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
}
this.moderationLogService
.log(operator, 'resolveAbuseReport', {
.log(moderator, 'resolveAbuseReport', {
reportId: report.id,
report: report,
forwarded: ps.forward && report.targetUserHost !== null,
resolvedAs: ps.resolvedAs,
})
.then();
}
@@ -125,4 +117,58 @@ export class AbuseReportService {
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
}
@bindThis
public async forward(
reportId: MiAbuseUserReport['id'],
moderator: MiUser,
) {
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
if (report.targetUserHost == null) {
throw new Error('The target user host is null.');
}
await this.abuseUserReportsRepository.update(report.id, {
forwarded: true,
});
const actor = await this.instanceActorService.getInstanceActor();
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
const contextAssignedFlag = this.apRendererService.addContext(flag);
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
this.moderationLogService
.log(moderator, 'forwardAbuseReport', {
reportId: report.id,
report: report,
})
.then();
}
@bindThis
public async update(
reportId: MiAbuseUserReport['id'],
params: {
moderationNote?: MiAbuseUserReport['moderationNote'];
},
moderator: MiUser,
) {
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
await this.abuseUserReportsRepository.update(report.id, {
moderationNote: params.moderationNote,
});
if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
reportId: report.id,
report: report,
before: report.moderationNote,
after: params.moderationNote,
});
}
}
}

View File

@@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
import { WebhookTestService } from '@/core/WebhookTestService.js';
import { FlashService } from '@/core/FlashService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -217,6 +218,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@@ -367,6 +369,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService,
UtilityService,
FileInfoService,
FlashService,
SearchService,
ClipService,
FeaturedService,
@@ -513,6 +516,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookTestService,
$UtilityService,
$FileInfoService,
$FlashService,
$SearchService,
$ClipService,
$FeaturedService,
@@ -660,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService,
UtilityService,
FileInfoService,
FlashService,
SearchService,
ClipService,
FeaturedService,

View File

@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { type FlashsRepository } from '@/models/_.js';
/**
* MisskeyPlay関係のService
*/
@Injectable()
export class FlashService {
constructor(
@Inject(DI.flashsRepository)
private flashRepository: FlashsRepository,
) {
}
/**
* 人気のあるPlay一覧を取得する.
*/
public async featured(opts?: { offset?: number, limit: number }) {
const builder = this.flashRepository.createQueryBuilder('flash')
.andWhere('flash.likedCount > 0')
.andWhere('flash.visibility = :visibility', { visibility: 'public' })
.addOrderBy('flash.likedCount', 'DESC')
.addOrderBy('flash.updatedAt', 'DESC')
.addOrderBy('flash.id', 'DESC');
if (opts?.offset) {
builder.skip(opts.offset);
}
builder.take(opts?.limit ?? 10);
return await builder.getMany();
}
}

View File

@@ -218,7 +218,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private utilityService: UtilityService,
private userBlockingService: UserBlockingService,
) {
this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
}
@bindThis

View File

@@ -15,8 +15,14 @@ import { QueueService } from '@/core/QueueService.js';
const oneDayMillis = 24 * 60 * 60 * 1000;
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
return {
type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
targetUser: Packed<'UserLite'> | null,
reporter: Packed<'UserLite'> | null,
assignee: Packed<'UserLite'> | null,
};
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user',
targetUser: null,
@@ -29,8 +35,17 @@ function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUser
comment: 'This is a dummy report for testing purposes.',
targetUserHost: null,
reporterHost: null,
resolvedAs: null,
moderationNote: 'foo',
...override,
};
return {
...result,
targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
};
}
function generateDummyUser(override?: Partial<MiUser>): MiUser {
@@ -268,7 +283,8 @@ const dummyUser3 = generateDummyUser({
@Injectable()
export class WebhookTestService {
public static NoSuchWebhookError = class extends Error {};
public static NoSuchWebhookError = class extends Error {
};
constructor(
private userWebhookService: UserWebhookService,

View File

@@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
resolvedAs: report.resolvedAs,
moderationNote: report.moderationNote,
});
}

View File

@@ -5,10 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFlash } from '@/models/Flash.js';
import { bindThis } from '@/decorators.js';
@@ -20,10 +18,8 @@ export class FlashEntityService {
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
@Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository,
private userEntityService: UserEntityService,
private idService: IdService,
) {
@@ -34,25 +30,36 @@ export class FlashEntityService {
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
packedUser?: Packed<'UserLite'>,
likedFlashIds?: MiFlash['id'][],
},
): Promise<Packed<'Flash'>> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
return await awaitAll({
// { schema: 'UserDetailed' } すると無限ループするので注意
const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
let isLiked = false;
if (meId) {
isLiked = hint?.likedFlashIds
? hint.likedFlashIds.includes(flash.id)
: await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
}
return {
id: flash.id,
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
user: user,
title: flash.title,
summary: flash.summary,
script: flash.script,
visibility: flash.visibility,
likedCount: flash.likedCount,
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
});
isLiked: isLiked,
};
}
@bindThis
@@ -63,7 +70,19 @@ export class FlashEntityService {
const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
const _likedFlashIds = me
? await this.flashLikesRepository.createQueryBuilder('flashLike')
.select('flashLike.flashId')
.where('flashLike.userId = :userId', { userId: me.id })
.getRawMany<{ flashLike_flashId: string }>()
.then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
: [];
return Promise.all(
flashes.map(flash => this.pack(flash, me, {
packedUser: _userMap.get(flash.userId),
likedFlashIds: _likedFlashIds,
})),
);
}
}

View File

@@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit {
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
name: role.name,
@@ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
...(isDetailed && (isMe || iAmModerator) ? {
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
} : {}),
...(isDetailed && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,

View File

@@ -50,6 +50,9 @@ export class MiAbuseUserReport {
})
public resolved: boolean;
/**
* リモートサーバーに転送したかどうか
*/
@Column('boolean', {
default: false,
})
@@ -60,6 +63,21 @@ export class MiAbuseUserReport {
})
public comment: string;
@Column('varchar', {
length: 8192, default: '',
})
public moderationNote: string;
/**
* accept 是認 ... 通報内容が正当であり、肯定的に対応された
* reject 否認 ... 通報内容が正当でなく、否定的に対応された
* null ... その他
*/
@Column('varchar', {
length: 128, nullable: true,
})
public resolvedAs: 'accept' | 'reject' | null;
//#region Denormalized fields
@Index()
@Column('varchar', {

View File

@@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
import { id } from './util/id.js';
import { MiUser } from './User.js';
export const flashVisibility = ['public', 'private'] as const;
export type FlashVisibility = typeof flashVisibility[number];
@Entity('flash')
export class MiFlash {
@PrimaryColumn(id())
@@ -63,5 +66,5 @@ export class MiFlash {
@Column('varchar', {
length: 512, default: 'public',
})
public visibility: 'public' | 'private';
public visibility: FlashVisibility;
}

View File

@@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
usePasswordLessLogin: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
securityKeys: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
roles: {
type: 'array',
nullable: false, optional: false,
@@ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string',
nullable: false, optional: true,
},
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: true,
},
usePasswordLessLogin: {
type: 'boolean',
nullable: false, optional: true,
},
securityKeys: {
type: 'boolean',
nullable: false, optional: true,
},
//#region relations
isFollowing: {
type: 'boolean',
@@ -630,6 +627,21 @@ export const packedMeDetailedOnlySchema = {
nullable: false, optional: false,
ref: 'RolePolicies',
},
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
usePasswordLessLogin: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
securityKeys: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
//#region secrets
email: {
type: 'string',

View File

@@ -59,7 +59,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
}
@bindThis

View File

@@ -125,7 +125,7 @@ export class ApiServerService {
fastify.post<{
Body: {
username: string;
password: string;
password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string;
@@ -133,7 +133,7 @@ export class ApiServerService {
'turnstile-response'?: string;
'm-captcha-response'?: string;
};
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
fastify.post<{
Body: {

View File

@@ -68,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -453,6 +455,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass
const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@@ -842,6 +846,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
$admin_forwardAbuseUserReport,
$admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
@@ -1225,6 +1231,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
$admin_forwardAbuseUserReport,
$admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,

View File

@@ -5,13 +5,14 @@
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm';
import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
import type {
MiMeta,
SigninsRepository,
UserProfilesRepository,
UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
@@ -43,6 +44,9 @@ export class SigninApiService {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository,
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
@@ -60,7 +64,7 @@ export class SigninApiService {
request: FastifyRequest<{
Body: {
username: string;
password: string;
password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string;
@@ -103,11 +107,6 @@ export class SigninApiService {
return;
}
if (typeof password !== 'string') {
reply.code(400);
return;
}
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
@@ -132,11 +131,32 @@ export class SigninApiService {
}
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
if (password == null) {
reply.code(200);
if (profile.twoFactorEnabled) {
return {
finished: false,
next: 'password',
} satisfies Misskey.entities.SigninFlowResponse;
} else {
return {
finished: false,
next: 'captcha',
} satisfies Misskey.entities.SigninFlowResponse;
}
}
if (typeof password !== 'string') {
reply.code(400);
return;
}
// Compare password
const same = await bcrypt.compare(password, profile.password!);
const fail = async (status?: number, failure?: { id: string }) => {
const fail = async (status?: number, failure?: { id: string; }) => {
// Append signin history
await this.signinsRepository.insert({
id: this.idService.gen(),
@@ -217,7 +237,7 @@ export class SigninApiService {
id: '93b86c4b-72f9-40eb-9815-798928603d1e',
});
}
} else {
} else if (securityKeysAvailable) {
if (!same && !profile.usePasswordLessLogin) {
return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
@@ -227,7 +247,23 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
reply.code(200);
return authRequest;
return {
finished: false,
next: 'passkey',
authRequest,
} satisfies Misskey.entities.SigninFlowResponse;
} else {
if (!same || !profile.twoFactorEnabled) {
return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
} else {
reply.code(200);
return {
finished: false,
next: 'totp',
} satisfies Misskey.entities.SigninFlowResponse;
}
}
// never get here
}

View File

@@ -4,6 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
@@ -57,9 +58,10 @@ export class SigninService {
reply.code(200);
return {
finished: true,
id: user.id,
i: user.token,
};
i: user.token!,
} satisfies Misskey.entities.SigninFlowResponse;
}
}

View File

@@ -74,6 +74,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -457,6 +459,8 @@ const eps = [
['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
['admin/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo],
['admin/show-moderation-logs', ep___admin_showModerationLogs],

View File

@@ -0,0 +1,55 @@
/*
* 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 { AbuseUserReportsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:resolve-abuse-user-report',
errors: {
noSuchAbuseReport: {
message: 'No such abuse report.',
code: 'NO_SUCH_ABUSE_REPORT',
id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
kind: 'server',
httpStatusCode: 404,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
reportId: { type: 'string', format: 'misskey:id' },
},
required: ['reportId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
if (!report) {
throw new ApiError(meta.errors.noSuchAbuseReport);
}
await this.abuseReportService.forward(report.id, me);
});
}
}

View File

@@ -32,7 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
reportId: { type: 'string', format: 'misskey:id' },
forward: { type: 'boolean', default: false },
resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
},
required: ['reportId'],
} as const;
@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchAbuseReport);
}
await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
});
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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 { AbuseUserReportsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:resolve-abuse-user-report',
errors: {
noSuchAbuseReport: {
message: 'No such abuse report.',
code: 'NO_SUCH_ABUSE_REPORT',
id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
kind: 'server',
httpStatusCode: 404,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
reportId: { type: 'string', format: 'misskey:id' },
moderationNote: { type: 'string' },
},
required: ['reportId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
if (!report) {
throw new ApiError(meta.errors.noSuchAbuseReport);
}
await this.abuseReportService.update(report.id, {
moderationNote: ps.moderationNote,
}, me);
});
}
}

View File

@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js';
import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['flash'],
@@ -27,26 +28,25 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {},
properties: {
offset: { type: 'integer', minimum: 0, default: 0 },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.flashsRepository.createQueryBuilder('flash')
.andWhere('flash.likedCount > 0')
.orderBy('flash.likedCount', 'DESC');
const flashs = await query.limit(10).getMany();
return await this.flashEntityService.packMany(flashs, me);
const result = await this.flashService.featured({
offset: ps.offset,
limit: ps.limit,
});
return await this.flashEntityService.packMany(result, me);
});
}
}

View File

@@ -99,6 +99,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
'forwardAbuseReport',
'updateAbuseReportNote',
'createInvitation',
'createAd',
'updateAd',
@@ -267,7 +269,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: {
reportId: string;
report: any;
forwarded: boolean;
forwarded?: boolean;
resolvedAs?: string | null;
};
forwardAbuseReport: {
reportId: string;
report: any;
};
updateAbuseReportNote: {
reportId: string;
report: any;
before: string;
after: string;
};
createInvitation: {
invitations: any[];

View File

@@ -136,13 +136,7 @@ describe('2要素認証', () => {
keyName: string,
credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON,
}): {
username: string,
password: string,
credential: AuthenticationResponseJSON,
'g-recaptcha-response'?: string | null,
'hcaptcha-response'?: string | null,
} => {
}): misskey.entities.SigninFlowRequest => {
// AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const authenticatorData = Buffer.concat([
@@ -202,17 +196,21 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('users/show', {
username,
}, alice);
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
const signinWithoutTokenResponse = await api('signin-flow', {
...signinParam(),
});
assert.strictEqual(signinWithoutTokenResponse.status, 200);
assert.deepStrictEqual(signinWithoutTokenResponse.body, {
finished: false,
next: 'totp',
});
const signinResponse = await api('signin', {
const signinResponse = await api('signin-flow', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
@@ -253,27 +251,23 @@ describe('2要素認証', () => {
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
const signinResponse = await api('signin', {
const signinResponse = await api('signin-flow', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined);
assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
assert.strictEqual(signinResponse.body.finished, false);
assert.strictEqual(signinResponse.body.next, 'passkey');
assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({
keyName,
credentialId,
requestOptions: signinResponse.body,
} as any));
requestOptions: signinResponse.body.authRequest,
}));
assert.strictEqual(signinResponse2.status, 200);
assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
@@ -315,28 +309,30 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(passwordLessResponse.status, 204);
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
const iResponse = await api('i', {}, alice);
assert.strictEqual(iResponse.status, 200);
assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
const signinResponse = await api('signin', {
const signinResponse = await api('signin-flow', {
...signinParam(),
password: '',
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined);
assert.strictEqual(signinResponse.body.finished, false);
assert.strictEqual(signinResponse.body.next, 'passkey');
assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
const signinResponse2 = await api('signin', {
const signinResponse2 = await api('signin-flow', {
...signinWithSecurityKeyParam({
keyName,
credentialId,
requestOptions: signinResponse.body,
requestOptions: signinResponse.body.authRequest,
} as any),
password: '',
});
assert.strictEqual(signinResponse2.status, 200);
assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
@@ -424,11 +420,11 @@ describe('2要素認証', () => {
assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す
const iResponse = await api('i', {
const beforeIResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
assert.ok(iResponse.body.securityKeysList);
for (const key of iResponse.body.securityKeysList) {
assert.strictEqual(beforeIResponse.status, 200);
assert.ok(beforeIResponse.body.securityKeysList);
for (const key of beforeIResponse.body.securityKeysList) {
const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret),
password,
@@ -437,17 +433,16 @@ describe('2要素認証', () => {
assert.strictEqual(removeKeyResponse.status, 200);
}
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
const afterIResponse = await api('i', {}, alice);
assert.strictEqual(afterIResponse.status, 200);
assert.strictEqual(afterIResponse.body.securityKeys, false);
const signinResponse = await api('signin', {
const signinResponse = await api('signin-flow', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
@@ -468,11 +463,9 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
const iResponse = await api('i', {}, alice);
assert.strictEqual(iResponse.status, 200);
assert.strictEqual(iResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
@@ -480,10 +473,11 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
const signinResponse = await api('signin', {
const signinResponse = await api('signin-flow', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け

View File

@@ -66,9 +66,9 @@ describe('Endpoints', () => {
});
});
describe('signin', () => {
describe('signin-flow', () => {
test('間違ったパスワードでサインインできない', async () => {
const res = await api('signin', {
const res = await api('signin-flow', {
username: 'test1',
password: 'bar',
});
@@ -77,7 +77,7 @@ describe('Endpoints', () => {
});
test('クエリをインジェクションできない', async () => {
const res = await api('signin', {
const res = await api('signin-flow', {
username: 'test1',
// @ts-expect-error password must be string
password: {
@@ -89,7 +89,7 @@ describe('Endpoints', () => {
});
test('正しい情報でサインインできる', async () => {
const res = await api('signin', {
const res = await api('signin-flow', {
username: 'test1',
password: 'test1',
});

View File

@@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
forward: false,
}, admin);
});
@@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
forward: false,
}, admin);
});
@@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
forward: false,
}, admin);
}).catch(e => e.message);
@@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
forward: false,
}, admin);
}).catch(e => e.message);
@@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
forward: false,
}, admin);
}).catch(e => e.message);
@@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
forward: false,
}, admin);
}).catch(e => e.message);

View File

@@ -83,9 +83,6 @@ describe('ユーザー', () => {
publicReactions: user.publicReactions,
followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility,
twoFactorEnabled: user.twoFactorEnabled,
usePasswordLessLogin: user.usePasswordLessLogin,
securityKeys: user.securityKeys,
roles: user.roles,
memo: user.memo,
});
@@ -149,6 +146,9 @@ describe('ユーザー', () => {
achievements: user.achievements,
loggedInDays: user.loggedInDays,
policies: user.policies,
twoFactorEnabled: user.twoFactorEnabled,
usePasswordLessLogin: user.usePasswordLessLogin,
securityKeys: user.securityKeys,
...(security ? {
email: user.email,
emailVerified: user.emailVerified,
@@ -343,9 +343,6 @@ describe('ユーザー', () => {
assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, 'public');
assert.strictEqual(response.twoFactorEnabled, false);
assert.strictEqual(response.usePasswordLessLogin, false);
assert.strictEqual(response.securityKeys, false);
assert.deepStrictEqual(response.roles, []);
assert.strictEqual(response.memo, null);
@@ -385,6 +382,9 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.achievements, []);
assert.deepStrictEqual(response.loggedInDays, 0);
assert.deepStrictEqual(response.policies, DEFAULT_POLICIES);
assert.strictEqual(response.twoFactorEnabled, false);
assert.strictEqual(response.usePasswordLessLogin, false);
assert.strictEqual(response.securityKeys, false);
assert.notStrictEqual(response.email, undefined);
assert.strictEqual(response.emailVerified, false);
assert.deepStrictEqual(response.securityKeysList, []);
@@ -618,6 +618,9 @@ describe('ユーザー', () => {
{ label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
// @ts-expect-error UserDetailedNotMe doesn't include isModerator
{ label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
{ label: '自分から見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => alice, selector: (user: misskey.entities.MeDetailed) => user.twoFactorEnabled, expected: () => false },
{ label: '自分以外から見た場合に二要素認証関連のプロパティがセットされていない', user: () => alice, me: () => bob, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => undefined },
{ label: 'モデレーターから見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => false },
{ label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
// FIXME: 落ちる
//{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },

View File

@@ -5,6 +5,7 @@
import { jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import { randomString } from '../utils.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import {
AbuseReportNotificationRecipientRepository,
@@ -25,7 +26,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { randomString } from '../utils.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
process.env.NODE_ENV = 'test';
@@ -110,6 +111,9 @@ describe('AbuseReportNotificationService', () => {
{
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
},
{
provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
},
{
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
},

View File

@@ -0,0 +1,152 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Test, TestingModule } from '@nestjs/testing';
import { FlashService } from '@/core/FlashService.js';
import { IdService } from '@/core/IdService.js';
import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GlobalModule } from '@/GlobalModule.js';
describe('FlashService', () => {
let app: TestingModule;
let service: FlashService;
// --------------------------------------------------------------------------------------
let flashsRepository: FlashsRepository;
let usersRepository: UsersRepository;
let userProfilesRepository: UserProfilesRepository;
let idService: IdService;
// --------------------------------------------------------------------------------------
let root: MiUser;
let alice: MiUser;
let bob: MiUser;
// --------------------------------------------------------------------------------------
async function createFlash(data: Partial<MiFlash>) {
return flashsRepository.insert({
id: idService.gen(),
updatedAt: new Date(),
userId: root.id,
title: 'title',
summary: 'summary',
script: 'script',
permissions: [],
likedCount: 0,
...data,
}).then(x => flashsRepository.findOneByOrFail(x.identifiers[0]));
}
async function createUser(data: Partial<MiUser> = {}) {
const user = await usersRepository
.insert({
id: idService.gen(),
...data,
})
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
await userProfilesRepository.insert({
userId: user.id,
});
return user;
}
// --------------------------------------------------------------------------------------
beforeEach(async () => {
app = await Test.createTestingModule({
imports: [
GlobalModule,
],
providers: [
FlashService,
IdService,
],
}).compile();
service = app.get(FlashService);
flashsRepository = app.get(DI.flashsRepository);
usersRepository = app.get(DI.usersRepository);
userProfilesRepository = app.get(DI.userProfilesRepository);
idService = app.get(IdService);
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
});
afterEach(async () => {
await usersRepository.delete({});
await userProfilesRepository.delete({});
await flashsRepository.delete({});
});
afterAll(async () => {
await app.close();
});
// --------------------------------------------------------------------------------------
describe('featured', () => {
test('should return featured flashes', async () => {
const flash1 = await createFlash({ likedCount: 1 });
const flash2 = await createFlash({ likedCount: 2 });
const flash3 = await createFlash({ likedCount: 3 });
const result = await service.featured({
offset: 0,
limit: 10,
});
expect(result).toEqual([flash3, flash2, flash1]);
});
test('should return featured flashes public visibility only', async () => {
const flash1 = await createFlash({ likedCount: 1, visibility: 'public' });
const flash2 = await createFlash({ likedCount: 2, visibility: 'public' });
const flash3 = await createFlash({ likedCount: 3, visibility: 'private' });
const result = await service.featured({
offset: 0,
limit: 10,
});
expect(result).toEqual([flash2, flash1]);
});
test('should return featured flashes with offset', async () => {
const flash1 = await createFlash({ likedCount: 1 });
const flash2 = await createFlash({ likedCount: 2 });
const flash3 = await createFlash({ likedCount: 3 });
const result = await service.featured({
offset: 1,
limit: 10,
});
expect(result).toEqual([flash2, flash1]);
});
test('should return featured flashes with limit', async () => {
const flash1 = await createFlash({ likedCount: 1 });
const flash2 = await createFlash({ likedCount: 2 });
const flash3 = await createFlash({ likedCount: 3 });
const result = await service.featured({
offset: 0,
limit: 2,
});
expect(result).toEqual([flash3, flash2]);
});
});
});

View File

@@ -18,7 +18,7 @@
"@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.10",
"@vue/compiler-sfc": "3.5.11",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@@ -27,8 +27,8 @@
"frontend-shared": "workspace:*",
"punycode": "2.3.1",
"rollup": "4.22.5",
"sass": "1.79.3",
"shiki": "1.12.0",
"sass": "1.79.4",
"shiki": "1.21.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
@@ -36,7 +36,7 @@
"uuid": "10.0.0",
"json5": "2.2.3",
"vite": "5.4.8",
"vue": "3.5.10"
"vue": "3.5.11"
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
@@ -51,10 +51,10 @@
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.10",
"@vue/runtime-core": "3.5.11",
"acorn": "8.12.1",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.28.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",

View File

@@ -38,8 +38,6 @@ const props = defineProps<{
host?: string | null;
url?: string;
useOriginalSize?: boolean;
menu?: boolean;
menuReaction?: boolean;
fallbackToImage?: boolean;
}>();

View File

@@ -6,6 +6,7 @@
import { VNode, h, SetupContext, provide } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import EmUrl from '@/components/EmUrl.vue';
import EmTime from '@/components/EmTime.vue';
import EmLink from '@/components/EmLink.vue';
@@ -13,7 +14,6 @@ import EmMention from '@/components/EmMention.vue';
import EmEmoji from '@/components/EmEmoji.vue';
import EmCustomEmoji from '@/components/EmCustomEmoji.vue';
import EmA from '@/components/EmA.vue';
import { host } from '@@/js/config.js';
function safeParseFloat(str: unknown): number | null {
if (typeof str !== 'string' || str === '') return null;
@@ -41,9 +41,6 @@ type MfmProps = {
rootScale?: number;
nyaize?: boolean | 'respect';
parsedNodes?: mfm.MfmNode[] | null;
enableEmojiMenu?: boolean;
enableEmojiMenuReaction?: boolean;
linkNavigationBehavior?: string;
};
type MfmEvents = {
@@ -52,8 +49,6 @@ type MfmEvents = {
// eslint-disable-next-line import/no-default-export
export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
provide('linkNavigationBehavior', props.linkNavigationBehavior);
const isNote = props.isNote ?? true;
const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
@@ -397,8 +392,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
normal: props.plain,
host: null,
useOriginalSize: scale >= 2.5,
menu: props.enableEmojiMenu,
menuReaction: props.enableEmojiMenuReaction,
fallbackToImage: false,
})];
} else {

View File

@@ -397,7 +397,18 @@ function toStories(component: string): Promise<string> {
const globs = await Promise.all([
glob('src/components/global/Mk*.vue'),
glob('src/components/global/RouterView.vue'),
glob('src/components/Mk[A-E]*.vue'),
glob('src/components/MkAbuseReportWindow.vue'),
glob('src/components/MkAccountMoved.vue'),
glob('src/components/MkAchievements.vue'),
glob('src/components/MkAnalogClock.vue'),
glob('src/components/MkAnimBg.vue'),
glob('src/components/MkAnnouncementDialog.vue'),
glob('src/components/MkAntennaEditor.vue'),
glob('src/components/MkAntennaEditorDialog.vue'),
glob('src/components/MkAsUi.vue'),
glob('src/components/MkAutocomplete.vue'),
glob('src/components/MkAvatars.vue'),
glob('src/components/Mk[B-E]*.vue'),
glob('src/components/MkFlashPreview.vue'),
glob('src/components/MkGalleryPostPreview.vue'),
glob('src/components/MkSignupServerRules.vue'),

View File

@@ -28,7 +28,7 @@
"@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.10",
"@vue/compiler-sfc": "3.5.11",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
"astring": "1.9.0",
"broadcast-channel": "7.0.0",
@@ -39,12 +39,13 @@
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
"chromatic": "11.10.4",
"chromatic": "11.11.0",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.2",
"date-fns": "2.30.0",
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
"frontend-shared": "workspace:*",
"idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
@@ -54,11 +55,10 @@
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"frontend-shared": "workspace:*",
"photoswipe": "5.4.4",
"punycode": "2.3.1",
"rollup": "4.22.5",
"sanitize-html": "2.13.0",
"sanitize-html": "2.13.1",
"sass": "1.79.3",
"shiki": "1.21.0",
"strict-event-emitter-types": "2.0.0",
@@ -72,30 +72,31 @@
"uuid": "10.0.0",
"v-code-diff": "1.13.1",
"vite": "5.4.8",
"vue": "3.5.10",
"vue": "3.5.11",
"vuedraggable": "next"
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.3.3",
"@storybook/addon-essentials": "8.3.3",
"@storybook/addon-interactions": "8.3.3",
"@storybook/addon-links": "8.3.3",
"@storybook/addon-mdx-gfm": "8.3.3",
"@storybook/addon-storysource": "8.3.3",
"@storybook/blocks": "8.3.3",
"@storybook/components": "8.3.3",
"@storybook/core-events": "8.3.3",
"@storybook/manager-api": "8.3.3",
"@storybook/preview-api": "8.3.3",
"@storybook/react": "8.3.3",
"@storybook/react-vite": "8.3.3",
"@storybook/test": "8.3.3",
"@storybook/theming": "8.3.3",
"@storybook/types": "8.3.3",
"@storybook/vue3": "8.3.3",
"@storybook/vue3-vite": "8.3.3",
"@storybook/addon-actions": "8.3.4",
"@storybook/addon-essentials": "8.3.4",
"@storybook/addon-interactions": "8.3.4",
"@storybook/addon-links": "8.3.4",
"@storybook/addon-mdx-gfm": "8.3.4",
"@storybook/addon-storysource": "8.3.4",
"@storybook/blocks": "8.3.4",
"@storybook/components": "8.3.4",
"@storybook/core-events": "8.3.4",
"@storybook/manager-api": "8.3.4",
"@storybook/preview-api": "8.3.4",
"@storybook/react": "8.3.4",
"@storybook/react-vite": "8.3.4",
"@storybook/test": "8.3.4",
"@storybook/theming": "8.3.4",
"@storybook/types": "8.3.4",
"@storybook/vue3": "8.3.4",
"@storybook/vue3-vite": "8.3.4",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "^1.6.4",
"@types/estree": "1.0.6",
"@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9",
@@ -110,11 +111,11 @@
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.10",
"@vue/runtime-core": "3.5.11",
"acorn": "8.12.1",
"cross-env": "7.0.3",
"cypress": "13.15.0",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.28.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
@@ -128,7 +129,7 @@
"react-dom": "18.3.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.8",
"storybook": "8.3.3",
"storybook": "8.3.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "1.6.0",

View File

@@ -4,112 +4,153 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="bcekxzvu _margin _panel">
<div class="target">
<MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'">
<MkAvatar class="avatar" :user="report.targetUser" indicator/>
<div class="names">
<MkUserName class="name" :user="report.targetUser"/>
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
</div>
</MkA>
<MkKeyValue>
<template #key>{{ i18n.ts.registeredDate }}</template>
<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
</MkKeyValue>
</div>
<div class="detail">
<div>
<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
<MkFolder>
<template #icon>
<i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--success)"></i>
<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--error)"></i>
<i v-else-if="report.resolved" class="ti ti-slash"></i>
<i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i>
</template>
<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
<template #caption>{{ report.comment }}</template>
<template #suffix><MkTime :time="report.createdAt"/></template>
<template #footer>
<div class="_buttons">
<template v-if="!report.resolved">
<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
<MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
</template>
<template v-if="report.targetUser.host == null">
<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
</template>
<button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div>
<hr/>
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>
</template>
<div :class="$style.root" class="_gaps_s">
<MkFolder :withSpacer="false">
<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
<template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
<div style="container-type: inline-size;">
<RouterView :router="targetRouter"/>
</div>
</MkFolder>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-message-2"></i></template>
<template #label>{{ i18n.ts.details }}</template>
<div class="_gaps_s">
<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
</div>
</MkFolder>
<MkFolder :withSpacer="false">
<template #icon><MkAvatar :user="report.reporter" style="width: 18px; height: 18px;"/></template>
<template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template>
<template #suffix>#{{ report.reporterId.toUpperCase() }}</template>
<div style="container-type: inline-size;">
<RouterView :router="reporterRouter"/>
</div>
</MkFolder>
<MkFolder :defaultOpen="false">
<template #icon><i class="ti ti-message-2"></i></template>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template>
<div class="_gaps_s">
<MkTextarea v-model="moderationNote" manualSave>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
</div>
</MkFolder>
<div v-if="report.assignee">
{{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/>
</div>
<div><MkTime :time="report.createdAt"/></div>
<div class="action">
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
{{ i18n.ts.forwardReport }}
<template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
</MkSwitch>
<MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
</div>
</div>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { provide, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkFolder from '@/components/MkFolder.vue';
import RouterView from '@/components/global/RouterView.vue';
import { useRouterFactory } from '@/router/supplier';
import MkTextarea from '@/components/MkTextarea.vue';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
report: any;
report: Misskey.entities.AdminAbuseUserReportsResponse[number];
}>();
const emit = defineEmits<{
(ev: 'resolved', reportId: string): void;
}>();
const forward = ref(props.report.forwarded);
const routerFactory = useRouterFactory();
const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`);
targetRouter.init();
const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
reporterRouter.init();
function resolve() {
os.apiWithDialog('admin/resolve-abuse-user-report', {
forward: forward.value,
const moderationNote = ref(props.report.moderationNote ?? '');
watch(moderationNote, async () => {
os.apiWithDialog('admin/update-abuse-user-report', {
reportId: props.report.id,
moderationNote: moderationNote.value,
}).then(() => {
});
});
function resolve(resolvedAs) {
os.apiWithDialog('admin/resolve-abuse-user-report', {
reportId: props.report.id,
resolvedAs,
}).then(() => {
emit('resolved', props.report.id);
});
}
function forward() {
os.apiWithDialog('admin/forward-abuse-user-report', {
reportId: props.report.id,
}).then(() => {
});
}
function showMenu(ev: MouseEvent) {
os.popupMenu([{
icon: 'ti ti-id',
text: 'Copy ID',
action: () => {
copyToClipboard(props.report.id);
},
}, {
icon: 'ti ti-json',
text: 'Copy JSON',
action: () => {
copyToClipboard(JSON.stringify(props.report, null, '\t'));
},
}], ev.currentTarget ?? ev.target);
}
</script>
<style lang="scss" scoped>
.bcekxzvu {
display: flex;
> .target {
width: 35%;
box-sizing: border-box;
text-align: left;
padding: 24px;
border-right: solid 1px var(--divider);
> .info {
display: flex;
box-sizing: border-box;
align-items: center;
padding: 14px;
border-radius: 8px;
--c: rgb(255 196 0 / 15%);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
> .avatar {
width: 42px;
height: 42px;
}
> .names {
margin-left: 0.3em;
padding: 0 8px;
flex: 1;
> .name {
font-weight: bold;
}
}
}
}
> .detail {
flex: 1;
padding: 24px;
}
<style lang="scss" module>
.root {
}
</style>

View File

@@ -38,9 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<KeepAlive>
<div v-show="opened">
<MkSpacer :marginMin="14" :marginMax="22">
<MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22">
<slot></slot>
</MkSpacer>
<div v-else>
<slot></slot>
</div>
<div v-if="$slots.footer" :class="$style.footer">
<slot name="footer"></slot>
</div>
@@ -59,9 +62,11 @@ import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
defaultOpen?: boolean;
maxHeight?: number | null;
withSpacer?: boolean;
}>(), {
defaultOpen: false,
maxHeight: null,
withSpacer: true,
});
const getBgColor = (el: HTMLElement) => {

View File

@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[
$style.root,
tail === 'left' ? $style.left : $style.right,
negativeMargin === true && $style.negativeMergin,
negativeMargin === true && $style.negativeMargin,
shadow === true && $style.shadow,
]"
>
@@ -54,7 +54,7 @@ withDefaults(defineProps<{
&.left {
padding-left: calc(var(--fukidashi-radius) * .13);
&.negativeMergin {
&.negativeMargin {
margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}
@@ -62,7 +62,7 @@ withDefaults(defineProps<{
&.right {
padding-right: calc(var(--fukidashi-radius) * .13);
&.negativeMergin {
&.negativeMargin {
margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}

View File

@@ -437,9 +437,11 @@ onBeforeUnmount(() => {
&.big:not(.asDrawer) {
> .menu {
min-width: 230px;
> .item {
padding: 6px 20px;
font-size: 1em;
font-size: 0.95em;
line-height: 24px;
}
}

View File

@@ -0,0 +1,206 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.wrapper" data-cy-signin-page-input>
<div :class="$style.root">
<div :class="$style.avatar">
<i class="ti ti-user"></i>
</div>
<!-- ログイン画面メッセージ -->
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<!-- 外部サーバーへの転送 -->
<div v-if="openOnRemote" class="_gaps_m">
<div class="_gaps_s">
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
</MkButton>
<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
{{ i18n.ts.specifyServerHost }}
</button>
</div>
<div :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
</div>
<!-- username入力 -->
<form class="_gaps_s" @submit.prevent="emit('usernameSubmitted', username)">
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkButton type="submit" large primary rounded style="margin: 0 auto;" data-cy-signin-page-input-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</form>
<!-- パスワードレスログイン -->
<div :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
<div>
<MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)">
<i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }}
</MkButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { toUnicode } from 'punycode/';
import { query, extractDomain } from '@@/js/url.js';
import { host as configHost } from '@@/js/config.js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
const props = withDefaults(defineProps<{
message?: string,
openOnRemote?: OpenOnRemoteOptions,
}>(), {
message: '',
openOnRemote: undefined,
});
const emit = defineEmits<{
(ev: 'usernameSubmitted', v: string): void;
(ev: 'passkeyClick', v: MouseEvent): void;
}>();
const host = toUnicode(configHost);
const username = ref('');
//#region Open on remote
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
switch (options.type) {
case 'web':
case 'lookup': {
let _path: string;
if (options.type === 'lookup') {
// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
} else {
_path = options.path;
}
if (targetHost) {
window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
}
break;
}
case 'share': {
const params = query(options.params);
if (targetHost) {
window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
}
break;
}
}
}
async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
const { canceled, result: hostTemp } = await os.inputText({
title: i18n.ts.inputHostName,
placeholder: 'misskey.example.com',
});
if (canceled) return;
let targetHost: string | null = hostTemp;
// ドメイン部分だけを取り出す
targetHost = extractDomain(targetHost ?? '');
if (targetHost == null) {
os.alert({
type: 'error',
title: i18n.ts.invalidValue,
text: i18n.ts.tryAgain,
});
return;
}
openRemote(options, targetHost);
}
//#endregion
</script>
<style lang="scss" module>
.root {
display: flex;
flex-direction: column;
gap: 20px;
}
.wrapper {
display: flex;
align-items: center;
width: 100%;
min-height: 336px;
> .root {
width: 100%;
}
}
.avatar {
margin: 0 auto;
background-color: color-mix(in srgb, var(--fg), transparent 85%);
color: color-mix(in srgb, var(--fg), transparent 25%);
text-align: center;
height: 64px;
width: 64px;
font-size: 24px;
line-height: 64px;
border-radius: 50%;
}
.instanceManualSelectButton {
display: block;
text-align: center;
opacity: .7;
font-size: .8em;
&:hover {
text-decoration: underline;
}
}
.orHr {
position: relative;
margin: .4em auto;
width: 100%;
height: 1px;
background: var(--divider);
}
.orMsg {
position: absolute;
top: -.6em;
display: inline-block;
padding: 0 1em;
background: var(--panel);
font-size: 0.8em;
color: var(--fgOnPanel);
margin: 0;
left: 50%;
transform: translateX(-50%);
}
</style>

View File

@@ -0,0 +1,92 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.wrapper">
<div class="_gaps" :class="$style.root">
<div class="_gaps_s">
<div :class="$style.passkeyIcon">
<i class="ti ti-fingerprint"></i>
</div>
<div :class="$style.passkeyDescription">{{ i18n.ts.useSecurityKey }}</div>
</div>
<MkButton large primary rounded :disabled="queryingKey" style="margin: 0 auto;" @click="queryKey">{{ i18n.ts.retry }}</MkButton>
<MkButton v-if="isPerformingPasswordlessLogin !== true" transparent rounded :disabled="queryingKey" style="margin: 0 auto;" @click="emit('useTotp')">{{ i18n.ts.useTotp }}</MkButton>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
const props = defineProps<{
credentialRequest: CredentialRequestOptions;
isPerformingPasswordlessLogin?: boolean;
}>();
const emit = defineEmits<{
(ev: 'done', credential: AuthenticationPublicKeyCredential): void;
(ev: 'useTotp'): void;
}>();
const queryingKey = ref(true);
async function queryKey() {
queryingKey.value = true;
await webAuthnRequest(props.credentialRequest)
.catch(() => {
return Promise.reject(null);
})
.then((credential) => {
emit('done', credential);
})
.finally(() => {
queryingKey.value = false;
});
}
onMounted(() => {
queryKey();
});
</script>
<style lang="scss" module>
.wrapper {
display: flex;
align-items: center;
width: 100%;
min-height: 336px;
> .root {
width: 100%;
}
}
.passkeyIcon {
margin: 0 auto;
background-color: var(--accentedBg);
color: var(--accent);
text-align: center;
height: 64px;
width: 64px;
font-size: 24px;
line-height: 64px;
border-radius: 50%;
}
.passkeyDescription {
text-align: center;
font-size: 1.1em;
}
</style>

View File

@@ -0,0 +1,181 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.wrapper" data-cy-signin-page-password>
<div class="_gaps" :class="$style.root">
<div :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined }"></div>
<div :class="$style.welcomeBackMessage">
<I18n :src="i18n.ts.welcomeBackWithName" tag="span">
<template #name><Mfm :text="user.name ?? user.username" :plain="true"/></template>
</I18n>
</div>
<!-- password入力 -->
<form class="_gaps_s" @submit.prevent="onSubmit">
<!-- ブラウザ オートコンプリート用 -->
<input type="hidden" name="username" autocomplete="username" :value="user.username">
<MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required autofocus data-cy-signin-password>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<div v-if="needCaptcha">
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
</div>
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</form>
</div>
</div>
</template>
<script lang="ts">
export type PwResponse = {
password: string;
captcha: {
hCaptchaResponse: string | null;
mCaptchaResponse: string | null;
reCaptchaResponse: string | null;
turnstileResponse: string | null;
};
};
</script>
<script setup lang="ts">
import { ref, computed, useTemplateRef, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkCaptcha from '@/components/MkCaptcha.vue';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
needCaptcha: boolean;
}>();
const emit = defineEmits<{
(ev: 'passwordSubmitted', v: PwResponse): void;
}>();
const password = ref('');
const hCaptcha = useTemplateRef('hcaptcha');
const mCaptcha = useTemplateRef('mcaptcha');
const reCaptcha = useTemplateRef('recaptcha');
const turnstile = useTemplateRef('turnstile');
const hCaptchaResponse = ref<string | null>(null);
const mCaptchaResponse = ref<string | null>(null);
const reCaptchaResponse = ref<string | null>(null);
const turnstileResponse = ref<string | null>(null);
const captchaFailed = computed((): boolean => {
return (
(instance.enableHcaptcha && !hCaptchaResponse.value) ||
(instance.enableMcaptcha && !mCaptchaResponse.value) ||
(instance.enableRecaptcha && !reCaptchaResponse.value) ||
(instance.enableTurnstile && !turnstileResponse.value)
);
});
function resetPassword(): void {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
closed: () => dispose(),
});
}
function onSubmit() {
emit('passwordSubmitted', {
password: password.value,
captcha: {
hCaptchaResponse: hCaptchaResponse.value,
mCaptchaResponse: mCaptchaResponse.value,
reCaptchaResponse: reCaptchaResponse.value,
turnstileResponse: turnstileResponse.value,
},
});
}
function resetCaptcha() {
hCaptcha.value?.reset();
mCaptcha.value?.reset();
reCaptcha.value?.reset();
turnstile.value?.reset();
}
defineExpose({
resetCaptcha,
});
</script>
<style lang="scss" module>
.wrapper {
display: flex;
align-items: center;
width: 100%;
min-height: 336px;
> .root {
width: 100%;
}
}
.avatar {
margin: 0 auto 0 auto;
width: 64px;
height: 64px;
background: #ddd;
background-position: center;
background-size: cover;
border-radius: 100%;
}
.welcomeBackMessage {
text-align: center;
font-size: 1.1em;
}
.instanceManualSelectButton {
display: block;
text-align: center;
opacity: .7;
font-size: .8em;
&:hover {
text-decoration: underline;
}
}
.orHr {
position: relative;
margin: .4em auto;
width: 100%;
height: 1px;
background: var(--divider);
}
.orMsg {
position: absolute;
top: -.6em;
display: inline-block;
padding: 0 1em;
background: var(--panel);
font-size: 0.8em;
color: var(--fgOnPanel);
margin: 0;
left: 50%;
transform: translateX(-50%);
}
</style>

View File

@@ -0,0 +1,74 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.wrapper">
<div class="_gaps" :class="$style.root">
<div class="_gaps_s">
<div :class="$style.totpIcon">
<i class="ti ti-key"></i>
</div>
<div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div>
</div>
<!-- totp入力 -->
<form class="_gaps_s" @submit.prevent="emit('totpSubmitted', token)">
<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required autofocus :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
</MkInput>
<MkButton type="submit" large primary rounded style="margin: 0 auto;">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
const emit = defineEmits<{
(ev: 'totpSubmitted', token: string): void;
}>();
const token = ref('');
const isBackupCode = ref(false);
</script>
<style lang="scss" module>
.wrapper {
display: flex;
align-items: center;
width: 100%;
min-height: 336px;
> .root {
width: 100%;
}
}
.totpIcon {
margin: 0 auto;
background-color: var(--accentedBg);
color: var(--accent);
text-align: center;
height: 64px;
width: 64px;
font-size: 24px;
line-height: 64px;
border-radius: 50%;
}
.totpDescription {
text-align: center;
font-size: 1.1em;
}
</style>

View File

@@ -4,269 +4,288 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="_gaps_m">
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<div v-if="openOnRemote" class="_gaps_m">
<div class="_gaps_s">
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
</MkButton>
<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
{{ i18n.ts.specifyServerHost }}
</button>
</div>
<div :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
</div>
<div v-if="!totpLogin" class="normal-signin _gaps_m">
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkButton type="submit" large primary rounded :disabled="captchaFailed || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
<p>{{ i18n.ts.useSecurityKey }}</p>
<MkButton v-if="!queryingKey" @click="query2FaKey">
{{ i18n.ts.retry }}
</MkButton>
</div>
<div v-if="user && user.securityKeys" :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
<div class="twofa-group totp-group _gaps">
<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
</MkInput>
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
</div>
<div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
<div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group">
<MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin">
<i class="ti ti-device-usb" style="font-size: medium;"></i>
{{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }}
</MkButton>
<p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p>
</div>
<div :class="$style.signinRoot">
<Transition
mode="out-in"
:enterActiveClass="$style.transition_enterActive"
:leaveActiveClass="$style.transition_leaveActive"
:enterFromClass="$style.transition_enterFrom"
:leaveToClass="$style.transition_leaveTo"
:inert="waiting"
>
<!-- 1. 外部サーバーへの転送username入力パスキー -->
<XInput
v-if="page === 'input'"
key="input"
:message="message"
:openOnRemote="openOnRemote"
@usernameSubmitted="onUsernameSubmitted"
@passkeyClick="onPasskeyLogin"
/>
<!-- 2. パスワード入力 -->
<XPassword
v-else-if="page === 'password'"
key="password"
ref="passwordPageEl"
:user="userInfo!"
:needCaptcha="needCaptcha"
@passwordSubmitted="onPasswordSubmitted"
/>
<!-- 3. ワンタイムパスワード -->
<XTotp
v-else-if="page === 'totp'"
key="totp"
@totpSubmitted="onTotpSubmitted"
/>
<!-- 4. パスキー -->
<XPasskey
v-else-if="page === 'passkey'"
key="passkey"
:credentialRequest="credentialRequest!"
:isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
@done="onPasskeyDone"
@useTotp="onUseTotp"
/>
</Transition>
<div v-if="waiting" :class="$style.waitingRoot">
<MkLoading/>
</div>
</form>
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue';
import { toUnicode } from 'punycode/';
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import { query, extractDomain } from '@@/js/url.js';
import { host as configHost } from '@@/js/config.js';
import MkDivider from './MkDivider.vue';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
import * as os from '@/os.js';
const signing = ref(false);
const user = ref<Misskey.entities.UserDetailed | null>(null);
const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true);
const username = ref('');
const password = ref('');
const token = ref('');
const host = ref(toUnicode(configHost));
const totpLogin = ref(false);
const isBackupCode = ref(false);
const queryingKey = ref(false);
let credentialRequest: CredentialRequestOptions | null = null;
const passkey_context = ref('');
const hcaptcha = ref<Captcha | undefined>();
const mcaptcha = ref<Captcha | undefined>();
const recaptcha = ref<Captcha | undefined>();
const turnstile = ref<Captcha | undefined>();
const hCaptchaResponse = ref<string | null>(null);
const mCaptchaResponse = ref<string | null>(null);
const reCaptchaResponse = ref<string | null>(null);
const turnstileResponse = ref<string | null>(null);
import XInput from '@/components/MkSignin.input.vue';
import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue';
import XTotp from '@/components/MkSignin.totp.vue';
import XPasskey from '@/components/MkSignin.passkey.vue';
const captchaFailed = computed((): boolean => {
return (
instance.enableHcaptcha && !hCaptchaResponse.value ||
instance.enableMcaptcha && !mCaptchaResponse.value ||
instance.enableRecaptcha && !reCaptchaResponse.value ||
instance.enableTurnstile && !turnstileResponse.value);
});
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
const emit = defineEmits<{
(ev: 'login', v: any): void;
(ev: 'login', v: Misskey.entities.SigninFlowResponse): void;
}>();
const props = withDefaults(defineProps<{
withAvatar?: boolean;
autoSet?: boolean;
message?: string,
openOnRemote?: OpenOnRemoteOptions,
}>(), {
withAvatar: true,
autoSet: false,
message: '',
openOnRemote: undefined,
});
function onUsernameChange(): void {
misskeyApi('users/show', {
username: username.value,
}).then(userResponse => {
user.value = userResponse;
usePasswordLessLogin.value = userResponse.usePasswordLessLogin;
}, () => {
user.value = null;
usePasswordLessLogin.value = true;
const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');
const waiting = ref(false);
const passwordPageEl = useTemplateRef('passwordPageEl');
const needCaptcha = ref(false);
const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
const password = ref('');
//#region Passkey Passwordless
const credentialRequest = shallowRef<CredentialRequestOptions | null>(null);
const passkeyContext = ref('');
const doingPasskeyFromInputPage = ref(false);
function onPasskeyLogin(): void {
if (webAuthnSupported()) {
doingPasskeyFromInputPage.value = true;
waiting.value = true;
misskeyApi('signin-with-passkey', {})
.then((res) => {
passkeyContext.value = res.context ?? '';
credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: res.option,
});
page.value = 'passkey';
waiting.value = false;
})
.catch(onSigninApiError);
}
}
function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
waiting.value = true;
if (doingPasskeyFromInputPage.value) {
misskeyApi('signin-with-passkey', {
credential: credential.toJSON(),
context: passkeyContext.value,
}).then((res) => {
if (res.signinResponse == null) {
onSigninApiError();
return;
}
emit('login', res.signinResponse);
}).catch(onSigninApiError);
} else if (userInfo.value != null) {
tryLogin({
username: userInfo.value.username,
password: password.value,
credential: credential.toJSON(),
});
}
}
function onUseTotp(): void {
page.value = 'totp';
}
//#endregion
async function onUsernameSubmitted(username: string) {
waiting.value = true;
userInfo.value = await misskeyApi('users/show', {
username,
}).catch(() => null);
await tryLogin({
username,
});
}
function onLogin(res: any): Promise<void> | void {
if (props.autoSet) {
return login(res.i);
}
}
async function onPasswordSubmitted(pw: PwResponse) {
waiting.value = true;
password.value = pw.password;
async function query2FaKey(): Promise<void> {
if (credentialRequest == null) return;
queryingKey.value = true;
await webAuthnRequest(credentialRequest)
.catch(() => {
queryingKey.value = false;
return Promise.reject(null);
}).then(credential => {
credentialRequest = null;
queryingKey.value = false;
signing.value = true;
return misskeyApi('signin', {
username: username.value,
password: password.value,
credential: credential.toJSON(),
});
}).then(res => {
emit('login', res);
return onLogin(res);
}).catch(err => {
if (err === null) return;
os.alert({
type: 'error',
text: i18n.ts.signinFailed,
});
signing.value = false;
if (userInfo.value == null) {
await os.alert({
type: 'error',
title: i18n.ts.noSuchUser,
text: i18n.ts.signinFailed,
});
}
function onPasskeyLogin(): void {
signing.value = true;
if (webAuthnSupported()) {
misskeyApi('signin-with-passkey', {})
.then(res => {
totpLogin.value = false;
signing.value = false;
queryingKey.value = true;
passkey_context.value = res.context ?? '';
credentialRequest = parseRequestOptionsFromJSON({
publicKey: res.option,
});
})
.then(() => queryPasskey())
.catch(loginFailed);
}
}
async function queryPasskey(): Promise<void> {
if (credentialRequest == null) return;
queryingKey.value = true;
console.log('Waiting passkey auth...');
await webAuthnRequest(credentialRequest)
.catch((err) => {
console.warn('Passkey Auth fail!: ', err);
queryingKey.value = false;
return Promise.reject(null);
}).then(credential => {
credentialRequest = null;
queryingKey.value = false;
signing.value = true;
return misskeyApi('signin-with-passkey', {
credential: credential.toJSON(),
context: passkey_context.value,
});
}).then(res => {
emit('login', res.signinResponse);
return onLogin(res.signinResponse);
});
}
function onSubmit(): void {
signing.value = true;
if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
if (webAuthnSupported() && user.value.securityKeys) {
misskeyApi('signin', {
username: username.value,
password: password.value,
}).then(res => {
totpLogin.value = true;
signing.value = false;
credentialRequest = parseRequestOptionsFromJSON({
publicKey: res,
});
})
.then(() => query2FaKey())
.catch(loginFailed);
} else {
totpLogin.value = true;
signing.value = false;
}
waiting.value = false;
return;
} else {
misskeyApi('signin', {
username: username.value,
password: password.value,
'hcaptcha-response': hCaptchaResponse.value,
'm-captcha-response': mCaptchaResponse.value,
'g-recaptcha-response': reCaptchaResponse.value,
'turnstile-response': turnstileResponse.value,
token: user.value?.twoFactorEnabled ? token.value : undefined,
}).then(res => {
emit('login', res);
onLogin(res);
}).catch(loginFailed);
await tryLogin({
username: userInfo.value.username,
password: pw.password,
'hcaptcha-response': pw.captcha.hCaptchaResponse,
'm-captcha-response': pw.captcha.mCaptchaResponse,
'g-recaptcha-response': pw.captcha.reCaptchaResponse,
'turnstile-response': pw.captcha.turnstileResponse,
});
}
}
function loginFailed(err: any): void {
hcaptcha.value?.reset?.();
mcaptcha.value?.reset?.();
recaptcha.value?.reset?.();
turnstile.value?.reset?.();
async function onTotpSubmitted(token: string) {
waiting.value = true;
switch (err.id) {
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,
});
}
}
async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> {
const _req = {
username: req.username ?? userInfo.value?.username,
...req,
};
function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest {
return x.username != null;
}
if (!assertIsSigninFlowRequest(_req)) {
throw new Error('Invalid request');
}
return await misskeyApi('signin-flow', _req).then(async (res) => {
if (res.finished) {
emit('login', res);
await onLoginSucceeded(res);
} else {
switch (res.next) {
case 'captcha': {
needCaptcha.value = true;
page.value = 'password';
break;
}
case 'password': {
needCaptcha.value = false;
page.value = 'password';
break;
}
case 'totp': {
page.value = 'totp';
break;
}
case 'passkey': {
if (webAuthnSupported()) {
credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: res.authRequest,
});
page.value = 'passkey';
} else {
page.value = 'totp';
}
break;
}
}
if (doingPasskeyFromInputPage.value === true) {
doingPasskeyFromInputPage.value = false;
page.value = 'input';
password.value = '';
}
passwordPageEl.value?.resetCaptcha();
nextTick(() => {
waiting.value = false;
});
}
return res;
}).catch((err) => {
onSigninApiError(err);
return Promise.reject(err);
});
}
async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true; }) {
if (props.autoSet) {
await login(res.i);
}
}
function onSigninApiError(err?: any): void {
const id = err?.id ?? null;
switch (id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
type: 'error',
@@ -295,6 +314,14 @@ function loginFailed(err: any): void {
});
break;
}
case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.incorrectTotp,
});
break;
}
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
os.alert({
type: 'error',
@@ -303,6 +330,14 @@ function loginFailed(err: any): void {
});
break;
}
case '93b86c4b-72f9-40eb-9815-798928603d1e': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.passkeyVerificationFailed,
});
break;
}
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
os.alert({
type: 'error',
@@ -329,113 +364,55 @@ function loginFailed(err: any): void {
}
}
totpLogin.value = false;
signing.value = false;
}
function resetPassword(): void {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
closed: () => dispose(),
if (doingPasskeyFromInputPage.value === true) {
doingPasskeyFromInputPage.value = false;
page.value = 'input';
password.value = '';
}
passwordPageEl.value?.resetCaptcha();
nextTick(() => {
waiting.value = false;
});
}
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
switch (options.type) {
case 'web':
case 'lookup': {
let _path: string;
if (options.type === 'lookup') {
// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
} else {
_path = options.path;
}
if (targetHost) {
window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
}
break;
}
case 'share': {
const params = query(options.params);
if (targetHost) {
window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
}
break;
}
}
}
async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
const { canceled, result: hostTemp } = await os.inputText({
title: i18n.ts.inputHostName,
placeholder: 'misskey.example.com',
});
if (canceled) return;
let targetHost: string | null = hostTemp;
// ドメイン部分だけを取り出す
targetHost = extractDomain(targetHost);
if (targetHost == null) {
os.alert({
type: 'error',
title: i18n.ts.invalidValue,
text: i18n.ts.tryAgain,
});
return;
}
openRemote(options, targetHost);
}
onBeforeUnmount(() => {
password.value = '';
needCaptcha.value = false;
userInfo.value = null;
});
</script>
<style lang="scss" module>
.avatar {
margin: 0 auto 0 auto;
width: 64px;
height: 64px;
background: #ddd;
background-position: center;
background-size: cover;
border-radius: 100%;
.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);
}
.instanceManualSelectButton {
display: block;
text-align: center;
opacity: .7;
font-size: .8em;
.signinRoot {
overflow-x: hidden;
overflow-x: clip;
&:hover {
text-decoration: underline;
}
}
.orHr {
position: relative;
margin: .4em auto;
width: 100%;
height: 1px;
background: var(--divider);
}
.orMsg {
.waitingRoot {
position: absolute;
top: -.6em;
display: inline-block;
padding: 0 1em;
background: var(--panel);
font-size: 0.8em;
color: var(--fgOnPanel);
margin: 0;
left: 50%;
transform: translateX(-50%);
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: color-mix(in srgb, var(--panel), transparent 50%);
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
</style>

View File

@@ -4,26 +4,29 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="onClose"
<MkModal
ref="modal"
:preferType="'dialog'"
@click="onClose"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.login }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
</MkSpacer>
</MkModalWindow>
<div :class="$style.root">
<div :class="$style.header">
<div :class="$style.headerText"><i class="ti ti-login-2"></i> {{ i18n.ts.login }}</div>
<button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button>
</div>
<div :class="$style.content">
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
</div>
</div>
</MkModal>
</template>
<script lang="ts" setup>
import { shallowRef } from 'vue';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import MkSignin from '@/components/MkSignin.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkModal from '@/components/MkModal.vue';
import { i18n } from '@/i18n.js';
withDefaults(defineProps<{
@@ -42,15 +45,62 @@ const emit = defineEmits<{
(ev: 'cancelled'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const modal = shallowRef<InstanceType<typeof MkModal>>();
function onClose() {
emit('cancelled');
if (dialog.value) dialog.value.close();
if (modal.value) modal.value.close();
}
function onLogin(res) {
emit('done', res);
if (dialog.value) dialog.value.close();
if (modal.value) modal.value.close();
}
</script>
<style lang="scss" module>
.root {
overflow: auto;
margin: auto;
position: relative;
width: 100%;
max-width: 400px;
height: 100%;
max-height: 450px;
box-sizing: border-box;
background: var(--panel);
border-radius: var(--radius);
}
.header {
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 50px;
box-sizing: border-box;
display: flex;
align-items: center;
font-weight: bold;
backdrop-filter: var(--blur, blur(15px));
background: var(--acrylicBg);
z-index: 1;
}
.headerText {
padding: 0 20px;
box-sizing: border-box;
}
.closeButton {
margin-left: auto;
padding: 16px;
font-size: 16px;
line-height: 16px;
}
.content {
padding: 32px;
box-sizing: border-box;
}
</style>

View File

@@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(ev: 'signup', user: Misskey.entities.SigninResponse): void;
(ev: 'signup', user: Misskey.entities.SigninFlowResponse): void;
(ev: 'signupEmailPending'): void;
}>();
@@ -269,14 +269,19 @@ async function onSubmit(): Promise<void> {
});
emit('signupEmailPending');
} else {
const res = await misskeyApi('signin', {
const res = await misskeyApi('signin-flow', {
username: username.value,
password: password.value,
});
emit('signup', res);
if (props.autoSet) {
if (props.autoSet && res.finished) {
return login(res.i);
} else {
os.alert({
type: 'error',
text: i18n.ts.somethingHappened,
});
}
}
} catch {

View File

@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(ev: 'done', res: Misskey.entities.SigninResponse): void;
(ev: 'done', res: Misskey.entities.SigninFlowResponse): void;
(ev: 'closed'): void;
}>();
@@ -55,7 +55,7 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const isAcceptedServerRule = ref(false);
function onSignup(res: Misskey.entities.SigninResponse) {
function onSignup(res: Misskey.entities.SigninFlowResponse) {
emit('done', res);
dialog.value?.close();
}

View File

@@ -6,6 +6,7 @@
import { VNode, h, SetupContext, provide } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import MkUrl from '@/components/global/MkUrl.vue';
import MkTime from '@/components/global/MkTime.vue';
import MkLink from '@/components/MkLink.vue';
@@ -17,7 +18,6 @@ import MkCodeInline from '@/components/MkCodeInline.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import MkA, { MkABehavior } from '@/components/global/MkA.vue';
import { host } from '@@/js/config.js';
import { defaultStore } from '@/store.js';
function safeParseFloat(str: unknown): number | null {
@@ -57,7 +57,8 @@ type MfmEvents = {
// eslint-disable-next-line import/no-default-export
export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
provide('linkNavigationBehavior', props.linkNavigationBehavior);
// こうしたいところだけど functional component 内では provide は使えない
//provide('linkNavigationBehavior', props.linkNavigationBehavior);
const isNote = props.isNote ?? true;
const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
@@ -350,6 +351,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
navigationBehavior: props.linkNavigationBehavior,
})];
}
@@ -358,6 +360,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
navigationBehavior: props.linkNavigationBehavior,
}, genEl(token.children, scale, true))];
}
@@ -366,6 +369,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
key: Math.random(),
host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
username: token.props.username,
navigationBehavior: props.linkNavigationBehavior,
})];
}
@@ -374,6 +378,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
key: Math.random(),
to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--hashtag);',
behavior: props.linkNavigationBehavior,
}, `#${token.props.hashtag}`)];
}

View File

@@ -27,6 +27,7 @@ import MkLoadingPage from '@/pages/_loading_.vue';
const props = defineProps<{
router?: IRouter;
nested?: boolean;
}>();
const router = props.router ?? inject('router');
@@ -39,6 +40,8 @@ const currentDepth = inject('routerCurrentDepth', 0);
provide('routerCurrentDepth', currentDepth + 1);
function resolveNested(current: Resolved, d = 0): Resolved | null {
if (!props.nested) return current;
if (d === currentDepth) {
return current;
} else {

View File

@@ -53,6 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
<!--
@@ -205,6 +206,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, defineAsyncComponent, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -220,7 +222,6 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@@/js/config.js';
import { acct } from '@/filters/user.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';

View File

@@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
</div>
<MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
{{ i18n.ts._abuseUserReport.resolveTutorial }}
</MkInfo>
<div :class="$style.inputs" class="_gaps">
<MkSelect v-model="state" style="margin: 0; flex: 1;">
<template #label>{{ i18n.ts.state }}</template>
@@ -44,8 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
-->
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination">
<div class="_gaps">
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
</div>
</MkPagination>
</div>
</MkSpacer>
@@ -54,7 +60,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, shallowRef, ref } from 'vue';
import XHeader from './_header_.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -62,6 +67,8 @@ import XAbuseReport from '@/components/MkAbuseReport.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import { defaultStore } from '@/store.js';
const reports = shallowRef<InstanceType<typeof MkPagination>>();
@@ -85,6 +92,10 @@ function resolved(reportId) {
reports.value?.removeItem(reportId);
}
function closeTutorial() {
defaultStore.set('abusesTutorial', false);
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);

View File

@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSpacer>
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
<RouterView/>
<RouterView nested/>
</div>
</div>
</template>

View File

@@ -165,6 +165,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div>
</template>
<template v-else-if="log.type === 'updateAbuseReportNote'">
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div>
</template>
<details>
<summary>raw</summary>

View File

@@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
<div class="_gaps_s">
<XModLog v-for="item in items" :key="item.id" :log="item"/>
</div>
<MkDateSeparatedList v-slot="{ item }" :items="items" :noGap="false" style="--margin: 8px;">
<XModLog :key="item.id" :log="item"/>
</MkDateSeparatedList>
</MkPagination>
</div>
</MkSpacer>
@@ -39,6 +39,7 @@ import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
const logs = shallowRef<InstanceType<typeof MkPagination>>();

View File

@@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkFolder>
<template #label>{{ entity.name || entity.url }}</template>
<template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template>
<template #icon>
<i v-if="!entity.isActive" class="ti ti-player-pause"/>
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>

View File

@@ -55,7 +55,8 @@ const tab = ref('featured');
const featuredFlashsPagination = {
endpoint: 'flash/featured' as const,
noPaging: true,
limit: 5,
offsetMode: true,
};
const myFlashsPagination = {
endpoint: 'flash/my' as const,

View File

@@ -51,6 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
<MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
</div>
</FormSection>

View File

@@ -14,30 +14,39 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{items}">
<div class="_gaps">
<div v-for="token in items" :key="token.id" class="_panel" :class="$style.app">
<img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
<div :class="$style.appBody">
<div :class="$style.appName">{{ token.name }}</div>
<div>{{ token.description }}</div>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.installedDate }}</template>
<template #value><MkTime :time="token.createdAt"/></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.lastUsedDate }}</template>
<template #value><MkTime :time="token.lastUsedAt"/></template>
</MkKeyValue>
<details>
<summary>{{ i18n.ts.details }}</summary>
<MkFolder v-for="token in items" :key="token.id" :defaultOpen="true">
<template #icon>
<img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
<i v-else class="ti ti-plug"/>
</template>
<template #label>{{ token.name }}</template>
<template #caption>{{ token.description }}</template>
<template #suffix><MkTime :time="token.lastUsedAt"/></template>
<template #footer>
<MkButton danger @click="revoke(token)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</template>
<div class="_gaps_s">
<div v-if="token.description">{{ token.description }}</div>
<div>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.installedDate }}</template>
<template #value><MkTime :time="token.createdAt" :mode="'detail'"/></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.lastUsedDate }}</template>
<template #value><MkTime :time="token.lastUsedAt" :mode="'detail'"/></template>
</MkKeyValue>
</div>
<MkFolder>
<template #label>{{ i18n.ts.permission }}</template>
<template #suffix>{{ Object.keys(token.permission).length === 0 ? i18n.ts.none : Object.keys(token.permission).length }}</template>
<ul>
<li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
</ul>
</details>
<div>
<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
</div>
</MkFolder>
</div>
</div>
</MkFolder>
</div>
</template>
</FormPagination>
@@ -52,6 +61,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import { infoImageUrl } from '@/instance.js';
const list = ref<InstanceType<typeof FormPagination>>();
@@ -82,26 +92,9 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
.app {
display: flex;
padding: 16px;
}
.appIcon {
display: block;
flex-shrink: 0;
margin: 0 12px 0 0;
width: 50px;
height: 50px;
border-radius: 8px;
}
.appBody {
width: calc(100% - 62px);
position: relative;
}
.appName {
font-weight: bold;
width: 20px;
height: 20px;
border-radius: 4px;
}
</style>

View File

@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
<div class="bkzroven" style="container-type: inline-size;">
<RouterView/>
<RouterView nested/>
</div>
</div>
</div>

View File

@@ -46,14 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder>
<template #icon><i class="ti ti-list"></i></template>
<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
<div :class="$style.metadataRoot">
<div :class="$style.metadataMargin">
<MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" inline danger style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
<MkButton v-else inline style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<template #footer>
<div class="_buttons">
<MkButton primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<MkButton :disabled="fields.length >= 16" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" danger @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
<MkButton v-else @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
</div>
</template>
<div :class="$style.metadataRoot" class="_gaps_s">
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
<Sortable
v-model="fields"
@@ -65,24 +68,20 @@ SPDX-License-Identifier: AGPL-3.0-only
@end="e => e.item.classList.remove('active')"
>
<template #item="{element, index}">
<div :class="$style.fieldDragItem">
<div v-panel :class="$style.fieldDragItem">
<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
<div :class="$style.dragItemForm">
<FormSplit :minWidth="200">
<MkInput v-model="element.name" small>
<template #label>{{ i18n.ts._profile.metadataLabel }}</template>
<MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
</MkInput>
<MkInput v-model="element.value" small>
<template #label>{{ i18n.ts._profile.metadataContent }}</template>
<MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
</MkInput>
</FormSplit>
</div>
</div>
</template>
</Sortable>
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
</div>
</MkFolder>
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
@@ -310,19 +309,11 @@ definePageMetadata(() => ({
container-type: inline-size;
}
.metadataMargin {
margin-bottom: 1.5em;
}
.fieldDragItem {
display: flex;
padding-bottom: .75em;
padding: 10px;
align-items: flex-end;
border-bottom: solid 0.5px var(--divider);
&:last-child {
border-bottom: 0;
}
border-radius: 6px;
/* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */
@container (max-width: 452px) {

View File

@@ -64,6 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="iAmModerator" class="moderationNote">
<MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
<div v-else>
<MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
@@ -159,6 +160,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { getScrollPosition } from '@@/js/scroll.js';
import MkNote from '@/components/MkNote.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import MkAccountMoved from '@/components/MkAccountMoved.vue';
@@ -168,7 +170,6 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkOmit from '@/components/MkOmit.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue';
import { getScrollPosition } from '@@/js/scroll.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';

View File

@@ -78,6 +78,10 @@ export const defaultStore = markRaw(new Storage('base', {
global: false,
},
},
abusesTutorial: {
where: 'account',
default: false,
},
keepCw: {
where: 'account',
default: true,

View File

@@ -213,6 +213,9 @@ type AdminFederationRemoveAllFollowingRequest = operations['admin___federation__
// @public (undocumented)
type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json'];
@@ -378,6 +381,9 @@ type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requ
// @public (undocumented)
type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
@@ -1158,9 +1164,9 @@ export type Endpoints = Overwrite<Endpoints_2, {
req: SignupPendingRequest;
res: SignupPendingResponse;
};
'signin': {
req: SigninRequest;
res: SigninResponse;
'signin-flow': {
req: SigninFlowRequest;
res: SigninFlowResponse;
};
'signin-with-passkey': {
req: SigninWithPasskeyRequest;
@@ -1208,11 +1214,11 @@ declare namespace entities {
SignupResponse,
SignupPendingRequest,
SignupPendingResponse,
SigninRequest,
SigninFlowRequest,
SigninFlowResponse,
SigninWithPasskeyRequest,
SigninWithPasskeyInitResponse,
SigninWithPasskeyResponse,
SigninResponse,
PartialRolePolicyOverride,
EmptyRequest,
EmptyResponse,
@@ -1298,6 +1304,8 @@ declare namespace entities {
AdminResetPasswordRequest,
AdminResetPasswordResponse,
AdminResolveAbuseUserReportRequest,
AdminForwardAbuseUserReportRequest,
AdminUpdateAbuseUserReportRequest,
AdminSendEmailRequest,
AdminServerInfoResponse,
AdminShowModerationLogsRequest,
@@ -1680,6 +1688,7 @@ declare namespace entities {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -1929,6 +1938,9 @@ type FlashCreateResponse = operations['flash___create']['responses']['200']['con
// @public (undocumented)
type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
// @public (undocumented)
type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
// @public (undocumented)
type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
@@ -2542,6 +2554,12 @@ type ModerationLog = {
} | {
type: 'resolveAbuseReport';
info: ModerationLogPayloads['resolveAbuseReport'];
} | {
type: 'forwardAbuseReport';
info: ModerationLogPayloads['forwardAbuseReport'];
} | {
type: 'updateAbuseReportNote';
info: ModerationLogPayloads['updateAbuseReportNote'];
} | {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
@@ -2581,7 +2599,7 @@ type ModerationLog = {
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@@ -3038,9 +3056,9 @@ type ServerStatsLog = ServerStats[];
type Signin = components['schemas']['Signin'];
// @public (undocumented)
type SigninRequest = {
type SigninFlowRequest = {
username: string;
password: string;
password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string | null;
@@ -3050,9 +3068,17 @@ type SigninRequest = {
};
// @public (undocumented)
type SigninResponse = {
type SigninFlowResponse = {
finished: true;
id: User['id'];
i: string;
} | {
finished: false;
next: 'captcha' | 'password' | 'totp';
} | {
finished: false;
next: 'passkey';
authRequest: PublicKeyCredentialRequestOptionsJSON;
};
// @public (undocumented)
@@ -3069,7 +3095,7 @@ type SigninWithPasskeyRequest = {
// @public (undocumented)
type SigninWithPasskeyResponse = {
signinResponse: SigninResponse;
signinResponse: SigninFlowResponse;
};
// @public (undocumented)

View File

@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.10.0-alpha.1",
"version": "2024.10.0-beta.5",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@@ -3,8 +3,8 @@ import { UserDetailed } from './autogen/models.js';
import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
import {
PartialRolePolicyOverride,
SigninRequest,
SigninResponse,
SigninFlowRequest,
SigninFlowResponse,
SigninWithPasskeyInitResponse,
SigninWithPasskeyRequest,
SigninWithPasskeyResponse,
@@ -81,9 +81,9 @@ export type Endpoints = Overwrite<
res: SignupPendingResponse;
},
// api.jsonには載せないものなのでここで定義
'signin': {
req: SigninRequest;
res: SigninResponse;
'signin-flow': {
req: SigninFlowRequest;
res: SigninFlowResponse;
},
'signin-with-passkey': {
req: SigninWithPasskeyRequest;

View File

@@ -691,6 +691,28 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
request<E extends 'admin/forward-abuse-user-report', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
request<E extends 'admin/update-abuse-user-report', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View File

@@ -83,6 +83,8 @@ import type {
AdminResetPasswordRequest,
AdminResetPasswordResponse,
AdminResolveAbuseUserReportRequest,
AdminForwardAbuseUserReportRequest,
AdminUpdateAbuseUserReportRequest,
AdminSendEmailRequest,
AdminServerInfoResponse,
AdminShowModerationLogsRequest,
@@ -465,6 +467,7 @@ import type {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -638,6 +641,8 @@ export type Endpoints = {
'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse };
'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse };
'admin/forward-abuse-user-report': { req: AdminForwardAbuseUserReportRequest; res: EmptyResponse };
'admin/update-abuse-user-report': { req: AdminUpdateAbuseUserReportRequest; res: EmptyResponse };
'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
@@ -889,7 +894,7 @@ export type Endpoints = {
'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
'flash/featured': { req: EmptyRequest; res: FlashFeaturedResponse };
'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };

View File

@@ -86,6 +86,8 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re
export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json'];
export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json'];
export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json'];
export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
@@ -468,6 +470,7 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];

View File

@@ -576,6 +576,24 @@ export type paths = {
*/
post: operations['admin___resolve-abuse-user-report'];
};
'/admin/forward-abuse-user-report': {
/**
* admin/forward-abuse-user-report
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
post: operations['admin___forward-abuse-user-report'];
};
'/admin/update-abuse-user-report': {
/**
* admin/update-abuse-user-report
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
post: operations['admin___update-abuse-user-report'];
};
'/admin/send-email': {
/**
* admin/send-email
@@ -3782,16 +3800,13 @@ export type components = {
followingVisibility: 'public' | 'followers' | 'private';
/** @enum {string} */
followersVisibility: 'public' | 'followers' | 'private';
/** @default false */
twoFactorEnabled: boolean;
/** @default false */
usePasswordLessLogin: boolean;
/** @default false */
securityKeys: boolean;
roles: components['schemas']['RoleLite'][];
followedMessage?: string | null;
memo: string | null;
moderationNote?: string;
twoFactorEnabled?: boolean;
usePasswordLessLogin?: boolean;
securityKeys?: boolean;
isFollowing?: boolean;
isFollowed?: boolean;
hasPendingFollowRequestFromYou?: boolean;
@@ -3972,6 +3987,12 @@ export type components = {
}[];
loggedInDays: number;
policies: components['schemas']['RolePolicies'];
/** @default false */
twoFactorEnabled: boolean;
/** @default false */
usePasswordLessLogin: boolean;
/** @default false */
securityKeys: boolean;
email?: string | null;
emailVerified?: boolean | null;
securityKeysList?: {
@@ -8690,8 +8711,113 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
reportId: string;
/** @default false */
forward?: boolean;
/** @enum {string|null} */
resolvedAs?: 'accept' | 'reject' | null;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* admin/forward-abuse-user-report
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
'admin___forward-abuse-user-report': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
reportId: string;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* admin/update-abuse-user-report
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
'admin___update-abuse-user-report': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
reportId: string;
moderationNote?: string;
};
};
};
@@ -23796,6 +23922,16 @@ export type operations = {
* **Credential required**: *No*
*/
flash___featured: {
requestBody: {
content: {
'application/json': {
/** @default 0 */
offset?: number;
/** @default 10 */
limit?: number;
};
};
};
responses: {
/** @description OK (with results) */
200: {

View File

@@ -142,6 +142,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
'forwardAbuseReport',
'updateAbuseReportNote',
'createInvitation',
'createAd',
'updateAd',
@@ -330,7 +332,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: {
reportId: string;
report: ReceivedAbuseReport;
forwarded: boolean;
forwarded?: boolean;
resolvedAs?: string | null;
};
forwardAbuseReport: {
reportId: string;
report: ReceivedAbuseReport;
};
updateAbuseReportNote: {
reportId: string;
report: ReceivedAbuseReport;
before: string;
after: string;
};
createInvitation: {
invitations: InviteCode[];

View File

@@ -153,6 +153,12 @@ export type ModerationLog = {
} | {
type: 'resolveAbuseReport';
info: ModerationLogPayloads['resolveAbuseReport'];
} | {
type: 'forwardAbuseReport';
info: ModerationLogPayloads['forwardAbuseReport'];
} | {
type: 'updateAbuseReportNote';
info: ModerationLogPayloads['updateAbuseReportNote'];
} | {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
@@ -267,9 +273,9 @@ export type SignupPendingResponse = {
i: string,
};
export type SigninRequest = {
export type SigninFlowRequest = {
username: string;
password: string;
password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string | null;
@@ -278,6 +284,19 @@ export type SigninRequest = {
'm-captcha-response'?: string | null;
};
export type SigninFlowResponse = {
finished: true;
id: User['id'];
i: string;
} | {
finished: false;
next: 'captcha' | 'password' | 'totp';
} | {
finished: false;
next: 'passkey';
authRequest: PublicKeyCredentialRequestOptionsJSON;
};
export type SigninWithPasskeyRequest = {
credential?: AuthenticationResponseJSON;
context?: string;
@@ -289,12 +308,7 @@ export type SigninWithPasskeyInitResponse = {
};
export type SigninWithPasskeyResponse = {
signinResponse: SigninResponse;
};
export type SigninResponse = {
id: User['id'],
i: string,
signinResponse: SigninFlowResponse;
};
type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];

1028
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff