From 85008303f5c292dc0f288e4db5b5a1fbd56879eb Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 22 Jul 2019 10:15:00 +0900
Subject: [PATCH] Prevent username reusing

---
 CHANGELOG.md                                  |  5 +++++
 migration/1563757595828-UsedUsername.ts       | 13 ++++++++++++
 src/db/postgre.ts                             |  2 ++
 src/models/entities/used-username.ts          | 20 +++++++++++++++++++
 src/models/index.ts                           |  2 ++
 .../api/endpoints/username/available.ts       |  6 ++++--
 src/server/api/private/signup.ts              | 14 ++++++++++++-
 7 files changed, 59 insertions(+), 3 deletions(-)
 create mode 100644 migration/1563757595828-UsedUsername.ts
 create mode 100644 src/models/entities/used-username.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00f20d14ce..de8db29bb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
 ChangeLog
 =========
 
+unreleased
+--------------------
+### 🐛Fixes
+* すでに使われたことのあるユーザー名を再度使えないように
+
 11.26.1 (2019/07/21)
 --------------------
 ### 🐛Fixes
diff --git a/migration/1563757595828-UsedUsername.ts b/migration/1563757595828-UsedUsername.ts
new file mode 100644
index 0000000000..a076fcc0c5
--- /dev/null
+++ b/migration/1563757595828-UsedUsername.ts
@@ -0,0 +1,13 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class UsedUsername1563757595828 implements MigrationInterface {
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`CREATE TABLE "used_username" ("username" character varying(128) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_78fd79d2d24c6ac2f4cc9a31a5d" PRIMARY KEY ("username"))`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`DROP TABLE "used_username"`);
+    }
+
+}
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 16cfbd2b2f..00476b8774 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -48,6 +48,7 @@ import { AttestationChallenge } from '../models/entities/attestation-challenge';
 import { Page } from '../models/entities/page';
 import { PageLike } from '../models/entities/page-like';
 import { ModerationLog } from '../models/entities/moderation-log';
+import { UsedUsername } from '../models/entities/used-username';
 
 const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
 
@@ -100,6 +101,7 @@ export const entities = [
 	UserGroupInvite,
 	UserNotePining,
 	UserSecurityKey,
+	UsedUsername,
 	AttestationChallenge,
 	Following,
 	FollowRequest,
diff --git a/src/models/entities/used-username.ts b/src/models/entities/used-username.ts
new file mode 100644
index 0000000000..eb90bef6ca
--- /dev/null
+++ b/src/models/entities/used-username.ts
@@ -0,0 +1,20 @@
+import { PrimaryColumn, Entity, Column } from 'typeorm';
+
+@Entity()
+export class UsedUsername {
+	@PrimaryColumn('varchar', {
+		length: 128,
+	})
+	public username: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	constructor(data: Partial<UsedUsername>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/src/models/index.ts b/src/models/index.ts
index 388bdc8f6f..fc40ebfb23 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -43,6 +43,7 @@ import { HashtagRepository } from './repositories/hashtag';
 import { PageRepository } from './repositories/page';
 import { PageLikeRepository } from './repositories/page-like';
 import { ModerationLogRepository } from './repositories/moderation-logs';
+import { UsedUsername } from './entities/used-username';
 
 export const Apps = getCustomRepository(AppRepository);
 export const Notes = getCustomRepository(NoteRepository);
@@ -64,6 +65,7 @@ export const UserGroups = getCustomRepository(UserGroupRepository);
 export const UserGroupJoinings = getRepository(UserGroupJoining);
 export const UserGroupInvites = getCustomRepository(UserGroupInviteRepository);
 export const UserNotePinings = getRepository(UserNotePining);
+export const UsedUsernames = getRepository(UsedUsername);
 export const Followings = getCustomRepository(FollowingRepository);
 export const FollowRequests = getCustomRepository(FollowRequestRepository);
 export const Instances = getRepository(Instance);
diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts
index 724bb3a0c3..f393d6ed0d 100644
--- a/src/server/api/endpoints/username/available.ts
+++ b/src/server/api/endpoints/username/available.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import define from '../../define';
-import { Users } from '../../../../models';
+import { Users, UsedUsernames } from '../../../../models';
 
 export const meta = {
 	tags: ['users'],
@@ -21,7 +21,9 @@ export default define(meta, async (ps) => {
 		usernameLower: ps.username.toLowerCase()
 	});
 
+	const exist2 = await UsedUsernames.count({ username: ps.username.toLowerCase() });
+
 	return {
-		available: exist === 0
+		available: exist === 0 && exist2 === 0
 	};
 });
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 026fe7485b..c1f06fd339 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -5,7 +5,7 @@ import generateUserToken from '../common/generate-native-user-token';
 import config from '../../../config';
 import { fetchMeta } from '../../../misc/fetch-meta';
 import * as recaptcha from 'recaptcha-promise';
-import { Users, Signins, RegistrationTickets } from '../../../models';
+import { Users, Signins, RegistrationTickets, UsedUsernames } from '../../../models';
 import { genId } from '../../../misc/gen-id';
 import { usersChart } from '../../../services/chart';
 import { User } from '../../../models/entities/user';
@@ -13,6 +13,7 @@ import { UserKeypair } from '../../../models/entities/user-keypair';
 import { toPunyNullable } from '../../../misc/convert-host';
 import { UserProfile } from '../../../models/entities/user-profile';
 import { getConnection } from 'typeorm';
+import { UsedUsername } from '../../../models/entities/used-username';
 
 export default async (ctx: Koa.BaseContext) => {
 	const body = ctx.request.body as any;
@@ -78,11 +79,18 @@ export default async (ctx: Koa.BaseContext) => {
 	// Generate secret
 	const secret = generateUserToken();
 
+	// Check username duplication
 	if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
 		ctx.status = 400;
 		return;
 	}
 
+	// Check deleted username duplication
+	if (await UsedUsernames.findOne({ username: username.toLowerCase() })) {
+		ctx.status = 400;
+		return;
+	}
+
 	const keyPair = await new Promise<string[]>((s, j) =>
 		generateKeyPair('rsa', {
 			modulusLength: 4096,
@@ -133,6 +141,10 @@ export default async (ctx: Koa.BaseContext) => {
 			autoWatch: false,
 			password: hash,
 		}));
+
+		await transactionalEntityManager.save(new UsedUsername({
+			username: username.toLowerCase(),
+		}));
 	});
 
 	usersChart.update(account, true);