Merge branch 'refactor/system-user' into 'develop'
fix: correct user count See merge request firefish/firefish!11124
This commit is contained in:
commit
277bffa08a
15 changed files with 68 additions and 54 deletions
|
@ -1,6 +1,7 @@
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
DELETE FROM "migrations" WHERE name IN (
|
DELETE FROM "migrations" WHERE name IN (
|
||||||
|
'CreateSystemActors1720618854585',
|
||||||
'AddMastodonSubscriptionType1715181461692',
|
'AddMastodonSubscriptionType1715181461692',
|
||||||
'SwSubscriptionAccessToken1709395223611',
|
'SwSubscriptionAccessToken1709395223611',
|
||||||
'UserProfileMentions1711075007936',
|
'UserProfileMentions1711075007936',
|
||||||
|
|
2
packages/backend-rs/index.d.ts
vendored
2
packages/backend-rs/index.d.ts
vendored
|
@ -257,6 +257,8 @@ export interface Config {
|
||||||
userAgent: string
|
userAgent: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare function countLocalUsers(): Promise<number>
|
||||||
|
|
||||||
export declare function countReactions(reactions: Record<string, number>): Record<string, number>
|
export declare function countReactions(reactions: Record<string, number>): Record<string, number>
|
||||||
|
|
||||||
export interface Cpu {
|
export interface Cpu {
|
||||||
|
|
|
@ -366,6 +366,7 @@ module.exports.AntennaSrc = nativeBinding.AntennaSrc
|
||||||
module.exports.ChatEvent = nativeBinding.ChatEvent
|
module.exports.ChatEvent = nativeBinding.ChatEvent
|
||||||
module.exports.ChatIndexEvent = nativeBinding.ChatIndexEvent
|
module.exports.ChatIndexEvent = nativeBinding.ChatIndexEvent
|
||||||
module.exports.checkWordMute = nativeBinding.checkWordMute
|
module.exports.checkWordMute = nativeBinding.checkWordMute
|
||||||
|
module.exports.countLocalUsers = nativeBinding.countLocalUsers
|
||||||
module.exports.countReactions = nativeBinding.countReactions
|
module.exports.countReactions = nativeBinding.countReactions
|
||||||
module.exports.cpuInfo = nativeBinding.cpuInfo
|
module.exports.cpuInfo = nativeBinding.cpuInfo
|
||||||
module.exports.cpuUsage = nativeBinding.cpuUsage
|
module.exports.cpuUsage = nativeBinding.cpuUsage
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::{
|
||||||
config::{local_server_info, CONFIG},
|
config::{local_server_info, CONFIG},
|
||||||
database::db_conn,
|
database::db_conn,
|
||||||
federation::nodeinfo::schema::*,
|
federation::nodeinfo::schema::*,
|
||||||
|
misc,
|
||||||
model::entity::{note, user},
|
model::entity::{note, user},
|
||||||
};
|
};
|
||||||
use sea_orm::prelude::*;
|
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 MONTH: chrono::TimeDelta = chrono::Duration::days(30);
|
||||||
const HALF_YEAR: chrono::TimeDelta = chrono::Duration::days(183);
|
const HALF_YEAR: chrono::TimeDelta = chrono::Duration::days(183);
|
||||||
|
|
||||||
let local_users = user::Entity::find()
|
let local_users = misc::user::count::local_total();
|
||||||
.filter(user::Column::Host.is_null())
|
|
||||||
.count(db);
|
|
||||||
let local_active_halfyear = user::Entity::find()
|
let local_active_halfyear = user::Entity::find()
|
||||||
.filter(user::Column::Host.is_null())
|
.filter(user::Column::Host.is_null())
|
||||||
.filter(user::Column::LastActiveDate.gt(now - HALF_YEAR))
|
.filter(user::Column::LastActiveDate.gt(now - HALF_YEAR))
|
||||||
|
|
|
@ -17,3 +17,4 @@ pub mod reaction;
|
||||||
pub mod remove_old_attestation_challenges;
|
pub mod remove_old_attestation_challenges;
|
||||||
pub mod should_nyaify;
|
pub mod should_nyaify;
|
||||||
pub mod system_info;
|
pub mod system_info;
|
||||||
|
pub mod user;
|
||||||
|
|
18
packages/backend-rs/src/misc/user/count.rs
Normal file
18
packages/backend-rs/src/misc/user/count.rs
Normal file
|
@ -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<u64, DbErr> {
|
||||||
|
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<u32, DbErr> {
|
||||||
|
local_total().await.map(|count| count as u32)
|
||||||
|
}
|
1
packages/backend-rs/src/misc/user/mod.rs
Normal file
1
packages/backend-rs/src/misc/user/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod count;
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { genRsaKeyPair } from "@/misc/gen-key-pair.js";
|
import { genRsaKeyPair } from "@/misc/gen-key-pair.js";
|
||||||
import { User } from "@/models/entities/user.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 { UsedUsername } from "@/models/entities/used-username.js";
|
||||||
import { db } from "@/db/postgre.js";
|
import { db } from "@/db/postgre.js";
|
||||||
|
|
||||||
export async function createSystemUser(username: string) {
|
async function createSystemUser(username: string) {
|
||||||
const password = uuid();
|
const password = uuid();
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
|
@ -20,22 +22,20 @@ export async function createSystemUser(username: string) {
|
||||||
|
|
||||||
const keyPair = await genRsaKeyPair(4096);
|
const keyPair = await genRsaKeyPair(4096);
|
||||||
|
|
||||||
let account!: User;
|
|
||||||
|
|
||||||
const exists = await Users.existsBy({
|
const exists = await Users.existsBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
throw new Error("the user already exists");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await db.transaction(async (transactionalEntityManager) => {
|
await db.transaction(async (transactionalEntityManager) => {
|
||||||
account = await transactionalEntityManager
|
const account = await transactionalEntityManager
|
||||||
.insert(User, {
|
.insert(User, {
|
||||||
id: genIdAt(now),
|
id: genIdAt(now),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
@ -69,6 +69,18 @@ export async function createSystemUser(username: string) {
|
||||||
username: username.toLowerCase(),
|
username: username.toLowerCase(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return account;
|
|
||||||
|
export class CreateSystemActors1720618854585 implements MigrationInterface {
|
||||||
|
public async up(_: QueryRunner): Promise<void> {
|
||||||
|
if (!db.isInitialized) {
|
||||||
|
db.initialize();
|
||||||
|
}
|
||||||
|
await createSystemUser("instance.actor");
|
||||||
|
await createSystemUser("relay.actor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(_: QueryRunner): Promise<void> {
|
||||||
|
/* You don't need to revert this migration. */
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,13 @@ import { User } from "@/models/entities/user.js";
|
||||||
import { Users, UsedUsernames } from "@/models/index.js";
|
import { Users, UsedUsernames } from "@/models/index.js";
|
||||||
import { UserProfile } from "@/models/entities/user-profile.js";
|
import { UserProfile } from "@/models/entities/user-profile.js";
|
||||||
import { IsNull } from "typeorm";
|
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 { UserKeypair } from "@/models/entities/user-keypair.js";
|
||||||
import { UsedUsername } from "@/models/entities/used-username.js";
|
import { UsedUsername } from "@/models/entities/used-username.js";
|
||||||
import { db } from "@/db/postgre.js";
|
import { db } from "@/db/postgre.js";
|
||||||
|
@ -18,9 +24,7 @@ export async function signup(opts: {
|
||||||
const { username, password, passwordHash, host } = opts;
|
const { username, password, passwordHash, host } = opts;
|
||||||
let hash = passwordHash;
|
let hash = passwordHash;
|
||||||
|
|
||||||
const userCount = await Users.countBy({
|
const userCount = await countLocalUsers();
|
||||||
host: IsNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config.maxUserSignups != null && userCount > config.maxUserSignups) {
|
if (config.maxUserSignups != null && userCount > config.maxUserSignups) {
|
||||||
throw new Error("MAX_USERS_REACHED");
|
throw new Error("MAX_USERS_REACHED");
|
||||||
|
@ -103,11 +107,7 @@ export async function signup(opts: {
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host == null ? null : toPuny(host),
|
host: host == null ? null : toPuny(host),
|
||||||
token: secret,
|
token: secret,
|
||||||
isAdmin:
|
isAdmin: (await countLocalUsers()) === 0,
|
||||||
(await Users.countBy({
|
|
||||||
host: IsNull(),
|
|
||||||
isAdmin: true,
|
|
||||||
})) === 0,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import define from "@/server/api/define.js";
|
import define from "@/server/api/define.js";
|
||||||
import { Users } from "@/models/index.js";
|
import { Users } from "@/models/index.js";
|
||||||
import { signup } from "@/server/api/common/signup.js";
|
import { signup } from "@/server/api/common/signup.js";
|
||||||
import { IsNull } from "typeorm";
|
import { countLocalUsers } from "backend-rs";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["admin"],
|
tags: ["admin"],
|
||||||
|
@ -32,11 +32,8 @@ export const paramDef = {
|
||||||
|
|
||||||
export default define(meta, paramDef, async (ps, _me, token) => {
|
export default define(meta, paramDef, async (ps, _me, token) => {
|
||||||
const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null;
|
const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null;
|
||||||
const noUsers =
|
if (!me?.isAdmin && (await countLocalUsers()) !== 0)
|
||||||
(await Users.countBy({
|
throw new Error("access denied");
|
||||||
host: IsNull(),
|
|
||||||
})) === 0;
|
|
||||||
if (!noUsers && !me?.isAdmin) throw new Error("access denied");
|
|
||||||
if (token) throw new Error("access denied");
|
if (token) throw new Error("access denied");
|
||||||
|
|
||||||
const { account, secret } = await signup({
|
const { account, secret } = await signup({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import JSON5 from "json5";
|
import JSON5 from "json5";
|
||||||
import { IsNull, MoreThan } from "typeorm";
|
import { IsNull, MoreThan } from "typeorm";
|
||||||
import { config } from "@/config.js";
|
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 { Ads, Emojis, Users } from "@/models/index.js";
|
||||||
import define from "@/server/api/define.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,
|
instance.privateMode && !me ? [] : instance.pinnedClipId,
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
markLocalFilesNsfwByDefault: instance.markLocalFilesNsfwByDefault,
|
markLocalFilesNsfwByDefault: instance.markLocalFilesNsfwByDefault,
|
||||||
requireSetup:
|
requireSetup: (await countLocalUsers()) === 0,
|
||||||
(await Users.countBy({
|
|
||||||
host: IsNull(),
|
|
||||||
isAdmin: true,
|
|
||||||
})) === 0,
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Instances, Users, Notes } from "@/models/index.js";
|
import { Instances, Users, Notes } from "@/models/index.js";
|
||||||
import define from "@/server/api/define.js";
|
import define from "@/server/api/define.js";
|
||||||
import { IsNull } from "typeorm";
|
import { IsNull } from "typeorm";
|
||||||
|
import { countLocalUsers } from "backend-rs";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
@ -69,11 +70,7 @@ export default define(meta, paramDef, async () => {
|
||||||
// usersCount
|
// usersCount
|
||||||
Users.count(),
|
Users.count(),
|
||||||
// originalUsersCount
|
// originalUsersCount
|
||||||
Users.count({
|
countLocalUsers(),
|
||||||
where: {
|
|
||||||
host: IsNull(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
// instances
|
// instances
|
||||||
Instances.count(),
|
Instances.count(),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { config } from "@/config.js";
|
import { config } from "@/config.js";
|
||||||
import { FILE_TYPE_BROWSERSAFE } from "backend-rs";
|
import { FILE_TYPE_BROWSERSAFE } from "backend-rs";
|
||||||
import { fetchMeta } from "backend-rs";
|
import { countLocalUsers, fetchMeta } from "backend-rs";
|
||||||
import {
|
import {
|
||||||
AnnouncementReads,
|
AnnouncementReads,
|
||||||
Announcements,
|
Announcements,
|
||||||
|
@ -31,7 +31,7 @@ export class MiscHelpers {
|
||||||
public static async getInstance(
|
public static async getInstance(
|
||||||
ctx: MastoContext,
|
ctx: MastoContext,
|
||||||
): Promise<MastodonEntity.Instance> {
|
): Promise<MastodonEntity.Instance> {
|
||||||
const userCount = Users.count({ where: { host: IsNull() } });
|
const userCount = countLocalUsers();
|
||||||
const noteCount = Notes.count({ where: { userHost: IsNull() } });
|
const noteCount = Notes.count({ where: { userHost: IsNull() } });
|
||||||
const instanceCount = Instances.count({ cache: 3600000 });
|
const instanceCount = Instances.count({ cache: 3600000 });
|
||||||
const contact = await Users.findOne({
|
const contact = await Users.findOne({
|
||||||
|
@ -109,7 +109,7 @@ export class MiscHelpers {
|
||||||
public static async getInstanceV2(
|
public static async getInstanceV2(
|
||||||
ctx: MastoContext,
|
ctx: MastoContext,
|
||||||
): Promise<MastodonEntity.InstanceV2> {
|
): Promise<MastodonEntity.InstanceV2> {
|
||||||
const userCount = await Users.count({ where: { host: IsNull() } });
|
const userCount = await countLocalUsers();
|
||||||
const contact = await Users.findOne({
|
const contact = await Users.findOne({
|
||||||
where: {
|
where: {
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { createSystemUser } from "./create-system-user.js";
|
|
||||||
import type { ILocalUser } from "@/models/entities/user.js";
|
import type { ILocalUser } from "@/models/entities/user.js";
|
||||||
import { Users } from "@/models/index.js";
|
import { Users } from "@/models/index.js";
|
||||||
import { Cache } from "@/misc/cache.js";
|
import { Cache } from "@/misc/cache.js";
|
||||||
|
@ -15,14 +14,8 @@ export async function getInstanceActor(): Promise<ILocalUser> {
|
||||||
const user = (await Users.findOneBy({
|
const user = (await Users.findOneBy({
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
username: ACTOR_USERNAME,
|
username: ACTOR_USERNAME,
|
||||||
})) as ILocalUser | undefined;
|
})) as ILocalUser;
|
||||||
|
|
||||||
if (user) {
|
|
||||||
await cache.set(null, user);
|
await cache.set(null, user);
|
||||||
return user;
|
return user;
|
||||||
} else {
|
|
||||||
const created = (await createSystemUser(ACTOR_USERNAME)) as ILocalUser;
|
|
||||||
await cache.set(null, created);
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { Users, Relays } from "@/models/index.js";
|
||||||
import { genId } from "backend-rs";
|
import { genId } from "backend-rs";
|
||||||
import { Cache } from "@/misc/cache.js";
|
import { Cache } from "@/misc/cache.js";
|
||||||
import type { Relay } from "@/models/entities/relay.js";
|
import type { Relay } from "@/models/entities/relay.js";
|
||||||
import { createSystemUser } from "@/services/create-system-user.js";
|
|
||||||
|
|
||||||
const ACTOR_USERNAME = "relay.actor" as const;
|
const ACTOR_USERNAME = "relay.actor" as const;
|
||||||
|
|
||||||
|
@ -23,10 +22,7 @@ export async function getRelayActor(): Promise<ILocalUser> {
|
||||||
username: ACTOR_USERNAME,
|
username: ACTOR_USERNAME,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user) return user as ILocalUser;
|
return user as ILocalUser;
|
||||||
|
|
||||||
const created = await createSystemUser(ACTOR_USERNAME);
|
|
||||||
return created as ILocalUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addRelay(inbox: string) {
|
export async function addRelay(inbox: string) {
|
||||||
|
|
Loading…
Reference in a new issue