It mostly works. Issues: Error on signup, and missing icons in signup page and admin page
This commit is contained in:
parent
f2eafaab73
commit
49650a1382
19 changed files with 212 additions and 13 deletions
22
packages/backend/migration/1736372582304-approval.js
Normal file
22
packages/backend/migration/1736372582304-approval.js
Normal 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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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({
|
||||||
|
|
|
@ -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}/);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -38,4 +38,9 @@ export class MiUserPending {
|
||||||
length: 128,
|
length: 128,
|
||||||
})
|
})
|
||||||
public password: string;
|
public password: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1000,
|
||||||
|
})
|
||||||
|
public reason: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 [];
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue