diff options
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | cypress.config.ts | 5 | ||||
| -rw-r--r-- | cypress/e2e/basic.cy.ts | 9 | ||||
| -rw-r--r-- | cypress/plugins/index.js | 22 | ||||
| -rw-r--r-- | locales/index.d.ts | 176 | ||||
| -rw-r--r-- | locales/ja-JP.yml | 47 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | packages/backend/migration/1746422049376-singleUserMode.js | 16 | ||||
| -rw-r--r-- | packages/backend/src/models/Meta.ts | 5 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/admin/meta.ts | 5 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/admin/update-meta.ts | 5 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkRadio.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkRadios.vue | 19 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkServerSetupWizard.vue | 356 | ||||
| -rw-r--r-- | packages/frontend/src/os.ts | 28 | ||||
| -rw-r--r-- | packages/frontend/src/pages/welcome.setup.vue | 178 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/types.ts | 2 |
17 files changed, 803 insertions, 82 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c364eea29..1da3ae6120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - デフォルト値は「ローカルのコンテンツだけ公開」になっています ### Client +- Feat: サーバー初期設定ウィザードが実装されました + - 簡単なウィザードに従うだけで、サーバーに最適な設定が適用されます - Feat: Websocket接続を行わずにMisskeyを利用するNo Websocketモードが実装されました(beta) - サーバーのパフォーマンス向上に寄与することが期待されます - 何らの理由によりWebsocket接続が行えない環境でも快適に利用可能です diff --git a/cypress.config.ts b/cypress.config.ts index e390c41a54..361acaf6e5 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -2,11 +2,6 @@ import { defineConfig } from 'cypress' export default defineConfig({ e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, baseUrl: 'http://localhost:61812', }, }) diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index 6471f96504..bd4021d2e3 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -31,6 +31,15 @@ describe('Before setup instance', () => { // なぜか動かない //cy.wait('@signup').should('have.property', 'response.statusCode'); cy.wait('@signup'); + + cy.intercept('POST', '/api/admin/update-meta').as('update-meta'); + + cy.get('[data-cy-next]').click(); + cy.get('[data-cy-next]').click(); + cy.get('[data-cy-server-name] input').type('Testskey'); + cy.get('[data-cy-server-setup-wizard-apply]').click(); + + cy.wait('@update-meta'); }); }); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js deleted file mode 100644 index 59b2bab6e4..0000000000 --- a/cypress/plugins/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/// <reference types="cypress" /> -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -/** - * @type {Cypress.PluginConfig} - */ -// eslint-disable-next-line no-unused-vars -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -} diff --git a/locales/index.d.ts b/locales/index.d.ts index e10506bfe3..6bd5c8c3d7 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1023,10 +1023,6 @@ export interface Locale extends ILocale { */ "pinLimitExceeded": string; /** - * Misskeyのインストールが完了しました!管理者アカウントを作成しましょう。 - */ - "intro": string; - /** * 完了 */ "done": string; @@ -5414,6 +5410,10 @@ export interface Locale extends ILocale { */ "scrollToClose": string; /** + * アドバイス + */ + "advice": string; + /** * リアルタイムモード */ "realtimeMode": string; @@ -6413,6 +6413,14 @@ export interface Locale extends ILocale { */ "deliverSuspendedSoftwareDescription": string; /** + * お一人様モード + */ + "singleUserMode": string; + /** + * このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。 + */ + "singleUserMode_description": string; + /** * 非利用者に対するユーザー作成コンテンツの公開範囲 */ "userGeneratedContentsVisibilityForVisitor": string; @@ -11670,6 +11678,166 @@ export interface Locale extends ILocale { */ "serverHostPlaceholder": string; }; + "_serverSetupWizard": { + /** + * Misskeyのインストールが完了しました! + */ + "installCompleted": string; + /** + * まずは、管理者アカウントを作成しましょう。 + */ + "firstCreateAccount": string; + /** + * 管理者アカウントが作成されました! + */ + "accountCreated": string; + /** + * サーバーの設定 + */ + "serverSetting": string; + /** + * このウィザードで簡単に最適なサーバーの設定が行えます。 + */ + "youCanEasilyConfigureOptimalServerSettingsWithThisWizard": string; + /** + * ここでの設定は、あとからでも変更できます。 + */ + "settingsYouMakeHereCanBeChangedLater": string; + /** + * Misskeyをどのように使いますか? + */ + "howWillYouUseMisskey": string; + "_use": { + /** + * お一人様サーバー + */ + "single": string; + /** + * 自分専用のサーバーとして、一人で使う + */ + "single_description": string; + /** + * お一人様サーバーとして運用する場合でも、アカウントは必要に応じて複数作成可能です。 + */ + "single_youCanCreateMultipleAccounts": string; + /** + * グループサーバー + */ + "group": string; + /** + * 信頼できる他の利用者を招待して、複数人で使う + */ + "group_description": string; + /** + * オープンサーバー + */ + "open": string; + /** + * 不特定多数の利用者を受け入れる運営を行う + */ + "open_description": string; + }; + /** + * 不特定多数の利用者を受け入れることはリスクが伴います。トラブルに対処できるよう、確実なモデレーション体制で運営することを推奨します。 + */ + "openServerAdvice": string; + /** + * 自サーバーがスパムの踏み台にならないように、reCAPTCHAといったアンチボット機能を有効にするなど、セキュリティについても細心の注意が必要です。 + */ + "openServerAntiSpamAdvice": string; + /** + * どれくらいの人数を想定していますか? + */ + "howManyUsersDoYouExpect": string; + "_scale": { + /** + * 100人以下 (小規模) + */ + "small": string; + /** + * 100人以上1000人以下 (中規模) + */ + "medium": string; + /** + * 1000人以上 (大規模) + */ + "large": string; + }; + /** + * 大規模なサーバーでは、ロードバランシングやデータベースのレプリケーションなど、高度なインフラストラクチャーの知識が必要になる場合があります。 + */ + "largeScaleServerAdvice": string; + /** + * Fediverseと接続しますか? + */ + "doYouConnectToFediverse": string; + /** + * 分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。 + */ + "doYouConnectToFediverse_description1": string; + /** + * Fediverseと接続することは「連合」とも呼ばれます。 + */ + "doYouConnectToFediverse_description2": string; + /** + * 連合可能なサーバーの指定など、高度な設定も後ほど可能です。 + */ + "youCanConfigureMoreFederationSettingsLater": string; + /** + * 管理者情報 + */ + "adminInfo": string; + /** + * 問い合わせを受け付けるために使用される管理者情報を設定します。 + */ + "adminInfo_description": string; + /** + * オープンサーバー、または連合がオンの場合は必ず入力が必要です。 + */ + "adminInfo_mustBeFilled": string; + /** + * 以下の設定が推奨されます + */ + "followingSettingsAreRecommended": string; + /** + * この設定を適用 + */ + "applyTheseSettings": string; + /** + * 設定をスキップ + */ + "skipSettings": string; + /** + * 設定が完了しました! + */ + "settingsCompleted": string; + /** + * お疲れ様でした。準備が整ったので、さっそくサーバーの使用を開始できます。 + */ + "settingsCompleted_description": string; + /** + * 詳細なサーバー設定は、「コントロールパネル」から行えます。 + */ + "settingsCompleted_description2": string; + /** + * 寄付のお願い + */ + "donationRequest": string; + "_donationRequest": { + /** + * Misskeyは有志によって開発されている無料のソフトウェアです。 + */ + "text1": string; + /** + * 今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。 + */ + "text2": string; + /** + * 支援者向け特典もあります! + */ + "text3": string; + }; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index aeaff7e875..6c73285295 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -251,7 +251,6 @@ noUsers: "ユーザーはいません" editProfile: "プロフィールを編集" noteDeleteConfirm: "このノートを削除しますか?" pinLimitExceeded: "これ以上ピン留めできません" -intro: "Misskeyのインストールが完了しました!管理者アカウントを作成しましょう。" done: "完了" processing: "処理中" preview: "プレビュー" @@ -1348,6 +1347,7 @@ goToDeck: "デッキへ戻る" federationJobs: "連合ジョブ" driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。" scrollToClose: "スクロールして閉じる" +advice: "アドバイス" realtimeMode: "リアルタイムモード" turnItOn: "オンにする" turnItOff: "オフにする" @@ -1629,6 +1629,8 @@ _serverSettings: thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。" deliverSuspendedSoftware: "配信停止中のソフトウェア" deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。" + singleUserMode: "お一人様モード" + singleUserMode_description: "このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。" userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲" userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。" userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。" @@ -3121,3 +3123,46 @@ _search: pleaseEnterServerHost: "サーバーのホストを入力してください" pleaseSelectUser: "ユーザーを選択してください" serverHostPlaceholder: "例: misskey.example.com" + +_serverSetupWizard: + installCompleted: "Misskeyのインストールが完了しました!" + firstCreateAccount: "まずは、管理者アカウントを作成しましょう。" + accountCreated: "管理者アカウントが作成されました!" + serverSetting: "サーバーの設定" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "このウィザードで簡単に最適なサーバーの設定が行えます。" + settingsYouMakeHereCanBeChangedLater: "ここでの設定は、あとからでも変更できます。" + howWillYouUseMisskey: "Misskeyをどのように使いますか?" + _use: + single: "お一人様サーバー" + single_description: "自分専用のサーバーとして、一人で使う" + single_youCanCreateMultipleAccounts: "お一人様サーバーとして運用する場合でも、アカウントは必要に応じて複数作成可能です。" + group: "グループサーバー" + group_description: "信頼できる他の利用者を招待して、複数人で使う" + open: "オープンサーバー" + open_description: "不特定多数の利用者を受け入れる運営を行う" + openServerAdvice: "不特定多数の利用者を受け入れることはリスクが伴います。トラブルに対処できるよう、確実なモデレーション体制で運営することを推奨します。" + openServerAntiSpamAdvice: "自サーバーがスパムの踏み台にならないように、reCAPTCHAといったアンチボット機能を有効にするなど、セキュリティについても細心の注意が必要です。" + howManyUsersDoYouExpect: "どれくらいの人数を想定していますか?" + _scale: + small: "100人以下 (小規模)" + medium: "100人以上1000人以下 (中規模)" + large: "1000人以上 (大規模)" + largeScaleServerAdvice: "大規模なサーバーでは、ロードバランシングやデータベースのレプリケーションなど、高度なインフラストラクチャーの知識が必要になる場合があります。" + doYouConnectToFediverse: "Fediverseと接続しますか?" + doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。" + doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。" + youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。" + adminInfo: "管理者情報" + adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。" + adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。" + followingSettingsAreRecommended: "以下の設定が推奨されます" + applyTheseSettings: "この設定を適用" + skipSettings: "設定をスキップ" + settingsCompleted: "設定が完了しました!" + settingsCompleted_description: "お疲れ様でした。準備が整ったので、さっそくサーバーの使用を開始できます。" + settingsCompleted_description2: "詳細なサーバー設定は、「コントロールパネル」から行えます。" + donationRequest: "寄付のお願い" + _donationRequest: + text1: "Misskeyは有志によって開発されている無料のソフトウェアです。" + text2: "今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。" + text3: "支援者向け特典もあります!" diff --git a/package.json b/package.json index ff08cbe97b..ff5f685d3f 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "watch": "pnpm dev", "dev": "node scripts/dev.mjs", "lint": "pnpm -r lint", - "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", + "cy:open": "pnpm cypress open --config-file=cypress.config.ts", "cy:run": "pnpm cypress run", "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", "e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run", diff --git a/packages/backend/migration/1746422049376-singleUserMode.js b/packages/backend/migration/1746422049376-singleUserMode.js new file mode 100644 index 0000000000..9a79d46d5b --- /dev/null +++ b/packages/backend/migration/1746422049376-singleUserMode.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SingleUserMode1746422049376 { + name = 'SingleUserMode1746422049376' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "singleUserMode" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "singleUserMode"`); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 95d19c8075..e3625b247c 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -675,6 +675,11 @@ export class MiMeta { default: [], }) public deliverSuspendedSoftware: SoftwareSuspension[]; + + @Column('boolean', { + default: false, + }) + public singleUserMode: boolean; } export type SoftwareSuspension = { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index cb48a1bc95..cd36985485 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -546,6 +546,10 @@ export const meta = { }, }, }, + singleUserMode: { + type: 'boolean', + optional: false, nullable: false, + }, ugcVisibilityForVisitor: { type: 'string', enum: ['all', 'local', 'none'], @@ -696,6 +700,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- federation: instance.federation, federationHosts: instance.federationHosts, deliverSuspendedSoftware: instance.deliverSuspendedSoftware, + singleUserMode: instance.singleUserMode, ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index fc6b890b69..a96fbd759c 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -196,6 +196,7 @@ export const paramDef = { required: ['software', 'versionRange'], }, }, + singleUserMode: { type: 'boolean' }, ugcVisibilityForVisitor: { type: 'string', enum: ['all', 'local', 'none'], @@ -694,6 +695,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); } + if (ps.singleUserMode !== undefined) { + set.singleUserMode = ps.singleUserMode; + } + if (ps.ugcVisibilityForVisitor !== undefined) { set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor; } diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index f16c8f6c2a..a7d77dd118 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -48,7 +48,8 @@ function toggle(): void { <style lang="scss" module> .root { position: relative; - display: inline-block; + display: inline-flex; + align-items: center; text-align: left; cursor: pointer; padding: 7px 10px; @@ -102,7 +103,8 @@ function toggle(): void { } .button { - position: absolute; + position: relative; + display: inline-block; width: 14px; height: 14px; background: none; @@ -126,7 +128,7 @@ function toggle(): void { } .label { - margin-left: 28px; + margin-left: 8px; display: block; line-height: 20px; cursor: pointer; diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index 884890bf70..8b641d0f93 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -5,14 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { defineComponent, h, ref, watch } from 'vue'; -import type { VNode } from 'vue'; import MkRadio from './MkRadio.vue'; +import type { VNode } from 'vue'; export default defineComponent({ props: { modelValue: { required: false, }, + vertical: { + type: Boolean, + default: false, + }, }, setup(props, context) { const value = ref(props.modelValue); @@ -34,7 +38,10 @@ export default defineComponent({ options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if')); return () => h('div', { - class: 'novjtcto', + class: [ + 'novjtcto', + ...(props.vertical ? ['vertical'] : []), + ], }, [ ...(label ? [h('div', { class: 'label', @@ -71,7 +78,7 @@ export default defineComponent({ > .body { display: flex; - gap: 12px; + gap: 10px; flex-wrap: wrap; } @@ -84,5 +91,11 @@ export default defineComponent({ display: none; } } + + &.vertical { + > .body { + flex-direction: column; + } + } } </style> diff --git a/packages/frontend/src/components/MkServerSetupWizard.vue b/packages/frontend/src/components/MkServerSetupWizard.vue new file mode 100644 index 0000000000..65e0d6d9de --- /dev/null +++ b/packages/frontend/src/components/MkServerSetupWizard.vue @@ -0,0 +1,356 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root" class="_gaps_m"> + <MkInput v-model="q_name" data-cy-server-name> + <template #label>{{ i18n.ts.instanceName }}</template> + </MkInput> + + <MkFolder :defaultOpen="true"> + <template #label>{{ i18n.ts._serverSetupWizard.howWillYouUseMisskey }}</template> + <template #icon><i class="ti ti-settings-question"></i></template> + + <div class="_gaps_s"> + <MkRadios v-model="q_use" :vertical="true"> + <option value="single"> + <div><i class="ti ti-user"></i> <b>{{ i18n.ts._serverSetupWizard._use.single }}</b></div> + <div>{{ i18n.ts._serverSetupWizard._use.single_description }}</div> + </option> + <option value="group"> + <div><i class="ti ti-lock"></i> <b>{{ i18n.ts._serverSetupWizard._use.group }}</b></div> + <div>{{ i18n.ts._serverSetupWizard._use.group_description }}</div> + </option> + <option value="open"> + <div><i class="ti ti-world"></i> <b>{{ i18n.ts._serverSetupWizard._use.open }}</b></div> + <div>{{ i18n.ts._serverSetupWizard._use.open_description }}</div> + </option> + </MkRadios> + + <MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo> + <MkInfo v-if="q_use === 'open'" warn><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.openServerAdvice }}</MkInfo> + <MkInfo v-if="q_use === 'open'" warn><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.openServerAntiSpamAdvice }}</MkInfo> + </div> + </MkFolder> + + <MkFolder v-if="q_use !== 'single'" :defaultOpen="true"> + <template #label>{{ i18n.ts._serverSetupWizard.howManyUsersDoYouExpect }}</template> + <template #icon><i class="ti ti-users"></i></template> + + <div class="_gaps_s"> + <MkRadios v-model="q_scale" :vertical="true"> + <option value="small"><i class="ti ti-user"></i> {{ i18n.ts._serverSetupWizard._scale.small }}</option> + <option value="medium"><i class="ti ti-users"></i> {{ i18n.ts._serverSetupWizard._scale.medium }}</option> + <option value="large"><i class="ti ti-users-group"></i> {{ i18n.ts._serverSetupWizard._scale.large }}</option> + </MkRadios> + + <MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo> + </div> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #label>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse }}</template> + <template #icon><i class="ti ti-planet"></i></template> + + <div class="_gaps_s"> + <div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}</div> + + <MkRadios v-model="q_federation" :vertical="true"> + <option value="yes">{{ i18n.ts.yes }}</option> + <option value="no">{{ i18n.ts.no }}</option> + </MkRadios> + + <MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo> + </div> + </MkFolder> + + <MkFolder v-if="q_use === 'open' || q_federation === 'yes'" :defaultOpen="true"> + <template #label>{{ i18n.ts._serverSetupWizard.adminInfo }}</template> + <template #icon><i class="ti ti-mail"></i></template> + + <div class="_gaps_s"> + <div>{{ i18n.ts._serverSetupWizard.adminInfo_description }}</div> + + <MkInfo warn>{{ i18n.ts._serverSetupWizard.adminInfo_mustBeFilled }}</MkInfo> + + <MkInput v-model="q_adminName"> + <template #label>{{ i18n.ts.maintainerName }}</template> + </MkInput> + + <MkInput v-model="q_adminEmail" type="email"> + <template #label>{{ i18n.ts.maintainerEmail }}</template> + </MkInput> + </div> + </MkFolder> + + <MkFolder :defaultOpen="true" :maxHeight="300"> + <template #label>{{ i18n.ts._serverSetupWizard.followingSettingsAreRecommended }}</template> + <template #icon><i class="ti ti-adjustments-alt"></i></template> + + <div class="_gaps_s"> + <div> + <div><b>{{ i18n.ts._serverSettings.singleUserMode }}:</b></div> + <div>{{ serverSettings.singleUserMode ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>{{ i18n.ts._serverSettings.openRegistration }}:</b></div> + <div>{{ !serverSettings.disableRegistration ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>{{ i18n.ts.emailRequiredForSignup }}:</b></div> + <div>{{ serverSettings.emailRequiredForSignup ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>Log IP:</b></div> + <div>{{ serverSettings.enableIpLogging ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>{{ i18n.ts.federation }}:</b></div> + <div>{{ serverSettings.federation === 'none' ? i18n.ts.no : i18n.ts.all }}</div> + </div> + <div> + <div><b>FTT:</b></div> + <div>{{ serverSettings.enableFanoutTimeline ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>FTT/{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}:</b></div> + <div>{{ serverSettings.enableFanoutTimelineDbFallback ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>RBT:</b></div> + <div>{{ serverSettings.enableReactionsBuffering ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.rateLimitFactor }}:</b></div> + <div>{{ defaultPolicies.rateLimitFactor }}</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.driveCapacity }}:</b></div> + <div>{{ defaultPolicies.driveCapacityMb }} MB</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.userListMax }}:</b></div> + <div>{{ defaultPolicies.userListLimit }}</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.antennaMax }}:</b></div> + <div>{{ defaultPolicies.antennaLimit }}</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.webhookMax }}:</b></div> + <div>{{ defaultPolicies.webhookLimit }}</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportFollowing }}:</b></div> + <div>{{ defaultPolicies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportMuting }}:</b></div> + <div>{{ defaultPolicies.canImportMuting ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportBlocking }}:</b></div> + <div>{{ defaultPolicies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportUserLists }}:</b></div> + <div>{{ defaultPolicies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + <div> + <div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportAntennas }}:</b></div> + <div>{{ defaultPolicies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}</div> + </div> + </div> + + <template #footer> + <MkButton gradate large rounded data-cy-server-setup-wizard-apply style="margin: 0 auto;" @click="applySettings"> + <i class="ti ti-check"></i> {{ i18n.ts._serverSetupWizard.applyTheseSettings }} + </MkButton> + </template> + </MkFolder> +</div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import { ROLE_POLICIES } from '@@/js/const.js'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import MkFolder from '@/components/MkFolder.vue'; +import MkRadios from '@/components/MkRadios.vue'; +import MkInfo from '@/components/MkInfo.vue'; + +const emit = defineEmits<{ + (ev: 'finished'): void; +}>(); + +const props = withDefaults(defineProps<{ + token?: string; +}>(), { +}); + +const q_name = ref(''); +const q_use = ref('single'); +const q_scale = ref('small'); +const q_federation = ref('yes'); +const q_adminName = ref(''); +const q_adminEmail = ref(''); + +const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => { + let enableReactionsBuffering; + if (q_use.value === 'single') { + enableReactionsBuffering = false; + } else { + enableReactionsBuffering = q_scale.value !== 'small'; + } + + return { + singleUserMode: q_use.value === 'single', + disableRegistration: q_use.value !== 'open', + emailRequiredForSignup: q_use.value === 'open', + enableIpLogging: q_use.value === 'open', + federation: q_federation.value === 'yes' ? 'all' : 'none', + enableFanoutTimeline: true, + enableFanoutTimelineDbFallback: q_use.value === 'single', + enableReactionsBuffering, + }; +}); + +const defaultPolicies = computed<Partial<Record<typeof ROLE_POLICIES[number], any>>>(() => { + let driveCapacityMb; + if (q_use.value === 'single') { + driveCapacityMb = 8192; + } else if (q_use.value === 'group') { + driveCapacityMb = 1000; + } else if (q_use.value === 'open') { + driveCapacityMb = 100; + } + + let rateLimitFactor; + if (q_use.value === 'single') { + rateLimitFactor = 0.3; + } else if (q_use.value === 'group') { + rateLimitFactor = 0.7; + } else if (q_use.value === 'open') { + if (q_scale.value === 'small') { + rateLimitFactor = 1; + } else if (q_scale.value === 'medium') { + rateLimitFactor = 1.25; + } else if (q_scale.value === 'large') { + rateLimitFactor = 1.5; + } + } + + let userListLimit; + if (q_use.value === 'single') { + userListLimit = 100; + } else if (q_use.value === 'group') { + userListLimit = 5; + } else if (q_use.value === 'open') { + userListLimit = 3; + } + + let antennaLimit; + if (q_use.value === 'single') { + antennaLimit = 100; + } else if (q_use.value === 'group') { + antennaLimit = 5; + } else if (q_use.value === 'open') { + antennaLimit = 0; + } + + let webhookLimit; + if (q_use.value === 'single') { + webhookLimit = 100; + } else if (q_use.value === 'group') { + webhookLimit = 0; + } else if (q_use.value === 'open') { + webhookLimit = 0; + } + + let canImportFollowing; + if (q_use.value === 'single') { + canImportFollowing = true; + } else { + canImportFollowing = false; + } + + let canImportMuting; + if (q_use.value === 'single') { + canImportMuting = true; + } else { + canImportMuting = false; + } + + let canImportBlocking; + if (q_use.value === 'single') { + canImportBlocking = true; + } else { + canImportBlocking = false; + } + + let canImportUserLists; + if (q_use.value === 'single') { + canImportUserLists = true; + } else { + canImportUserLists = false; + } + + let canImportAntennas; + if (q_use.value === 'single') { + canImportAntennas = true; + } else { + canImportAntennas = false; + } + + return { + rateLimitFactor, + driveCapacityMb, + userListLimit, + antennaLimit, + webhookLimit, + canImportFollowing, + canImportMuting, + canImportBlocking, + canImportUserLists, + canImportAntennas, + }; +}); + +function applySettings() { + const _close = os.waiting(); + Promise.all([ + misskeyApi('admin/update-meta', { + ...serverSettings.value, + name: q_name.value === '' ? undefined : q_name.value, + maintainerName: q_adminName.value === '' ? undefined : q_adminName.value, + maintainerEmail: q_adminEmail.value === '' ? undefined : q_adminEmail.value, + }, props.token), + misskeyApi('admin/roles/update-default-policies', { + policies: defaultPolicies.value, + }, props.token), + ]).then(() => { + emit('finished'); + }).catch((err) => { + os.alert({ + type: 'error', + title: err.code, + text: err.message, + }); + }).finally(() => { + _close(); + }); +} +</script> + +<style lang="scss" module> +.root { +} +</style> diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 813b49635d..d891525782 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -547,18 +547,24 @@ export function success(): Promise<void> { }); } -export function waiting(text?: string | null): Promise<void> { - return new Promise(resolve => { - const showing = ref(true); - const { dispose } = popup(MkWaitingDialog, { - success: false, - showing: showing, - text, - }, { - done: () => resolve(), - closed: () => dispose(), - }); +export function waiting(text?: string | null): () => void { + window.document.body.setAttribute('inert', 'true'); + + const showing = ref(true); + const { dispose } = popup(MkWaitingDialog, { + success: false, + showing: showing, + text, + }, { + closed: () => { + window.document.body.removeAttribute('inert'); + dispose(); + }, }); + + return () => { + showing.value = false; + }; } export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> { diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 69a654595a..675e82a71d 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -6,39 +6,126 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithAnimBg> <div :class="$style.formContainer"> - <form :class="$style.form" class="_panel" @submit.prevent="submit()"> + <div :class="$style.form" class="_panel"> + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="z-index:1;position:relative" viewBox="0 0 854 300"> + <defs> + <linearGradient id="linear" x1="0%" y1="0%" x2="100%" y2="0%"> + <stop offset="0%" stop-color="#86b300"/><stop offset="100%" stop-color="#4ab300"/> + </linearGradient> + </defs> + + <g transform="translate(427, 150) scale(1, 1) translate(-427, -150)"> + <path d="" fill="url(#linear)" opacity="0.4"> + <animate + attributeName="d" + dur="20s" + repeatCount="indefinite" + keyTimes="0;0.333;0.667;1" + calcmod="spline" + keySplines="0.2 0 0.2 1;0.2 0 0.2 1;0.2 0 0.2 1" + begin="0s" + values="M0 0L 0 220Q 213.5 260 427 230T 854 255L 854 0 Z;M0 0L 0 245Q 213.5 260 427 240T 854 230L 854 0 Z;M0 0L 0 265Q 213.5 235 427 265T 854 230L 854 0 Z;M0 0L 0 220Q 213.5 260 427 230T 854 255L 854 0 Z" + > + </animate> + </path> + <path d="" fill="url(#linear)" opacity="0.4"> + <animate + attributeName="d" + dur="20s" + repeatCount="indefinite" + keyTimes="0;0.333;0.667;1" + calcmod="spline" + keySplines="0.2 0 0.2 1;0.2 0 0.2 1;0.2 0 0.2 1" + begin="-10s" + values="M0 0L 0 235Q 213.5 280 427 250T 854 260L 854 0 Z;M0 0L 0 250Q 213.5 220 427 220T 854 240L 854 0 Z;M0 0L 0 245Q 213.5 225 427 250T 854 265L 854 0 Z;M0 0L 0 235Q 213.5 280 427 250T 854 260L 854 0 Z" + > + </animate> + </path> + </g> + </svg> <div :class="$style.title"> <div>Welcome to Misskey!</div> <div :class="$style.version">v{{ version }}</div> </div> - <div class="_gaps_m" style="padding: 32px;"> - <div>{{ i18n.ts.intro }}</div> - <MkInput v-model="setupPassword" type="password" data-cy-admin-initial-password> - <template #label>{{ i18n.ts.initialPasswordForSetup }} <div v-tooltip:dialog="i18n.ts.initialPasswordForSetupDescription" class="_button _help"><i class="ti ti-help-circle"></i></div></template> - <template #prefix><i class="ti ti-lock"></i></template> - </MkInput> - <MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username> - <template #label>{{ i18n.ts.username }}</template> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - </MkInput> - <MkInput v-model="password" type="password" data-cy-admin-password> - <template #label>{{ i18n.ts.password }}</template> - <template #prefix><i class="ti ti-lock"></i></template> - </MkInput> - <div> - <MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;"> - {{ submitting ? i18n.ts.processing : i18n.ts.done }}<MkEllipsis v-if="submitting"/> + <div style="padding: 16px 32px 32px 32px;"> + <form v-if="!accountCreated" class="_gaps_m" @submit.prevent="createAccount()"> + <div style="text-align: center;" class="_gaps_s"> + <div><b>{{ i18n.ts._serverSetupWizard.installCompleted }}</b></div> + <div>{{ i18n.ts._serverSetupWizard.firstCreateAccount }}</div> + </div> + <MkInput v-model="setupPassword" type="password" data-cy-admin-initial-password> + <template #label>{{ i18n.ts.initialPasswordForSetup }} <div v-tooltip:dialog="i18n.ts.initialPasswordForSetupDescription" class="_button _help"><i class="ti ti-help-circle"></i></div></template> + <template #prefix><i class="ti ti-lock"></i></template> + </MkInput> + <MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username> + <template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> + <template #prefix>@</template> + <template #suffix>@{{ host }}</template> + </MkInput> + <MkInput v-model="password" type="password" data-cy-admin-password> + <template #label>{{ i18n.ts.password }}</template> + <template #prefix><i class="ti ti-lock"></i></template> + </MkInput> + <div> + <MkButton gradate large rounded :disabled="accountCreating" data-cy-admin-ok style="margin: 0 auto;" type="submit"> + {{ accountCreating ? i18n.ts.processing : i18n.ts.next }}<MkEllipsis v-if="accountCreating"/> + </MkButton> + </div> + </form> + <div v-else-if="step === 0" class="_gaps_m"> + <div style="text-align: center;" class="_gaps_s"> + <div><b>{{ i18n.ts._serverSetupWizard.accountCreated }}</b></div> + </div> + <MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="step++"> + {{ i18n.ts.next }} + </MkButton> + </div> + <div v-else-if="step === 1" class="_gaps_m"> + <div style="text-align: center;" class="_gaps_s"> + <div><b>{{ i18n.ts._serverSetupWizard.donationRequest }}</b></div> + <div>{{ i18n.ts._serverSetupWizard._donationRequest.text1 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text2 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text3 }}</div> + </div> + <MkLink target="_blank" url="https://misskey-hub.net/docs/donate/" style="margin: 0 auto;">{{ i18n.ts.learnMore }}</MkLink> + <div class="_buttonsCenter"> + <MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="step++"> + {{ i18n.ts.next }} + </MkButton> + </div> + </div> + <div v-else-if="step === 2" class="_gaps_m"> + <div style="text-align: center;" class="_gaps_s"> + <div style="font-size: 120%;"><b>{{ i18n.ts._serverSetupWizard.serverSetting }}</b></div> + <div>{{ i18n.ts._serverSetupWizard.youCanEasilyConfigureOptimalServerSettingsWithThisWizard }}</div> + <div>{{ i18n.ts._serverSetupWizard.settingsYouMakeHereCanBeChangedLater }}</div> + </div> + + <MkServerSetupWizard :token="token" @finished="onWizardFinished"/> + + <MkButton rounded style="margin: 0 auto;" @click="skipSettings"> + {{ i18n.ts._serverSetupWizard.skipSettings }} </MkButton> </div> + <div v-else-if="step === 3" class="_gaps_m"> + <div style="text-align: center;" class="_gaps_s"> + <div><b>{{ i18n.ts._serverSetupWizard.settingsCompleted }}</b></div> + <div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description }}</div> + <div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description2 }}</div> + </div> + <div class="_buttonsCenter"> + <MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="finish"> + {{ i18n.ts.start }} + </MkButton> + </div> + </div> </div> - </form> + </div> </div> </PageWithAnimBg> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { computed, ref } from 'vue'; +import * as Misskey from 'misskey-js'; import { host, version } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -46,24 +133,33 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { login } from '@/accounts.js'; +import MkLink from '@/components/MkLink.vue'; +import MkServerSetupWizard from '@/components/MkServerSetupWizard.vue'; const username = ref(''); const password = ref(''); const setupPassword = ref(''); -const submitting = ref(false); +const accountCreating = ref(false); +const accountCreated = ref(false); +const step = ref(0); + +let token; -function submit() { - if (submitting.value) return; - submitting.value = true; +function createAccount() { + if (accountCreating.value) return; + accountCreating.value = true; + + const _close = os.waiting(); misskeyApi('admin/accounts/create', { username: username.value, password: password.value, setupPassword: setupPassword.value === '' ? null : setupPassword.value, }).then(res => { - return login(res.token); + token = res.token; + accountCreated.value = true; }).catch((err) => { - submitting.value = false; + accountCreating.value = false; let title = i18n.ts.somethingHappened; let text = err.message + '\n' + err.id; @@ -81,8 +177,22 @@ function submit() { title, text, }); + }).finally(() => { + _close(); }); } + +function onWizardFinished() { + step.value++; +} + +function skipSettings() { + step.value++; +} + +function finish() { + login(token); +} </script> <style lang="scss" module> @@ -90,8 +200,7 @@ function submit() { min-height: 100svh; padding: 32px 32px 64px 32px; box-sizing: border-box; - display: grid; - place-content: center; + align-content: center; } .form { @@ -100,16 +209,21 @@ function submit() { border-radius: var(--MI-radius); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); overflow: clip; - max-width: 500px; + max-width: 550px; + margin: 0 auto; } .title { + position: absolute; + top: 16px; + left: 0; + right: 0; + z-index: 1; margin: 0; font-size: 1.5em; text-align: center; padding: 32px; - background: var(--MI_THEME-accentedBg); - color: var(--MI_THEME-accent); + color: #fff; font-weight: bold; } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 4a71dd5465..b2543587cc 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -8780,6 +8780,7 @@ export type operations = { software: string; versionRange: string; }[]; + singleUserMode: boolean; /** @enum {string} */ ugcVisibilityForVisitor: 'all' | 'local' | 'none'; }; @@ -11452,6 +11453,7 @@ export type operations = { software: string; versionRange: string; }[]; + singleUserMode?: boolean; /** @enum {string} */ ugcVisibilityForVisitor?: 'all' | 'local' | 'none'; }; |