diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-05-09 19:41:54 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-09 19:41:54 +0900 |
| commit | 96bc042d67a6175e0d899b7f7ce7d13de3a0dbf2 (patch) | |
| tree | b96466858e471fe789741694d2651bd1349183e6 /packages | |
| parent | fix: exported antenna data cannot be imported in some cases (#15985) (diff) | |
| download | misskey-96bc042d67a6175e0d899b7f7ce7d13de3a0dbf2.tar.gz misskey-96bc042d67a6175e0d899b7f7ce7d13de3a0dbf2.tar.bz2 misskey-96bc042d67a6175e0d899b7f7ce7d13de3a0dbf2.zip | |
Feat: サーバー初期設定ウィザード (#15954)
* wip
* wip
* Update welcome.setup.vue
* wip
* wip
* wip
* wip
* Update MkServerSetupWizard.vue
* Update MkServerSetupWizard.vue
* wip
* wip
* wip
* Update types.ts
* wip
* wip
* Update CHANGELOG.md
Diffstat (limited to 'packages')
| -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 |
10 files changed, 573 insertions, 49 deletions
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'; }; |