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
|
||||
|
||||
- [ ] 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?
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {Credentials} from '../auth'
|
||||
|
||||
export interface NewUser extends Credentials {
|
||||
firstName: string
|
||||
lastName: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export interface UserProfile {
|
||||
id: number
|
||||
username: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 './1552899385211-user-first-last-name'
|
||||
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) => {
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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/)
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user