Add ability to register account without email

TODO: add captcha
This commit is contained in:
Jerko Steiner 2019-11-01 14:43:17 -04:00
parent 2e06a40006
commit 8449416366
15 changed files with 79 additions and 26 deletions

View File

@ -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?

View File

@ -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)
}

View File

@ -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[]

View File

@ -1,6 +1,7 @@
import {Credentials} from '../auth'
export interface NewUser extends Credentials {
firstName: string
lastName: string
firstName?: string
lastName?: string
email?: string
}

View File

@ -1,6 +1,6 @@
export interface UserProfile {
id: number
username: string
firstName: string
lastName: string
firstName: string | null
lastName: string | null
}

View File

@ -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',

View File

@ -6,11 +6,17 @@ export const UserEntity = new EntitySchema<User>({
name: 'user',
columns: {
...BaseEntitySchemaPart,
username: {
type: String,
unique: true,
},
firstName: {
type: String,
nullable: true,
},
lastName: {
type: String,
nullable: true,
},
password: {
type: String,

View File

@ -2,7 +2,7 @@
/// <reference path="../@types/react-ssr-prepass.d.ts" />
if (require.main === module) {
if (!process.env.LOG) {
process.env.LOG = 'api,sql:warn'
process.env.LOG = 'api,sql:warn,config'
}
}
export * from './application'

View File

@ -0,0 +1,28 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class userUsername1572631394827 implements MigrationInterface {
name = 'userUsername1572631394827'
public async up(queryRunner: QueryRunner): Promise<any> {
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<any> {
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);
}
}

View File

@ -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'

View File

@ -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,

View File

@ -41,7 +41,7 @@ export class SQLUserService implements RPC<UserService> {
return {
id: userEmail.userId!,
username: userEmail.email,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
}

View File

@ -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/)
})

View File

@ -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<UserProfile> {
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,
}

View File

@ -122,6 +122,7 @@ export class TestUtils<T extends Routes> {
.send({
firstName: 'test',
lastName: 'test',
email: username || this.username,
...this.getLoginBody(token, username),
})
.expect(200)