Add ability to register account without email
TODO: add captcha
This commit is contained in:
parent
2e06a40006
commit
8449416366
2
TODO.md
2
TODO.md
@ -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?
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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[]
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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'
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
await this.db.getRepository(UserEmailEntity).save({
|
if (email) {
|
||||||
email: newUser.username,
|
await this.db.getRepository(UserEmailEntity).save({
|
||||||
userId: user.id,
|
email,
|
||||||
})
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user