From 8449416366878a2b5082f423df1b93131f235440 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Fri, 1 Nov 2019 14:43:17 -0400 Subject: [PATCH] Add ability to register account without email TODO: add captcha --- TODO.md | 2 +- packages/common/src/StringUtils.ts | 7 +++++ packages/common/src/entities.ts | 5 ++-- packages/common/src/user/NewUser.ts | 5 ++-- packages/common/src/user/UserProfile.ts | 4 +-- packages/server/src/cli/run.ts | 3 ++ packages/server/src/entities/UserEntity.ts | 6 ++++ packages/server/src/index.ts | 2 +- .../migrations/1572631394827-user-username.ts | 28 ++++++++++++++++++ packages/server/src/migrations/index.ts | 1 + .../server/src/routes/configureAuthRoutes.ts | 1 + packages/server/src/rpc/SQLUserService.ts | 2 +- .../src/services/SQLAuthService.test.ts | 9 ++++-- .../server/src/services/SQLAuthService.ts | 29 +++++++++---------- packages/server/src/test-utils/TestUtils.ts | 1 + 15 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 packages/server/src/migrations/1572631394827-user-username.ts diff --git a/TODO.md b/TODO.md index 7e9d4dd..534e3cf 100644 --- a/TODO.md +++ b/TODO.md @@ -43,7 +43,7 @@ - [ ] Add/Remove profile picture - [ ] Privacy - - [ ] Do not require email for account creation + - [x] Do not require email for account creation - [ ] Preventing fake accounts/spam using either: - [ ] Moderation techniques described below - [ ] Require proof of work during acct creation? diff --git a/packages/common/src/StringUtils.ts b/packages/common/src/StringUtils.ts index 8bfdaff..dc69cc3 100644 --- a/packages/common/src/StringUtils.ts +++ b/packages/common/src/StringUtils.ts @@ -20,3 +20,10 @@ export function trim(str?: string) { } return str.trim() } + +export function nullable(str: string | undefined) { + if (!str) { + return null + } + return trim(str) +} diff --git a/packages/common/src/entities.ts b/packages/common/src/entities.ts index e8e3562..3c2c9aa 100644 --- a/packages/common/src/entities.ts +++ b/packages/common/src/entities.ts @@ -25,8 +25,9 @@ export interface Team { } export interface User { - firstName: string - lastName: string + username: string + firstName: string | null + lastName: string | null emails: UserEmail[] password?: string sessions: Session[] diff --git a/packages/common/src/user/NewUser.ts b/packages/common/src/user/NewUser.ts index 9f967fc..535c9fc 100644 --- a/packages/common/src/user/NewUser.ts +++ b/packages/common/src/user/NewUser.ts @@ -1,6 +1,7 @@ import {Credentials} from '../auth' export interface NewUser extends Credentials { - firstName: string - lastName: string + firstName?: string + lastName?: string + email?: string } diff --git a/packages/common/src/user/UserProfile.ts b/packages/common/src/user/UserProfile.ts index 145dec1..d1a16d1 100644 --- a/packages/common/src/user/UserProfile.ts +++ b/packages/common/src/user/UserProfile.ts @@ -1,6 +1,6 @@ export interface UserProfile { id: number username: string - firstName: string - lastName: string + firstName: string | null + lastName: string | null } diff --git a/packages/server/src/cli/run.ts b/packages/server/src/cli/run.ts index 3b94402..54444e2 100644 --- a/packages/server/src/cli/run.ts +++ b/packages/server/src/cli/run.ts @@ -1,6 +1,8 @@ import { arg, argparse } from '@rondo.dev/argparse' import { cpus } from 'os' import { Bootstrap } from '../application' +import { loggerFactory } from '../logger' +import { LogLevel } from '@rondo.dev/logger' const numberOfCPUs = cpus().length @@ -62,6 +64,7 @@ const commands = { .startCluster(args.workers, args.port || args.socket, args.host) }, async migrate(bootstrap: Bootstrap, argv: string[]) { + loggerFactory.setLoggerLevel('sql', LogLevel.INFO) const {parse} = argparse({ undo: arg('boolean', { alias: 'u', diff --git a/packages/server/src/entities/UserEntity.ts b/packages/server/src/entities/UserEntity.ts index a47e03a..3ad67a5 100644 --- a/packages/server/src/entities/UserEntity.ts +++ b/packages/server/src/entities/UserEntity.ts @@ -6,11 +6,17 @@ export const UserEntity = new EntitySchema({ name: 'user', columns: { ...BaseEntitySchemaPart, + username: { + type: String, + unique: true, + }, firstName: { type: String, + nullable: true, }, lastName: { type: String, + nullable: true, }, password: { type: String, diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 3949aeb..1378fc8 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -2,7 +2,7 @@ /// if (require.main === module) { if (!process.env.LOG) { - process.env.LOG = 'api,sql:warn' + process.env.LOG = 'api,sql:warn,config' } } export * from './application' diff --git a/packages/server/src/migrations/1572631394827-user-username.ts b/packages/server/src/migrations/1572631394827-user-username.ts new file mode 100644 index 0000000..c44d136 --- /dev/null +++ b/packages/server/src/migrations/1572631394827-user-username.ts @@ -0,0 +1,28 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class userUsername1572631394827 implements MigrationInterface { + name = 'userUsername1572631394827' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "password" varchar(60), "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "firstName" varchar NOT NULL, "lastName" varchar NOT NULL, "username" varchar NOT NULL, CONSTRAINT "UQ_3021ae0235cf9c4a6d59663f859" UNIQUE ("username"))`, undefined); + await queryRunner.query(`INSERT INTO "temporary_user"("id", "password", "createDate", "updateDate", "firstName", "lastName") SELECT "id", "password", "createDate", "updateDate", "firstName", "lastName" FROM "user"`, undefined); + await queryRunner.query(`DROP TABLE "user"`, undefined); + await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`, undefined); + await queryRunner.query(`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "password" varchar(60), "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "firstName" varchar, "lastName" varchar, "username" varchar NOT NULL, CONSTRAINT "UQ_3021ae0235cf9c4a6d59663f859" UNIQUE ("username"))`, undefined); + await queryRunner.query(`INSERT INTO "temporary_user"("id", "password", "createDate", "updateDate", "firstName", "lastName", "username") SELECT "id", "password", "createDate", "updateDate", "firstName", "lastName", "username" FROM "user"`, undefined); + await queryRunner.query(`DROP TABLE "user"`, undefined); + await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`, undefined); + await queryRunner.query(`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "password" varchar(60), "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "firstName" varchar NOT NULL, "lastName" varchar NOT NULL, "username" varchar NOT NULL, CONSTRAINT "UQ_3021ae0235cf9c4a6d59663f859" UNIQUE ("username"))`, undefined); + await queryRunner.query(`INSERT INTO "user"("id", "password", "createDate", "updateDate", "firstName", "lastName", "username") SELECT "id", "password", "createDate", "updateDate", "firstName", "lastName", "username" FROM "temporary_user"`, undefined); + await queryRunner.query(`DROP TABLE "temporary_user"`, undefined); + await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`, undefined); + await queryRunner.query(`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "password" varchar(60), "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "firstName" varchar NOT NULL, "lastName" varchar NOT NULL)`, undefined); + await queryRunner.query(`INSERT INTO "user"("id", "password", "createDate", "updateDate", "firstName", "lastName") SELECT "id", "password", "createDate", "updateDate", "firstName", "lastName" FROM "temporary_user"`, undefined); + await queryRunner.query(`DROP TABLE "temporary_user"`, undefined); + } + +} diff --git a/packages/server/src/migrations/index.ts b/packages/server/src/migrations/index.ts index f9e72c8..23d908a 100644 --- a/packages/server/src/migrations/index.ts +++ b/packages/server/src/migrations/index.ts @@ -10,3 +10,4 @@ export * from './1552227347990-comment-parentid-nullable' export * from './1552227652042-nullable' export * from './1552899385211-user-first-last-name' export * from './1569211662484-session_expiredAt_bigint' +export * from './1572631394827-user-username' diff --git a/packages/server/src/routes/configureAuthRoutes.ts b/packages/server/src/routes/configureAuthRoutes.ts index da07c46..26f168a 100644 --- a/packages/server/src/routes/configureAuthRoutes.ts +++ b/packages/server/src/routes/configureAuthRoutes.ts @@ -10,6 +10,7 @@ export function configureAuthRoutes( t.post('/auth/register', async (req, res) => { const user = await authService.createUser({ username: req.body.username, + email: req.body.email, password: req.body.password, firstName: req.body.firstName, lastName: req.body.lastName, diff --git a/packages/server/src/rpc/SQLUserService.ts b/packages/server/src/rpc/SQLUserService.ts index 795f650..8c06cee 100644 --- a/packages/server/src/rpc/SQLUserService.ts +++ b/packages/server/src/rpc/SQLUserService.ts @@ -41,7 +41,7 @@ export class SQLUserService implements RPC { return { id: userEmail.userId!, - username: userEmail.email, + username: user.username, firstName: user.firstName, lastName: user.lastName, } diff --git a/packages/server/src/services/SQLAuthService.test.ts b/packages/server/src/services/SQLAuthService.test.ts index 88cd2b6..d50dc85 100644 --- a/packages/server/src/services/SQLAuthService.test.ts +++ b/packages/server/src/services/SQLAuthService.test.ts @@ -14,6 +14,7 @@ describe('SQLAuthService', () => { return authService.createUser({ username: u, password: p, + email: u, firstName: 'test', lastName: 'test', }) @@ -28,8 +29,12 @@ describe('SQLAuthService', () => { expect(user).not.toHaveProperty('password') }) - it('throws when username is not an email', async () => { - const err = await test.getError(createUser('test', password)) + it('throws when email is present is not a valid email', async () => { + const err = await test.getError(authService.createUser({ + username, + password, + email: username.replace('@', '_'), + })) expect(err.message).toMatch(/not a valid e-mail/) }) diff --git a/packages/server/src/services/SQLAuthService.ts b/packages/server/src/services/SQLAuthService.ts index c6d7d71..2007c69 100644 --- a/packages/server/src/services/SQLAuthService.ts +++ b/packages/server/src/services/SQLAuthService.ts @@ -1,4 +1,4 @@ -import { AuthService, Credentials, NewUser, trim, UserProfile } from '@rondo.dev/common' +import { AuthService, Credentials, NewUser, trim, UserProfile, nullable } from '@rondo.dev/common' import { TypeORMDatabase } from '@rondo.dev/db-typeorm' import Validator from '@rondo.dev/validator' import { compare, hash } from 'bcrypt' @@ -15,11 +15,13 @@ export class SQLAuthService implements AuthService { async createUser(payload: NewUser): Promise { const newUser = { username: trim(payload.username), - firstName: trim(payload.firstName), - lastName: trim(payload.lastName), + firstName: nullable(payload.firstName), + lastName: nullable(payload.lastName), } - if (!validateEmail(newUser.username)) { + const email = nullable(payload.email) + + if (email && !validateEmail(email)) { throw createError(400, 'Username is not a valid e-mail') } if (payload.password.length < MIN_PASSWORD_LENGTH) { @@ -29,8 +31,6 @@ export class SQLAuthService implements AuthService { new Validator(newUser) .ensure('username') - .ensure('firstName') - .ensure('lastName') .throw() const password = await this.hash(payload.password) @@ -38,10 +38,12 @@ export class SQLAuthService implements AuthService { ...newUser, password, }) - await this.db.getRepository(UserEmailEntity).save({ - email: newUser.username, - userId: user.id, - }) + if (email) { + await this.db.getRepository(UserEmailEntity).save({ + email, + userId: user.id, + }) + } return { id: user.id, ...newUser, @@ -113,17 +115,14 @@ export class SQLAuthService implements AuthService { .createQueryBuilder('user') .select('user') .addSelect('user.password') - .addSelect('emails') - .innerJoin('user.emails', 'emails', 'emails.email = :email', { - email: username, - }) + .where('user.username = :username', { username }) .getOne() const isValid = await compare(password, user ? user.password! : '') if (user && isValid) { return { id: user.id, - username: user.emails[0].email, + username: user.username, firstName: user.firstName, lastName: user.lastName, } diff --git a/packages/server/src/test-utils/TestUtils.ts b/packages/server/src/test-utils/TestUtils.ts index 2f79921..8f3cdb1 100644 --- a/packages/server/src/test-utils/TestUtils.ts +++ b/packages/server/src/test-utils/TestUtils.ts @@ -122,6 +122,7 @@ export class TestUtils { .send({ firstName: 'test', lastName: 'test', + email: username || this.username, ...this.getLoginBody(token, username), }) .expect(200)