hippofish/packages/backend/src/core/SignupService.ts

162 lines
4.7 KiB
TypeScript
Raw Normal View History

/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
2022-09-17 20:27:08 +02:00
import { generateKeyPair } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
2022-09-17 20:27:08 +02:00
import { DataSource, IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import { MiUser } from '@/models/User.js';
import { MiUserProfile } from '@/models/UserProfile.js';
2022-09-17 20:27:08 +02:00
import { IdService } from '@/core/IdService.js';
import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js';
2022-09-17 20:27:08 +02:00
import generateUserToken from '@/misc/generate-native-user-token.js';
2022-12-04 02:16:03 +01:00
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
2022-09-17 20:27:08 +02:00
@Injectable()
export class SignupService {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository,
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
2022-09-17 20:27:08 +02:00
private usersChart: UsersChart,
) {
}
@bindThis
2022-09-17 20:27:08 +02:00
public async signup(opts: {
username: MiUser['username'];
2022-09-17 20:27:08 +02:00
password?: string | null;
passwordHash?: MiUserProfile['password'] | null;
2022-09-17 20:27:08 +02:00
host?: string | null;
2023-10-18 02:41:36 +02:00
reason?: string | null;
ignorePreservedUsernames?: boolean;
2022-09-17 20:27:08 +02:00
}) {
2023-10-18 02:41:36 +02:00
const { username, password, passwordHash, host, reason } = opts;
2022-09-17 20:27:08 +02:00
let hash = passwordHash;
2023-10-18 02:41:36 +02:00
const instance = await this.metaService.fetch(true);
2022-09-17 20:27:08 +02:00
// Validate username
if (!this.userEntityService.validateLocalUsername(username)) {
throw new Error('INVALID_USERNAME');
}
2022-09-17 20:27:08 +02:00
if (password != null && passwordHash == null) {
// Validate password
if (!this.userEntityService.validatePassword(password)) {
throw new Error('INVALID_PASSWORD');
}
2022-09-17 20:27:08 +02:00
// Generate hash of password
//const salt = await bcrypt.genSalt(8);
hash = await argon2.hash(password);
2022-09-17 20:27:08 +02:00
}
2022-09-17 20:27:08 +02:00
// Generate secret
const secret = generateUserToken();
2022-09-17 20:27:08 +02:00
// Check username duplication
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
2022-09-17 20:27:08 +02:00
throw new Error('DUPLICATED_USERNAME');
}
2022-09-17 20:27:08 +02:00
// Check deleted username duplication
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
2022-09-17 20:27:08 +02:00
throw new Error('USED_USERNAME');
}
2023-05-02 12:26:18 +02:00
const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0;
2023-05-02 18:21:18 +02:00
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) {
throw new Error('USED_USERNAME');
}
}
2023-05-02 12:26:18 +02:00
2022-09-17 20:27:08 +02:00
const keyPair = await new Promise<string[]>((res, rej) =>
generateKeyPair('rsa', {
2023-07-21 04:59:00 +02:00
modulusLength: 2048,
2022-09-17 20:27:08 +02:00
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: undefined,
passphrase: undefined,
},
}, (err, publicKey, privateKey) =>
2022-09-17 20:27:08 +02:00
err ? rej(err) : res([publicKey, privateKey]),
));
let account!: MiUser;
2023-10-18 02:41:36 +02:00
let defaultApproval = false;
if (!instance.approvalRequiredForSignup) defaultApproval = true;
2022-09-17 20:27:08 +02:00
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(MiUser, {
2022-09-17 20:27:08 +02:00
usernameLower: username.toLowerCase(),
host: IsNull(),
});
2022-09-17 20:27:08 +02:00
if (exist) throw new Error(' the username is already used');
account = await transactionalEntityManager.save(new MiUser({
id: this.idService.gen(),
2022-09-17 20:27:08 +02:00
username: username,
usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host),
token: secret,
2023-05-02 12:26:18 +02:00
isRoot: isTheFirstUser,
2023-10-18 02:41:36 +02:00
approved: defaultApproval,
signupReason: reason,
2022-09-17 20:27:08 +02:00
}));
await transactionalEntityManager.save(new MiUserKeypair({
2022-09-17 20:27:08 +02:00
publicKey: keyPair[0],
privateKey: keyPair[1],
userId: account.id,
}));
await transactionalEntityManager.save(new MiUserProfile({
2022-09-17 20:27:08 +02:00
userId: account.id,
autoAcceptFollowed: true,
password: hash,
}));
await transactionalEntityManager.save(new MiUsedUsername({
2022-09-17 20:27:08 +02:00
createdAt: new Date(),
username: username.toLowerCase(),
}));
});
2022-09-17 20:27:08 +02:00
this.usersChart.update(account, true);
2022-09-17 20:27:08 +02:00
return { account, secret };
}
}