add: Require Approval for Signup
This commit is contained in:
parent
7ca0af9e7e
commit
f69b50a83f
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,10 +47,12 @@ 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;
|
||||||
|
const instance = await this.metaService.fetch(true);
|
||||||
|
|
||||||
// Validate username
|
// Validate username
|
||||||
if (!this.userEntityService.validateLocalUsername(username)) {
|
if (!this.userEntityService.validateLocalUsername(username)) {
|
||||||
|
|
@ -84,7 +86,6 @@ export class SignupService {
|
||||||
const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0;
|
const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0;
|
||||||
|
|
||||||
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');
|
||||||
|
|
@ -109,6 +110,9 @@ export class SignupService {
|
||||||
));
|
));
|
||||||
|
|
||||||
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 => {
|
||||||
|
|
@ -126,6 +130,8 @@ export class SignupService {
|
||||||
host: this.utilityService.toPunyNullable(host),
|
host: this.utilityService.toPunyNullable(host),
|
||||||
token: secret,
|
token: secret,
|
||||||
isRoot: isTheFirstUser,
|
isRoot: isTheFirstUser,
|
||||||
|
approved: defaultApproval,
|
||||||
|
signupReason: reason,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await transactionalEntityManager.save(new MiUserKeypair({
|
await transactionalEntityManager.save(new MiUserKeypair({
|
||||||
|
|
|
||||||
|
|
@ -489,6 +489,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: {
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,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,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,9 @@ export class MiUserPending {
|
||||||
length: 128,
|
length: 128,
|
||||||
})
|
})
|
||||||
public password: string;
|
public password: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1000,
|
||||||
|
})
|
||||||
|
public reason: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderatio
|
||||||
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_deleteAccount from './endpoints/admin/delete-account.js';
|
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||||
|
|
@ -430,6 +431,7 @@ const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation
|
||||||
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_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default };
|
const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default };
|
||||||
|
|
@ -795,6 +797,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_showUser,
|
$admin_showUser,
|
||||||
$admin_showUsers,
|
$admin_showUsers,
|
||||||
$admin_suspendUser,
|
$admin_suspendUser,
|
||||||
|
$admin_approveUser,
|
||||||
$admin_unsuspendUser,
|
$admin_unsuspendUser,
|
||||||
$admin_updateMeta,
|
$admin_updateMeta,
|
||||||
$admin_deleteAccount,
|
$admin_deleteAccount,
|
||||||
|
|
@ -1154,6 +1157,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_showUser,
|
$admin_showUser,
|
||||||
$admin_showUsers,
|
$admin_showUsers,
|
||||||
$admin_suspendUser,
|
$admin_suspendUser,
|
||||||
|
$admin_approveUser,
|
||||||
$admin_unsuspendUser,
|
$admin_unsuspendUser,
|
||||||
$admin_updateMeta,
|
$admin_updateMeta,
|
||||||
$admin_deleteAccount,
|
$admin_deleteAccount,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { IdService } from '@/core/IdService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { RateLimiterService } from './RateLimiterService.js';
|
import { RateLimiterService } from './RateLimiterService.js';
|
||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
|
import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
|
||||||
|
|
@ -45,6 +46,7 @@ export class SigninApiService {
|
||||||
private signinService: SigninService,
|
private signinService: SigninService,
|
||||||
private userAuthService: UserAuthService,
|
private userAuthService: UserAuthService,
|
||||||
private webAuthnService: WebAuthnService,
|
private webAuthnService: WebAuthnService,
|
||||||
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +65,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'];
|
||||||
|
|
@ -122,6 +126,17 @@ export class SigninApiService {
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
|
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!);
|
||||||
|
|
||||||
|
|
@ -140,6 +155,7 @@ export class SigninApiService {
|
||||||
|
|
||||||
if (!profile.twoFactorEnabled) {
|
if (!profile.twoFactorEnabled) {
|
||||||
if (same) {
|
if (same) {
|
||||||
|
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||||
return this.signinService.signin(request, reply, user);
|
return this.signinService.signin(request, reply, user);
|
||||||
} else {
|
} else {
|
||||||
return await fail(403, {
|
return await fail(403, {
|
||||||
|
|
@ -163,6 +179,8 @@ export class SigninApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||||
|
|
||||||
return this.signinService.signin(request, reply, user);
|
return this.signinService.signin(request, reply, user);
|
||||||
} else if (body.credential) {
|
} else if (body.credential) {
|
||||||
if (!same && !profile.usePasswordLessLogin) {
|
if (!same && !profile.usePasswordLessLogin) {
|
||||||
|
|
@ -174,6 +192,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 });
|
||||||
return this.signinService.signin(request, reply, user);
|
return this.signinService.signin(request, reply, user);
|
||||||
} else {
|
} else {
|
||||||
return await fail(403, {
|
return await fail(403, {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import instance from './endpoints/charts/instance.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SignupApiService {
|
export class SignupApiService {
|
||||||
|
|
@ -62,6 +63,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;
|
||||||
|
|
@ -99,6 +101,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) {
|
||||||
|
|
@ -114,6 +117,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) {
|
||||||
|
|
@ -182,6 +192,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}`;
|
||||||
|
|
@ -197,6 +208,19 @@ export class SignupApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reply.code(204);
|
||||||
|
return;
|
||||||
|
} 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.');
|
||||||
|
}
|
||||||
|
|
||||||
reply.code(204);
|
reply.code(204);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -234,6 +258,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 });
|
||||||
|
|
||||||
|
|
@ -244,6 +270,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({
|
||||||
|
|
@ -267,6 +294,11 @@ export class SignupApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (instance.approvalRequiredForSignup) {
|
||||||
|
reply.code(204);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return this.signinService.signin(request, reply, account as MiLocalUser);
|
return this.signinService.signin(request, reply, account as MiLocalUser);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderatio
|
||||||
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_deleteAccount from './endpoints/admin/delete-account.js';
|
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||||
|
|
@ -429,6 +430,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/delete-account', ep___admin_deleteAccount],
|
['admin/delete-account', ep___admin_deleteAccount],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
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,
|
||||||
|
} 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -454,6 +458,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,
|
||||||
enableRecaptcha: instance.enableRecaptcha,
|
enableRecaptcha: instance.enableRecaptcha,
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,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,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,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 },
|
||||||
|
|
@ -254,6 +255,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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,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,
|
||||||
|
|
@ -349,6 +353,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,
|
||||||
enableRecaptcha: instance.enableRecaptcha,
|
enableRecaptcha: instance.enableRecaptcha,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export const followersVisibilities = ['public', 'followers', 'private'] as const
|
||||||
export const moderationLogTypes = [
|
export const moderationLogTypes = [
|
||||||
'updateServerSettings',
|
'updateServerSettings',
|
||||||
'suspend',
|
'suspend',
|
||||||
|
'approve',
|
||||||
'unsuspend',
|
'unsuspend',
|
||||||
'updateUserNote',
|
'updateUserNote',
|
||||||
'addCustomEmoji',
|
'addCustomEmoji',
|
||||||
|
|
@ -93,6 +94,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;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
<MkInput v-if="instance.approvalRequiredForSignup" v-model="reason" type="text" :spellcheck="false" required data-cy-signup-reason>
|
||||||
|
<template #label>Reason <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question ph-bold ph-lg"></i></div></template>
|
||||||
|
<template #prefix><i class="ph-envelope ph-bold ph-lg"></i></template>
|
||||||
|
</MkInput>
|
||||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||||
|
|
@ -97,6 +101,7 @@ const props = withDefaults(defineProps<{
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'signup', user: Record<string, any>): void;
|
(ev: 'signup', user: Record<string, any>): void;
|
||||||
(ev: 'signupEmailPending'): void;
|
(ev: 'signupEmailPending'): void;
|
||||||
|
(ev: 'approvalPending'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const host = toUnicode(config.host);
|
const host = toUnicode(config.host);
|
||||||
|
|
@ -109,6 +114,7 @@ const username = ref<string>('');
|
||||||
const password = ref<string>('');
|
const password = ref<string>('');
|
||||||
const retypedPassword = ref<string>('');
|
const retypedPassword = ref<string>('');
|
||||||
const invitationCode = ref<string>('');
|
const invitationCode = ref<string>('');
|
||||||
|
const reason = ref<string>('');
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null);
|
const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null);
|
||||||
const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:banned' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
|
const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:banned' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
|
||||||
|
|
@ -250,6 +256,7 @@ async function onSubmit(): Promise<void> {
|
||||||
password: password.value,
|
password: password.value,
|
||||||
emailAddress: email.value,
|
emailAddress: email.value,
|
||||||
invitationCode: invitationCode.value,
|
invitationCode: invitationCode.value,
|
||||||
|
reason: reason.value,
|
||||||
'hcaptcha-response': hCaptchaResponse.value,
|
'hcaptcha-response': hCaptchaResponse.value,
|
||||||
'g-recaptcha-response': reCaptchaResponse.value,
|
'g-recaptcha-response': reCaptchaResponse.value,
|
||||||
'turnstile-response': turnstileResponse.value,
|
'turnstile-response': turnstileResponse.value,
|
||||||
|
|
@ -261,6 +268,13 @@ async function onSubmit(): Promise<void> {
|
||||||
text: i18n.t('_signup.emailSent', { email: email.value }),
|
text: i18n.t('_signup.emailSent', { email: email.value }),
|
||||||
});
|
});
|
||||||
emit('signupEmailPending');
|
emit('signupEmailPending');
|
||||||
|
} else if (instance.approvalRequiredForSignup) {
|
||||||
|
os.alert({
|
||||||
|
type: 'success',
|
||||||
|
title: i18n.ts._signup.almostThere,
|
||||||
|
text: i18n.t('_signup.emailSent', { email }),
|
||||||
|
});
|
||||||
|
emit('approvalPending');
|
||||||
} else {
|
} else {
|
||||||
const res = await os.api('signin', {
|
const res = await os.api('signin', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/>
|
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending" @approvalPending="onApprovalPending"/>
|
||||||
</template>
|
</template>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,6 +63,9 @@ function onSignup(res) {
|
||||||
function onSignupEmailPending() {
|
function onSignupEmailPending() {
|
||||||
dialog.value.close();
|
dialog.value.close();
|
||||||
}
|
}
|
||||||
|
function onApprovalPending() {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="instance.disableRegistration" :class="$style.mainWarn">
|
<div v-if="instance.disableRegistration" :class="$style.mainWarn">
|
||||||
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="instance.approvalRequiredForSignup" :class="$style.mainWarn">
|
||||||
|
<MkInfo warn>This instance is only accepting users who specify a reason for registration.<br />You must enter a reason during sign up as to why you want to join this instance.</MkInfo>
|
||||||
|
</div>
|
||||||
<div class="_gaps_s" :class="$style.mainActions">
|
<div class="_gaps_s" :class="$style.mainActions">
|
||||||
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
||||||
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||||
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||||
<span class="state">
|
<span class="state">
|
||||||
|
<span v-if="!approved" class="silenced">Not Approved</span>
|
||||||
<span v-if="suspended" class="suspended">Suspended</span>
|
<span v-if="suspended" class="suspended">Suspended</span>
|
||||||
<span v-if="silenced" class="silenced">Silenced</span>
|
<span v-if="silenced" class="silenced">Silenced</span>
|
||||||
<span v-if="moderator" class="moderator">Moderator</span>
|
<span v-if="moderator" class="moderator">Moderator</span>
|
||||||
|
|
@ -197,6 +198,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkObjectView tall :value="user">
|
<MkObjectView tall :value="user">
|
||||||
</MkObjectView>
|
</MkObjectView>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tab === 'approval'" class="_gaps_m">
|
||||||
|
<MkKeyValue oneline>
|
||||||
|
<template #key>Approval Status</template>
|
||||||
|
<template #value><span class="_monospace">{{ approved ? 'Approved' : 'Not Approved' }}</span></template>
|
||||||
|
</MkKeyValue>
|
||||||
|
|
||||||
|
<MkTextarea v-model="signupReason" readonly>
|
||||||
|
<template #label>Reason</template>
|
||||||
|
</MkTextarea>
|
||||||
|
|
||||||
|
<MkButton v-if="$i.isAdmin" inline success @click="approveAccount">Approve</MkButton>
|
||||||
|
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">Deny & Delete</MkButton>
|
||||||
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
|
|
@ -243,8 +258,10 @@ const ips = ref<Misskey.entities.AdminGetUserIpsResponse | null>(null);
|
||||||
const ap = ref<any>(null);
|
const ap = ref<any>(null);
|
||||||
const moderator = ref(false);
|
const moderator = ref(false);
|
||||||
const silenced = ref(false);
|
const silenced = ref(false);
|
||||||
|
const approved = ref(false);
|
||||||
const suspended = ref(false);
|
const suspended = ref(false);
|
||||||
const moderationNote = ref('');
|
const moderationNote = ref('');
|
||||||
|
const signupReason = ref('');
|
||||||
const filesPagination = {
|
const filesPagination = {
|
||||||
endpoint: 'admin/drive/files' as const,
|
endpoint: 'admin/drive/files' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|
@ -274,8 +291,10 @@ function createFetcher() {
|
||||||
ips.value = _ips;
|
ips.value = _ips;
|
||||||
moderator.value = info.value.isModerator;
|
moderator.value = info.value.isModerator;
|
||||||
silenced.value = info.value.isSilenced;
|
silenced.value = info.value.isSilenced;
|
||||||
|
approved.value = info.value.approved;
|
||||||
suspended.value = info.value.isSuspended;
|
suspended.value = info.value.isSuspended;
|
||||||
moderationNote.value = info.value.moderationNote;
|
moderationNote.value = info.value.moderationNote;
|
||||||
|
signupReason.value = info.signupReason;
|
||||||
|
|
||||||
watch(moderationNote, async () => {
|
watch(moderationNote, async () => {
|
||||||
await os.api('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
|
await os.api('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
|
||||||
|
|
@ -405,6 +424,16 @@ async function deleteAccount() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function approveAccount() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.suspendConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
await os.api('admin/approve-user', { userId: user.id });
|
||||||
|
await refreshUser();
|
||||||
|
}
|
||||||
|
|
||||||
async function assignRole() {
|
async function assignRole() {
|
||||||
const roles = await os.api('admin/roles/list');
|
const roles = await os.api('admin/roles/list');
|
||||||
|
|
||||||
|
|
@ -515,7 +544,11 @@ const headerTabs = computed(() => [{
|
||||||
key: 'raw',
|
key: 'raw',
|
||||||
title: 'Raw',
|
title: 'Raw',
|
||||||
icon: 'ti ti-code',
|
icon: 'ti ti-code',
|
||||||
}]);
|
}, (iAmAdmin && !approved) ? {
|
||||||
|
key: 'approval',
|
||||||
|
title: 'Approval',
|
||||||
|
icon: 'ti ti-eye',
|
||||||
|
} : undefined]);
|
||||||
|
|
||||||
definePageMetadata(computed(() => ({
|
definePageMetadata(computed(() => ({
|
||||||
title: user.value ? acct(user.value) : i18n.ts.userInfo,
|
title: user.value ? acct(user.value) : i18n.ts.userInfo,
|
||||||
|
|
@ -606,6 +639,18 @@ definePageMetadata(computed(() => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.casdwq {
|
||||||
|
.silenced {
|
||||||
|
color: var(--warn);
|
||||||
|
border-color: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.moderator {
|
||||||
|
color: var(--success);
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
|
<MkSwitch v-model="approvalRequiredForSignup">
|
||||||
|
<template #label>Require approval for new sign-ups</template>
|
||||||
|
</MkSwitch>
|
||||||
|
|
||||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||||
|
|
||||||
<MkInput v-model="tosUrl" type="url">
|
<MkInput v-model="tosUrl" type="url">
|
||||||
|
|
@ -74,6 +78,7 @@ import FormLink from '@/components/form/link.vue';
|
||||||
|
|
||||||
const enableRegistration = ref<boolean>(false);
|
const enableRegistration = ref<boolean>(false);
|
||||||
const emailRequiredForSignup = ref<boolean>(false);
|
const emailRequiredForSignup = ref<boolean>(false);
|
||||||
|
const approvalRequiredForSignup = ref<boolean>(false);
|
||||||
const sensitiveWords = ref<string>('');
|
const sensitiveWords = ref<string>('');
|
||||||
const hiddenTags = ref<string>('');
|
const hiddenTags = ref<string>('');
|
||||||
const preservedUsernames = ref<string>('');
|
const preservedUsernames = ref<string>('');
|
||||||
|
|
@ -84,6 +89,7 @@ async function init() {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
enableRegistration.value = !meta.disableRegistration;
|
enableRegistration.value = !meta.disableRegistration;
|
||||||
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
||||||
|
approvalRequiredForSignup.value = meta.approvalRequiredForSignup;
|
||||||
sensitiveWords.value = meta.sensitiveWords.join('\n');
|
sensitiveWords.value = meta.sensitiveWords.join('\n');
|
||||||
hiddenTags.value = meta.hiddenTags.join('\n');
|
hiddenTags.value = meta.hiddenTags.join('\n');
|
||||||
preservedUsernames.value = meta.preservedUsernames.join('\n');
|
preservedUsernames.value = meta.preservedUsernames.join('\n');
|
||||||
|
|
@ -95,6 +101,7 @@ function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
disableRegistration: !enableRegistration.value,
|
disableRegistration: !enableRegistration.value,
|
||||||
emailRequiredForSignup: emailRequiredForSignup.value,
|
emailRequiredForSignup: emailRequiredForSignup.value,
|
||||||
|
approvalRequiredForSignup: approvalRequiredForSignup.value,
|
||||||
tosUrl: tosUrl.value,
|
tosUrl: tosUrl.value,
|
||||||
privacyPolicyUrl: privacyPolicyUrl.value,
|
privacyPolicyUrl: privacyPolicyUrl.value,
|
||||||
sensitiveWords: sensitiveWords.value.split('\n'),
|
sensitiveWords: sensitiveWords.value.split('\n'),
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:class="{
|
:class="{
|
||||||
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
|
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
|
||||||
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
|
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
|
||||||
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
|
[$style.logRed]: ['suspend', 'approve', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
|
||||||
}"
|
}"
|
||||||
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
|
<span v-else-if="log.type === 'approve'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
|
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
|
||||||
|
|
@ -68,6 +69,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-else-if="log.type === 'suspend'">
|
<template v-else-if="log.type === 'suspend'">
|
||||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="log.type === 'approve'">
|
||||||
|
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||||
|
</template>
|
||||||
<template v-else-if="log.type === 'unsuspend'">
|
<template v-else-if="log.type === 'unsuspend'">
|
||||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ export const permissions = [
|
||||||
export const moderationLogTypes = [
|
export const moderationLogTypes = [
|
||||||
'updateServerSettings',
|
'updateServerSettings',
|
||||||
'suspend',
|
'suspend',
|
||||||
|
'approve',
|
||||||
'unsuspend',
|
'unsuspend',
|
||||||
'updateUserNote',
|
'updateUserNote',
|
||||||
'addCustomEmoji',
|
'addCustomEmoji',
|
||||||
|
|
@ -145,6 +146,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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue