From f5c21f6bc421634ec0d26d164d3540e2833d5b09 Mon Sep 17 00:00:00 2001 From: naskya Date: Wed, 10 Jul 2024 22:48:14 +0900 Subject: [PATCH 1/2] refactor (backend): ensure that instance.actor and relay.actor exist --- docs/downgrade.sql | 1 + .../1720618854585-create-system-actors.ts} | 26 ++++++++++++++----- .../backend/src/services/instance-actor.ts | 13 +++------- packages/backend/src/services/relay.ts | 6 +---- 4 files changed, 24 insertions(+), 22 deletions(-) rename packages/backend/src/{services/create-system-user.ts => migration/1720618854585-create-system-actors.ts} (75%) diff --git a/docs/downgrade.sql b/docs/downgrade.sql index a8a949de62..a645eb1b65 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'CreateSystemActors1720618854585', 'AddMastodonSubscriptionType1715181461692', 'SwSubscriptionAccessToken1709395223611', 'UserProfileMentions1711075007936', diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/migration/1720618854585-create-system-actors.ts similarity index 75% rename from packages/backend/src/services/create-system-user.ts rename to packages/backend/src/migration/1720618854585-create-system-actors.ts index 0aabf7cc83..b1db8d6602 100644 --- a/packages/backend/src/services/create-system-user.ts +++ b/packages/backend/src/migration/1720618854585-create-system-actors.ts @@ -1,3 +1,5 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + import { v4 as uuid } from "uuid"; import { genRsaKeyPair } from "@/misc/gen-key-pair.js"; import { User } from "@/models/entities/user.js"; @@ -9,7 +11,7 @@ import { UserKeypair } from "@/models/entities/user-keypair.js"; import { UsedUsername } from "@/models/entities/used-username.js"; import { db } from "@/db/postgre.js"; -export async function createSystemUser(username: string) { +async function createSystemUser(username: string) { const password = uuid(); // Generate hash of password @@ -20,22 +22,20 @@ export async function createSystemUser(username: string) { const keyPair = await genRsaKeyPair(4096); - let account!: User; - const exists = await Users.existsBy({ usernameLower: username.toLowerCase(), host: IsNull(), }); if (exists) { - throw new Error("the user already exists"); + return; } const now = new Date(); // Start transaction await db.transaction(async (transactionalEntityManager) => { - account = await transactionalEntityManager + const account = await transactionalEntityManager .insert(User, { id: genIdAt(now), createdAt: now, @@ -69,6 +69,18 @@ export async function createSystemUser(username: string) { username: username.toLowerCase(), }); }); - - return account; +} + +export class CreateSystemActors1720618854585 implements MigrationInterface { + public async up(_: QueryRunner): Promise { + if (!db.isInitialized) { + db.initialize(); + } + await createSystemUser("instance.actor"); + await createSystemUser("relay.actor"); + } + + public async down(_: QueryRunner): Promise { + /* You don't need to revert this migration. */ + } } diff --git a/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts index a8b34ea57b..8bdcc1d36d 100644 --- a/packages/backend/src/services/instance-actor.ts +++ b/packages/backend/src/services/instance-actor.ts @@ -1,4 +1,3 @@ -import { createSystemUser } from "./create-system-user.js"; import type { ILocalUser } from "@/models/entities/user.js"; import { Users } from "@/models/index.js"; import { Cache } from "@/misc/cache.js"; @@ -15,14 +14,8 @@ export async function getInstanceActor(): Promise { const user = (await Users.findOneBy({ host: IsNull(), username: ACTOR_USERNAME, - })) as ILocalUser | undefined; + })) as ILocalUser; - if (user) { - await cache.set(null, user); - return user; - } else { - const created = (await createSystemUser(ACTOR_USERNAME)) as ILocalUser; - await cache.set(null, created); - return created; - } + await cache.set(null, user); + return user; } diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index bd632ab1c1..cbc90e25ac 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -11,7 +11,6 @@ import { Users, Relays } from "@/models/index.js"; import { genId } from "backend-rs"; import { Cache } from "@/misc/cache.js"; import type { Relay } from "@/models/entities/relay.js"; -import { createSystemUser } from "@/services/create-system-user.js"; const ACTOR_USERNAME = "relay.actor" as const; @@ -23,10 +22,7 @@ export async function getRelayActor(): Promise { username: ACTOR_USERNAME, }); - if (user) return user as ILocalUser; - - const created = await createSystemUser(ACTOR_USERNAME); - return created as ILocalUser; + return user as ILocalUser; } export async function addRelay(inbox: string) { From edea72913e09ffbfb88a985dd3dc04b8351f39ef Mon Sep 17 00:00:00 2001 From: naskya Date: Wed, 10 Jul 2024 23:34:46 +0900 Subject: [PATCH 2/2] fix (backend): count correct local users --- packages/backend-rs/index.d.ts | 2 ++ packages/backend-rs/index.js | 1 + .../src/federation/nodeinfo/generate.rs | 5 ++--- packages/backend-rs/src/misc/mod.rs | 1 + packages/backend-rs/src/misc/user/count.rs | 18 ++++++++++++++++++ packages/backend-rs/src/misc/user/mod.rs | 1 + .../backend/src/server/api/common/signup.ts | 18 +++++++++--------- .../api/endpoints/admin/accounts/create.ts | 9 +++------ .../backend/src/server/api/endpoints/meta.ts | 8 ++------ .../backend/src/server/api/endpoints/stats.ts | 7 ++----- .../src/server/api/mastodon/helpers/misc.ts | 6 +++--- 11 files changed, 44 insertions(+), 32 deletions(-) create mode 100644 packages/backend-rs/src/misc/user/count.rs create mode 100644 packages/backend-rs/src/misc/user/mod.rs diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 3b6d5a7a72..90bc2cfcc1 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -257,6 +257,8 @@ export interface Config { userAgent: string } +export declare function countLocalUsers(): Promise + export declare function countReactions(reactions: Record): Record export interface Cpu { diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 6e0a162fc5..e45060a513 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -366,6 +366,7 @@ module.exports.AntennaSrc = nativeBinding.AntennaSrc module.exports.ChatEvent = nativeBinding.ChatEvent module.exports.ChatIndexEvent = nativeBinding.ChatIndexEvent module.exports.checkWordMute = nativeBinding.checkWordMute +module.exports.countLocalUsers = nativeBinding.countLocalUsers module.exports.countReactions = nativeBinding.countReactions module.exports.cpuInfo = nativeBinding.cpuInfo module.exports.cpuUsage = nativeBinding.cpuUsage diff --git a/packages/backend-rs/src/federation/nodeinfo/generate.rs b/packages/backend-rs/src/federation/nodeinfo/generate.rs index 7b1c858095..86dadfdc58 100644 --- a/packages/backend-rs/src/federation/nodeinfo/generate.rs +++ b/packages/backend-rs/src/federation/nodeinfo/generate.rs @@ -4,6 +4,7 @@ use crate::{ config::{local_server_info, CONFIG}, database::db_conn, federation::nodeinfo::schema::*, + misc, model::entity::{note, user}, }; use sea_orm::prelude::*; @@ -33,9 +34,7 @@ async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> { const MONTH: chrono::TimeDelta = chrono::Duration::days(30); const HALF_YEAR: chrono::TimeDelta = chrono::Duration::days(183); - let local_users = user::Entity::find() - .filter(user::Column::Host.is_null()) - .count(db); + let local_users = misc::user::count::local_total(); let local_active_halfyear = user::Entity::find() .filter(user::Column::Host.is_null()) .filter(user::Column::LastActiveDate.gt(now - HALF_YEAR)) diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 37cdbee45a..afa3b1396e 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -17,3 +17,4 @@ pub mod reaction; pub mod remove_old_attestation_challenges; pub mod should_nyaify; pub mod system_info; +pub mod user; diff --git a/packages/backend-rs/src/misc/user/count.rs b/packages/backend-rs/src/misc/user/count.rs new file mode 100644 index 0000000000..bcea6bad9d --- /dev/null +++ b/packages/backend-rs/src/misc/user/count.rs @@ -0,0 +1,18 @@ +use crate::{database::db_conn, model::entity::user}; +use sea_orm::prelude::*; + +// @instance.actor and @relay.actor are not real users +const NUMBER_OF_SYSTEM_ACTORS: u64 = 2; + +pub async fn local_total() -> Result { + user::Entity::find() + .filter(user::Column::Host.is_null()) + .count(db_conn().await?) + .await + .map(|count| count - NUMBER_OF_SYSTEM_ACTORS) +} + +#[macros::ts_export(js_name = "countLocalUsers")] +pub async fn local_total_js() -> Result { + local_total().await.map(|count| count as u32) +} diff --git a/packages/backend-rs/src/misc/user/mod.rs b/packages/backend-rs/src/misc/user/mod.rs new file mode 100644 index 0000000000..16ee43aeca --- /dev/null +++ b/packages/backend-rs/src/misc/user/mod.rs @@ -0,0 +1 @@ +pub mod count; diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts index 05e92bcbea..687a22223e 100644 --- a/packages/backend/src/server/api/common/signup.ts +++ b/packages/backend/src/server/api/common/signup.ts @@ -3,7 +3,13 @@ import { User } from "@/models/entities/user.js"; import { Users, UsedUsernames } from "@/models/index.js"; import { UserProfile } from "@/models/entities/user-profile.js"; import { IsNull } from "typeorm"; -import { genIdAt, generateUserToken, hashPassword, toPuny } from "backend-rs"; +import { + countLocalUsers, + genIdAt, + generateUserToken, + hashPassword, + toPuny, +} from "backend-rs"; import { UserKeypair } from "@/models/entities/user-keypair.js"; import { UsedUsername } from "@/models/entities/used-username.js"; import { db } from "@/db/postgre.js"; @@ -18,9 +24,7 @@ export async function signup(opts: { const { username, password, passwordHash, host } = opts; let hash = passwordHash; - const userCount = await Users.countBy({ - host: IsNull(), - }); + const userCount = await countLocalUsers(); if (config.maxUserSignups != null && userCount > config.maxUserSignups) { throw new Error("MAX_USERS_REACHED"); @@ -103,11 +107,7 @@ export async function signup(opts: { usernameLower: username.toLowerCase(), host: host == null ? null : toPuny(host), token: secret, - isAdmin: - (await Users.countBy({ - host: IsNull(), - isAdmin: true, - })) === 0, + isAdmin: (await countLocalUsers()) === 0, }), ); diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index c5a990c941..db70d1e62b 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -1,7 +1,7 @@ import define from "@/server/api/define.js"; import { Users } from "@/models/index.js"; import { signup } from "@/server/api/common/signup.js"; -import { IsNull } from "typeorm"; +import { countLocalUsers } from "backend-rs"; export const meta = { tags: ["admin"], @@ -32,11 +32,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, _me, token) => { const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null; - const noUsers = - (await Users.countBy({ - host: IsNull(), - })) === 0; - if (!noUsers && !me?.isAdmin) throw new Error("access denied"); + if (!me?.isAdmin && (await countLocalUsers()) !== 0) + throw new Error("access denied"); if (token) throw new Error("access denied"); const { account, secret } = await signup({ diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index a9622c2ed3..5c74bb32df 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,7 +1,7 @@ import JSON5 from "json5"; import { IsNull, MoreThan } from "typeorm"; import { config } from "@/config.js"; -import { fetchMeta } from "backend-rs"; +import { countLocalUsers, fetchMeta } from "backend-rs"; import { Ads, Emojis, Users } from "@/models/index.js"; import define from "@/server/api/define.js"; @@ -501,11 +501,7 @@ export default define(meta, paramDef, async (ps, me) => { instance.privateMode && !me ? [] : instance.pinnedClipId, cacheRemoteFiles: instance.cacheRemoteFiles, markLocalFilesNsfwByDefault: instance.markLocalFilesNsfwByDefault, - requireSetup: - (await Users.countBy({ - host: IsNull(), - isAdmin: true, - })) === 0, + requireSetup: (await countLocalUsers()) === 0, } : {}), }; diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index e0e4b4ec8a..794eef8f4e 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,6 +1,7 @@ import { Instances, Users, Notes } from "@/models/index.js"; import define from "@/server/api/define.js"; import { IsNull } from "typeorm"; +import { countLocalUsers } from "backend-rs"; export const meta = { requireCredential: false, @@ -69,11 +70,7 @@ export default define(meta, paramDef, async () => { // usersCount Users.count(), // originalUsersCount - Users.count({ - where: { - host: IsNull(), - }, - }), + countLocalUsers(), // instances Instances.count(), ]); diff --git a/packages/backend/src/server/api/mastodon/helpers/misc.ts b/packages/backend/src/server/api/mastodon/helpers/misc.ts index 3af8b78dce..96d6251252 100644 --- a/packages/backend/src/server/api/mastodon/helpers/misc.ts +++ b/packages/backend/src/server/api/mastodon/helpers/misc.ts @@ -1,6 +1,6 @@ import { config } from "@/config.js"; import { FILE_TYPE_BROWSERSAFE } from "backend-rs"; -import { fetchMeta } from "backend-rs"; +import { countLocalUsers, fetchMeta } from "backend-rs"; import { AnnouncementReads, Announcements, @@ -31,7 +31,7 @@ export class MiscHelpers { public static async getInstance( ctx: MastoContext, ): Promise { - const userCount = Users.count({ where: { host: IsNull() } }); + const userCount = countLocalUsers(); const noteCount = Notes.count({ where: { userHost: IsNull() } }); const instanceCount = Instances.count({ cache: 3600000 }); const contact = await Users.findOne({ @@ -109,7 +109,7 @@ export class MiscHelpers { public static async getInstanceV2( ctx: MastoContext, ): Promise { - const userCount = await Users.count({ where: { host: IsNull() } }); + const userCount = await countLocalUsers(); const contact = await Users.findOne({ where: { host: IsNull(),