It mostly works. Issues: Error on signup, and missing icons in signup page and admin page

This commit is contained in:
Leah 2025-01-09 00:53:20 +01:00
parent f2eafaab73
commit 49650a1382
19 changed files with 212 additions and 13 deletions

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ApprovalSignup1697580470000 {
name = 'ApprovalSignup1697580470000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "approvalRequiredForSignup" boolean DEFAULT false NOT NULL`);
await queryRunner.query(`ALTER TABLE "user" ADD "approved" boolean DEFAULT false NOT NULL`);
await queryRunner.query(`ALTER TABLE "user" ADD "signupReason" character varying(1000) NULL`);
await queryRunner.query(`ALTER TABLE "user_pending" ADD "reason" character varying(1000) NULL`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "approvalRequiredForSignup"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "approved"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "signupReason"`);
await queryRunner.query(`ALTER TABLE "user_pending" DROP COLUMN "reason"`);
}
}

View file

@ -58,9 +58,10 @@ export class SignupService {
password?: string | null; password?: string | null;
passwordHash?: MiUserProfile['password'] | null; passwordHash?: MiUserProfile['password'] | null;
host?: string | null; host?: string | null;
reason?: string | null;
ignorePreservedUsernames?: boolean; ignorePreservedUsernames?: boolean;
}) { }) {
const { username, password, passwordHash, host } = opts; const { username, password, passwordHash, host, reason } = opts;
let hash = passwordHash; let hash = passwordHash;
// Validate username // Validate username
@ -93,9 +94,9 @@ export class SignupService {
} }
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
const instance = await this.metaService.fetch(true);
if (!opts.ignorePreservedUsernames && !isTheFirstUser) { if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
const instance = await this.metaService.fetch(true);
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) { if (isPreserved) {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
@ -128,6 +129,10 @@ export class SignupService {
try { try {
let account!: MiUser; let account!: MiUser;
let defaultApproval = false;
if (!instance.approvalRequiredForSignup) defaultApproval = true;
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(MiUser, { const exist = await transactionalEntityManager.findOneBy(MiUser, {
@ -144,6 +149,8 @@ export class SignupService {
host: host ? this.utilityService.normalizeHost(host) : null, host: host ? this.utilityService.normalizeHost(host) : null,
token: secret, token: secret,
isRoot: isTheFirstUser, isRoot: isTheFirstUser,
approved: defaultApproval,
signupReason: reason,
})); }));
await transactionalEntityManager.save(new MiUserKeypair({ await transactionalEntityManager.save(new MiUserKeypair({

View file

@ -328,8 +328,8 @@ export class ApPersonService implements OnModuleInit {
this.logger.error('error occurred while fetching following/followers collection', { error: err }); this.logger.error('error occurred while fetching following/followers collection', { error: err });
} }
return 'private'; return 'private';
}) }),
) ),
); );
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@ -378,6 +378,7 @@ export class ApPersonService implements OnModuleInit {
alsoKnownAs: person.alsoKnownAs, alsoKnownAs: person.alsoKnownAs,
isExplorable: person.discoverable, isExplorable: person.discoverable,
username: person.preferredUsername, username: person.preferredUsername,
approved: true,
usernameLower: person.preferredUsername?.toLowerCase(), usernameLower: person.preferredUsername?.toLowerCase(),
host, host,
inbox: person.inbox, inbox: person.inbox,
@ -526,8 +527,8 @@ export class ApPersonService implements OnModuleInit {
return undefined; return undefined;
} }
return 'private'; return 'private';
}) }),
) ),
); );
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);

View file

@ -68,6 +68,7 @@ export class MetaEntityService {
privacyPolicyUrl: instance.privacyPolicyUrl, privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration, disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
approvalRequiredForSignup: instance.approvalRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha, enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey, hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha, enableMcaptcha: instance.enableMcaptcha,
@ -140,6 +141,7 @@ export class MetaEntityService {
globalTimeline: instance.policies.gtlAvailable, globalTimeline: instance.policies.gtlAvailable,
registration: !instance.disableRegistration, registration: !instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
approvalRequiredForSignup: instance.approvalRequiredForSignup,
hCaptcha: instance.enableHcaptcha, hCaptcha: instance.enableHcaptcha,
hcaptcha: instance.enableHcaptcha, hcaptcha: instance.enableHcaptcha,
mCaptcha: instance.enableMcaptcha, mCaptcha: instance.enableMcaptcha,

View file

@ -614,6 +614,8 @@ export class UserEntityService implements OnModuleInit {
...(opts.includeSecrets ? { ...(opts.includeSecrets ? {
email: profile!.email, email: profile!.email,
emailVerified: profile!.emailVerified, emailVerified: profile!.emailVerified,
approved: user.approved,
signupReason: user.signupReason,
securityKeysList: profile!.twoFactorEnabled securityKeysList: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.find({ ? this.userSecurityKeysRepository.find({
where: { where: {
@ -704,7 +706,7 @@ export class UserEntityService implements OnModuleInit {
} }
} }
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes })))) return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
.filter(result => result.status === 'fulfilled') .filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value); .map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
} }

View file

@ -184,6 +184,11 @@ export class MiMeta {
}) })
public emailRequiredForSignup: boolean; public emailRequiredForSignup: boolean;
@Column('boolean', {
default: false,
})
public approvalRequiredForSignup: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View file

@ -261,6 +261,16 @@ export class MiUser {
}) })
public token: string | null; public token: string | null;
@Column('boolean', {
default: false,
})
public approved: boolean;
@Column('varchar', {
length: 1000, nullable: true,
})
public signupReason: string | null;
constructor(data: Partial<MiUser>) { constructor(data: Partial<MiUser>) {
if (data == null) return; if (data == null) return;

View file

@ -38,4 +38,9 @@ export class MiUserPending {
length: 128, length: 128,
}) })
public password: string; public password: string;
@Column('varchar', {
length: 1000,
})
public reason: string;
} }

View file

@ -75,6 +75,10 @@ export const packedMetaLiteSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
approvalRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: { enableHcaptcha: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -275,6 +279,10 @@ export const packedMetaDetailedOnlySchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
approvalRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
localTimeline: { localTimeline: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -82,6 +82,7 @@ import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user
import * as ep___admin_showUser from './endpoints/admin/show-user.js'; import * as ep___admin_showUser from './endpoints/admin/show-user.js';
import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js'; import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js';
@ -477,6 +478,7 @@ const $admin_showUserAccountMoveLogs: Provider = { provide: 'ep:admin/show-user-
const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default }; const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default };
const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default }; const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default };
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
const $admin_approveUser: Provider = { provide: 'ep:admin/approve-user', useClass: ep___admin_approveUser.default };
const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default }; const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default }; const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
const $admin_updateUserName: Provider = { provide: 'ep:admin/update-user-name', useClass: ep___admin_updateUserName.default }; const $admin_updateUserName: Provider = { provide: 'ep:admin/update-user-name', useClass: ep___admin_updateUserName.default };
@ -876,6 +878,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_showUser, $admin_showUser,
$admin_showUsers, $admin_showUsers,
$admin_suspendUser, $admin_suspendUser,
$admin_approveUser,
$admin_unsuspendUser, $admin_unsuspendUser,
$admin_updateMeta, $admin_updateMeta,
$admin_updateUserName, $admin_updateUserName,
@ -1269,6 +1272,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_showUser, $admin_showUser,
$admin_showUsers, $admin_showUsers,
$admin_suspendUser, $admin_suspendUser,
$admin_approveUser,
$admin_unsuspendUser, $admin_unsuspendUser,
$admin_updateMeta, $admin_updateMeta,
$admin_updateUserName, $admin_updateUserName,

View file

@ -79,6 +79,8 @@ export class SigninApiService {
reply.header('Access-Control-Allow-Origin', this.config.url); reply.header('Access-Control-Allow-Origin', this.config.url);
reply.header('Access-Control-Allow-Credentials', 'true'); reply.header('Access-Control-Allow-Credentials', 'true');
const instance = await this.metaService.fetch(true);
const body = request.body; const body = request.body;
const username = body['username']; const username = body['username'];
const password = body['password']; const password = body['password'];
@ -132,13 +134,13 @@ export class SigninApiService {
emailVerified: true, emailVerified: true,
user: { user: {
host: IsNull(), host: IsNull(),
} },
} : { } : {
user: { user: {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: IsNull(), host: IsNull(),
} },
} },
}); });
const user = (profile?.user as MiLocalUser) ?? null; const user = (profile?.user as MiLocalUser) ?? null;
@ -163,6 +165,17 @@ export class SigninApiService {
}); });
} }
if (!user.approved && instance.approvalRequiredForSignup) {
reply.code(403);
return {
error: {
message: 'The account has not been approved by an admin yet. Try again later.',
code: 'NOT_APPROVED',
id: '22d05606-fbcf-421a-a2db-b32241faft1b',
},
};
}
// Compare password // Compare password
const same = await bcrypt.compare(password, profile.password!); const same = await bcrypt.compare(password, profile.password!);
@ -207,6 +220,7 @@ export class SigninApiService {
} }
if (same) { if (same) {
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
logger.info('Successfully signed in with password.'); logger.info('Successfully signed in with password.');
return this.signinService.signin(request, reply, user); return this.signinService.signin(request, reply, user);
} else { } else {
@ -234,6 +248,7 @@ export class SigninApiService {
}); });
} }
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
logger.info('Successfully signed in with password and two-factor token.'); logger.info('Successfully signed in with password and two-factor token.');
return this.signinService.signin(request, reply, user); return this.signinService.signin(request, reply, user);
} else if (body.credential) { } else if (body.credential) {
@ -247,6 +262,7 @@ export class SigninApiService {
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential); const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
if (authorized) { if (authorized) {
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
logger.info('Successfully signed in with WebAuthn authentication.'); logger.info('Successfully signed in with WebAuthn authentication.');
return this.signinService.signin(request, reply, user); return this.signinService.signin(request, reply, user);
} else { } else {

View file

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
@ -21,8 +22,8 @@ import { bindThis } from '@/decorators.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import instance from './endpoints/charts/instance.js';
import type { FastifyRequest, FastifyReply } from 'fastify'; import type { FastifyRequest, FastifyReply } from 'fastify';
import { randomUUID } from 'node:crypto';
@Injectable() @Injectable()
export class SignupApiService { export class SignupApiService {
@ -65,6 +66,7 @@ export class SignupApiService {
host?: string; host?: string;
invitationCode?: string; invitationCode?: string;
emailAddress?: string; emailAddress?: string;
reason?: string;
'hcaptcha-response'?: string; 'hcaptcha-response'?: string;
'g-recaptcha-response'?: string; 'g-recaptcha-response'?: string;
'turnstile-response'?: string; 'turnstile-response'?: string;
@ -117,6 +119,7 @@ export class SignupApiService {
const password = body['password']; const password = body['password'];
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null; const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null;
const invitationCode = body['invitationCode']; const invitationCode = body['invitationCode'];
const reason = body['reason'];
const emailAddress = body['emailAddress']; const emailAddress = body['emailAddress'];
if (instance.emailRequiredForSignup) { if (instance.emailRequiredForSignup) {
@ -134,6 +137,13 @@ export class SignupApiService {
} }
} }
if (instance.approvalRequiredForSignup) {
if (reason == null || typeof reason !== 'string') {
reply.code(400);
return;
}
}
let ticket: MiRegistrationTicket | null = null; let ticket: MiRegistrationTicket | null = null;
if (instance.disableRegistration) { if (instance.disableRegistration) {
@ -211,6 +221,7 @@ export class SignupApiService {
email: emailAddress!, email: emailAddress!,
username: username, username: username,
password: hash, password: hash,
reason: reason,
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
const link = `${this.config.url}/signup-complete/${code}`; const link = `${this.config.url}/signup-complete/${code}`;
@ -231,6 +242,17 @@ export class SignupApiService {
reply.code(204); reply.code(204);
return; return;
} else { } else {
if (instance.approvalRequiredForSignup) {
await this.signupService.signup({
username, password, host, reason,
});
if (emailAddress) {
this.emailService.sendEmail(emailAddress, 'Approval pending',
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.',
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.');
}
}
try { try {
const { account, secret } = await this.signupService.signup({ const { account, secret } = await this.signupService.signup({
username, password, host, username, password, host,
@ -272,6 +294,8 @@ export class SignupApiService {
const code = body['code']; const code = body['code'];
const instance = await this.metaService.fetch(true);
try { try {
const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code });
@ -283,6 +307,7 @@ export class SignupApiService {
const { account, secret } = await this.signupService.signup({ const { account, secret } = await this.signupService.signup({
username: pendingUser.username, username: pendingUser.username,
passwordHash: pendingUser.password, passwordHash: pendingUser.password,
reason: pendingUser.reason,
}); });
this.userPendingsRepository.delete({ this.userPendingsRepository.delete({
@ -306,6 +331,15 @@ export class SignupApiService {
}); });
} }
if (instance.approvalRequiredForSignup) {
if (pendingUser.email) {
this.emailService.sendEmail(pendingUser.email, 'Approval pending',
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.',
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.');
}
return { pendingApproval: true };
}
logger.info('Successfully created user.', { userId: account.id }); logger.info('Successfully created user.', { userId: account.id });
return this.signinService.signin(request, reply, account as MiLocalUser); return this.signinService.signin(request, reply, account as MiLocalUser);
} catch (err) { } catch (err) {

View file

@ -82,6 +82,7 @@ import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user
import * as ep___admin_showUser from './endpoints/admin/show-user.js'; import * as ep___admin_showUser from './endpoints/admin/show-user.js';
import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js'; import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js';
@ -475,6 +476,7 @@ const eps = [
['admin/show-user', ep___admin_showUser], ['admin/show-user', ep___admin_showUser],
['admin/show-users', ep___admin_showUsers], ['admin/show-users', ep___admin_showUsers],
['admin/suspend-user', ep___admin_suspendUser], ['admin/suspend-user', ep___admin_suspendUser],
['admin/approve-user', ep___admin_approveUser],
['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/unsuspend-user', ep___admin_unsuspendUser],
['admin/update-meta', ep___admin_updateMeta], ['admin/update-meta', ep___admin_updateMeta],
['admin/update-user-name', ep___admin_updateUserName], ['admin/update-user-name', ep___admin_updateUserName],

View file

@ -0,0 +1,62 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { DI } from '@/di-symbols.js';
import { EmailService } from '@/core/EmailService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:approve-user',
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private moderationLogService: ModerationLogService,
private emailService: EmailService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
const profile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
await this.usersRepository.update(user.id, {
approved: true,
});
if (profile?.email) {
this.emailService.sendEmail(profile.email, 'Account Approved',
'Your Account has been approved have fun socializing!',
'Your Account has been approved have fun socializing!');
}
this.moderationLogService.log(me, 'approve', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
});
}
}

View file

@ -33,6 +33,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
approvalRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: { enableHcaptcha: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -552,6 +556,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
privacyPolicyUrl: instance.privacyPolicyUrl, privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration, disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
approvalRequiredForSignup: instance.approvalRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha, enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey, hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha, enableMcaptcha: instance.enableMcaptcha,

View file

@ -180,7 +180,7 @@ export const meta = {
memo: { memo: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
} },
}, },
}, },
}, },
@ -240,6 +240,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return { return {
email: profile.email, email: profile.email,
emailVerified: profile.emailVerified, emailVerified: profile.emailVerified,
approved: user.approved,
signupReason: user.signupReason,
autoAcceptFollowed: profile.autoAcceptFollowed, autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle, noCrawle: profile.noCrawle,
preventAiLearning: profile.preventAiLearning, preventAiLearning: profile.preventAiLearning,

View file

@ -35,7 +35,7 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 }, offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+lastActiveDate', '-lastActiveDate'] }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+lastActiveDate', '-lastActiveDate'] },
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended'], default: 'all' }, state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended', 'approved'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
username: { type: 'string', nullable: true, default: null }, username: { type: 'string', nullable: true, default: null },
hostname: { hostname: {
@ -64,6 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case 'available': query.where('user.isSuspended = FALSE'); break; case 'available': query.where('user.isSuspended = FALSE'); break;
case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
case 'suspended': query.where('user.isSuspended = TRUE'); break; case 'suspended': query.where('user.isSuspended = TRUE'); break;
case 'approved': query.where('user.approved = FALSE'); break;
case 'admin': { case 'admin': {
const adminIds = await this.roleService.getAdministratorIds(); const adminIds = await this.roleService.getAdministratorIds();
if (adminIds.length === 0) return []; if (adminIds.length === 0) return [];

View file

@ -65,6 +65,7 @@ export const paramDef = {
cacheRemoteFiles: { type: 'boolean' }, cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' }, cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' },
approvalRequiredForSignup: { type: 'boolean' },
enableHcaptcha: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' },
hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSiteKey: { type: 'string', nullable: true },
hcaptchaSecretKey: { type: 'string', nullable: true }, hcaptchaSecretKey: { type: 'string', nullable: true },
@ -323,6 +324,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.emailRequiredForSignup = ps.emailRequiredForSignup; set.emailRequiredForSignup = ps.emailRequiredForSignup;
} }
if (ps.approvalRequiredForSignup !== undefined) {
set.approvalRequiredForSignup = ps.approvalRequiredForSignup;
}
if (ps.enableHcaptcha !== undefined) { if (ps.enableHcaptcha !== undefined) {
set.enableHcaptcha = ps.enableHcaptcha; set.enableHcaptcha = ps.enableHcaptcha;
} }

View file

@ -54,6 +54,7 @@ export const followersVisibilities = ['public', 'followers', 'private'] as const
export const moderationLogTypes = [ export const moderationLogTypes = [
'updateServerSettings', 'updateServerSettings',
'suspend', 'suspend',
'approve',
'unsuspend', 'unsuspend',
'updateUserName', 'updateUserName',
'updateUserNote', 'updateUserNote',
@ -111,6 +112,11 @@ export type ModerationLogPayloads = {
userUsername: string; userUsername: string;
userHost: string | null; userHost: string | null;
}; };
approve: {
userId: string;
userUsername: string;
userHost: string | null;
};
unsuspend: { unsuspend: {
userId: string; userId: string;
userUsername: string; userUsername: string;