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 - [ ] Add/Remove profile picture
- [ ] Privacy - [ ] Privacy
- [ ] Do not require email for account creation - [x] Do not require email for account creation
- [ ] Preventing fake accounts/spam using either: - [ ] Preventing fake accounts/spam using either:
- [ ] Moderation techniques described below - [ ] Moderation techniques described below
- [ ] Require proof of work during acct creation? - [ ] Require proof of work during acct creation?

View File

@ -20,3 +20,10 @@ export function trim(str?: string) {
} }
return str.trim() 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 { export interface User {
firstName: string username: string
lastName: string firstName: string | null
lastName: string | null
emails: UserEmail[] emails: UserEmail[]
password?: string password?: string
sessions: Session[] sessions: Session[]

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import { arg, argparse } from '@rondo.dev/argparse' import { arg, argparse } from '@rondo.dev/argparse'
import { cpus } from 'os' import { cpus } from 'os'
import { Bootstrap } from '../application' import { Bootstrap } from '../application'
import { loggerFactory } from '../logger'
import { LogLevel } from '@rondo.dev/logger'
const numberOfCPUs = cpus().length const numberOfCPUs = cpus().length
@ -62,6 +64,7 @@ const commands = {
.startCluster(args.workers, args.port || args.socket, args.host) .startCluster(args.workers, args.port || args.socket, args.host)
}, },
async migrate(bootstrap: Bootstrap, argv: string[]) { async migrate(bootstrap: Bootstrap, argv: string[]) {
loggerFactory.setLoggerLevel('sql', LogLevel.INFO)
const {parse} = argparse({ const {parse} = argparse({
undo: arg('boolean', { undo: arg('boolean', {
alias: 'u', alias: 'u',

View File

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

View File

@ -2,7 +2,7 @@
/// <reference path="../@types/react-ssr-prepass.d.ts" /> /// <reference path="../@types/react-ssr-prepass.d.ts" />
if (require.main === module) { if (require.main === module) {
if (!process.env.LOG) { if (!process.env.LOG) {
process.env.LOG = 'api,sql:warn' process.env.LOG = 'api,sql:warn,config'
} }
} }
export * from './application' 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 './1552227652042-nullable'
export * from './1552899385211-user-first-last-name' export * from './1552899385211-user-first-last-name'
export * from './1569211662484-session_expiredAt_bigint' 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) => { t.post('/auth/register', async (req, res) => {
const user = await authService.createUser({ const user = await authService.createUser({
username: req.body.username, username: req.body.username,
email: req.body.email,
password: req.body.password, password: req.body.password,
firstName: req.body.firstName, firstName: req.body.firstName,
lastName: req.body.lastName, lastName: req.body.lastName,

View File

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

View File

@ -14,6 +14,7 @@ describe('SQLAuthService', () => {
return authService.createUser({ return authService.createUser({
username: u, username: u,
password: p, password: p,
email: u,
firstName: 'test', firstName: 'test',
lastName: 'test', lastName: 'test',
}) })
@ -28,8 +29,12 @@ describe('SQLAuthService', () => {
expect(user).not.toHaveProperty('password') expect(user).not.toHaveProperty('password')
}) })
it('throws when username is not an email', async () => { it('throws when email is present is not a valid email', async () => {
const err = await test.getError(createUser('test', password)) const err = await test.getError(authService.createUser({
username,
password,
email: username.replace('@', '_'),
}))
expect(err.message).toMatch(/not a valid e-mail/) 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 { TypeORMDatabase } from '@rondo.dev/db-typeorm'
import Validator from '@rondo.dev/validator' import Validator from '@rondo.dev/validator'
import { compare, hash } from 'bcrypt' import { compare, hash } from 'bcrypt'
@ -15,11 +15,13 @@ export class SQLAuthService implements AuthService {
async createUser(payload: NewUser): Promise<UserProfile> { async createUser(payload: NewUser): Promise<UserProfile> {
const newUser = { const newUser = {
username: trim(payload.username), username: trim(payload.username),
firstName: trim(payload.firstName), firstName: nullable(payload.firstName),
lastName: trim(payload.lastName), 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') throw createError(400, 'Username is not a valid e-mail')
} }
if (payload.password.length < MIN_PASSWORD_LENGTH) { if (payload.password.length < MIN_PASSWORD_LENGTH) {
@ -29,8 +31,6 @@ export class SQLAuthService implements AuthService {
new Validator(newUser) new Validator(newUser)
.ensure('username') .ensure('username')
.ensure('firstName')
.ensure('lastName')
.throw() .throw()
const password = await this.hash(payload.password) const password = await this.hash(payload.password)
@ -38,10 +38,12 @@ export class SQLAuthService implements AuthService {
...newUser, ...newUser,
password, password,
}) })
if (email) {
await this.db.getRepository(UserEmailEntity).save({ await this.db.getRepository(UserEmailEntity).save({
email: newUser.username, email,
userId: user.id, userId: user.id,
}) })
}
return { return {
id: user.id, id: user.id,
...newUser, ...newUser,
@ -113,17 +115,14 @@ export class SQLAuthService implements AuthService {
.createQueryBuilder('user') .createQueryBuilder('user')
.select('user') .select('user')
.addSelect('user.password') .addSelect('user.password')
.addSelect('emails') .where('user.username = :username', { username })
.innerJoin('user.emails', 'emails', 'emails.email = :email', {
email: username,
})
.getOne() .getOne()
const isValid = await compare(password, user ? user.password! : '') const isValid = await compare(password, user ? user.password! : '')
if (user && isValid) { if (user && isValid) {
return { return {
id: user.id, id: user.id,
username: user.emails[0].email, username: user.username,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
} }

View File

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