Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email password signup #33

Open
wants to merge 6 commits into
base: create-unlink-hive-account-endpoint
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/repositories/user/schemas/user.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ export class LegacyUser {
@Prop({ type: String, required: true })
user_id!: string;

@Prop({ type: String, unique: true })
@Prop({ type: String, unique: true, sparse: true })
sub?: string;

@Prop({ type: Boolean, required: true, default: false })
banned?: boolean;

@Prop({ type: String })
@Prop({ type: String, unique: true, sparse: true })
email?: string;

@Prop({ type: Types.ObjectId, ref: LegacyHiveAccount.name })
Expand Down
4 changes: 2 additions & 2 deletions src/repositories/userAccount/schemas/user-account.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ export type LegacyUserDocument = mongoose.Document & LegacyUserAccount;

@Schema()
export class LegacyUserAccount {
@Prop({ type: String, required: false })
@Prop({ type: String, default: () => uuid() })
confirmationCode?: string;

@Prop({ type: Date, required: true, default: new Date() })
createdAt?: Date;

@Prop({ type: String })
@Prop({ type: String, unique: true, sparse: true })
email?: string;

@Prop({ type: Boolean, required: true, default: false })
Expand Down
6 changes: 4 additions & 2 deletions src/repositories/userAccount/user-account.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ export class LegacyUserAccountRepository {
private legacyUserAccountModel: Model<LegacyUserAccount>,
) {}

async findOneByEmail(query: Pick<LegacyUserAccount, 'email'>): Promise<LegacyUserAccount | null> {
const authUser = await this.legacyUserAccountModel.findOne(query);
async findOneVerifiedByEmail(
query: Pick<LegacyUserAccount, 'email'>,
): Promise<LegacyUserAccount | null> {
const authUser = await this.legacyUserAccountModel.findOne({ ...query, emailVerified: true });
this.#logger.log(authUser); // TODO: delete - not suitable for prod

return authUser;
Expand Down
13 changes: 5 additions & 8 deletions src/services/api/api.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,20 +253,17 @@ export class ApiController {
@Post(`/hive/vote`)
async votePost(@Body() data: VotePostDto, @Request() req: any) {
const parsedRequest = parseAndValidateRequest(req, this.#logger);
const { author, permlink, weight, votingAccount } = data;

const user = await this.authService.getUserByUserId({ user_id: parsedRequest.user.user_id });

if (!user) throw new UnauthorizedException('User not found');
const votingAccount = await this.hiveService.parseHiveUsername(
parsedRequest,
data.votingAccount,
);
const { author, permlink, weight } = data;

return await this.hiveService.vote({
sub: parsedRequest.user.sub,
votingAccount,
author,
permlink,
weight,
network: parsedRequest.user.network,
user_id: user._id,
});
}
}
99 changes: 89 additions & 10 deletions src/services/auth/auth.controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dotenv/config'
import { UserAccountModule } from '../../repositories/userAccount/user-account.module';
import { SessionModule } from '../../repositories/session/session.module';
import { AuthController } from './auth.controller';
Expand All @@ -19,12 +18,15 @@ import { Ed25519Provider } from 'key-did-provider-ed25519';
import { INestApplication, Module, ValidationPipe } from '@nestjs/common';
import * as KeyResolver from 'key-did-resolver';
import { TestingModule } from '@nestjs/testing';
import crypto from 'crypto';
import crypto, { randomUUID } from 'crypto';
import { PrivateKey } from '@hiveio/dhive';
import { AuthGuard } from '@nestjs/passport';
import { MockAuthGuard, MockDidUserDetailsInterceptor, UserDetailsInterceptor } from '../api/utils';
import { HiveService } from '../hive/hive.service';
import { HiveModule } from '../hive/hive.module';
import { LegacyUserRepository } from '../../repositories/user/user.repository';
import { EmailService } from '../email/email.service';
import { LegacyUserAccountRepository } from '../../repositories/userAccount/user-account.repository';

describe('AuthController', () => {
let app: INestApplication
Expand All @@ -35,11 +37,13 @@ describe('AuthController', () => {
let mongod: MongoMemoryServer;
let authService: AuthService;
let hiveService: HiveService;

let userRepository: LegacyUserRepository;
let emailService: EmailService;
let userAccountRepository: LegacyUserAccountRepository;

beforeEach(async () => {
mongod = await MongoMemoryServer.create()
const uri: string = mongod.getUri()
mongod = await MongoMemoryServer.create();
const uri: string = mongod.getUri();

process.env.JWT_PRIVATE_KEY = crypto.randomBytes(64).toString('hex');
process.env.DELEGATED_ACCOUNT = 'threespeak';
Expand Down Expand Up @@ -88,21 +92,24 @@ describe('AuthController', () => {
})
class TestModule {}

let moduleRef: TestingModule;

moduleRef = await Test.createTestingModule({
let moduleRef = await Test.createTestingModule({
imports: [TestModule],
}).overrideGuard(AuthGuard('jwt'))
.useClass(MockAuthGuard)
.overrideInterceptor(UserDetailsInterceptor)
.useClass(MockDidUserDetailsInterceptor)
.compile();

authService = moduleRef.get<AuthService>(AuthService);
hiveService = moduleRef.get<HiveService>(HiveService);
userRepository = moduleRef.get<LegacyUserRepository>(LegacyUserRepository);
userAccountRepository = moduleRef.get<LegacyUserAccountRepository>(LegacyUserAccountRepository);
emailService = moduleRef.get<EmailService>(EmailService);

app = moduleRef.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init()
})
await app.init();
});

afterEach(async () => {
await app.close();
Expand Down Expand Up @@ -183,7 +190,78 @@ describe('AuthController', () => {
expect(await hiveService.isHiveAccountLinked({ account: 'bob', user_id: user._id })).toBe(true)
expect(await hiveService.isHiveAccountLinked({ account: username, user_id: user._id })).toBe(false)
});
});
});

describe('/POST register', () => {
it('registers a new user successfully with valid email', async () => {
const email = '[email protected]';
const password = '!SUPER-SECRET_password!7';

const response = await request(app.getHttpServer())
.post('/v1/auth/register')
.send({ email, password })
.expect(201);

expect(response.body).toEqual({ access_token: expect.any(String) });

const user = await userRepository.findOneByEmail(email);
expect(user).toBeDefined();
});

it('throws error when email is already registered', async () => {
const email = '[email protected]';
const password = '!SUPER-SECRET_password!7';

// Create a user with the same email
await authService.registerEmailAndPasswordUser(email, password);

return request(app.getHttpServer())
.post('/v1/auth/register')
.send({ email, password })
.expect(400)
.then(response => {
expect(response.body).toEqual({ reason: 'Email Password account already created!' });
});
});

it('throws error when email is invalid', async () => {
const email = 'tesnvalid.example.org';
const password = '!SUPER-SECRET_password!7';

return request(app.getHttpServer())
.post('/v1/auth/register')
.send({ email, password })
.expect(400)
.then(response => {
expect(response.body).toEqual({
error: "Bad Request",
message: [
"Email must be a valid email address",
"email must be an email",
],
statusCode: 400
});
});
});

it('throws error when password is not strong enough', async () => {
const email = '[email protected]';
const password = '!SUPER-SECRET_password!';

return request(app.getHttpServer())
.post('/v1/auth/register')
.send({ email, password })
.expect(400)
.then(response => {
expect(response.body).toEqual({
error: "Bad Request",
message: [
"password is not strong enough",
],
statusCode: 400
});
});
});


Expand Down Expand Up @@ -308,6 +386,7 @@ describe('AuthController', () => {
statusCode: 401,
})
})
})
})
})
});
56 changes: 24 additions & 32 deletions src/services/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { RequestHiveAccountDto } from '../api/dto/RequestHiveAccount.dto';
import { HiveService } from '../hive/hive.service';
import { AuthInterceptor, UserDetailsInterceptor } from '../api/utils';
import { randomUUID } from 'crypto';
import { v4 as uuid } from 'uuid';
import { EmailRegisterDto } from './dto/EmailRegister.dto';

@Controller('/v1/auth')
export class AuthController {
Expand Down Expand Up @@ -308,52 +308,42 @@ export class AuthController {
summary: 'Registers an account using email/password login',
})
@ApiBody({
type: EmailRegisterDto,
})
@ApiOkResponse({
schema: {
properties: {
password: {
type: 'string',
default: '!SUPER-SECRET_PASSWORD!',
},
email: {
type: 'string',
default: '[email protected]',
ok: {
type: 'boolean',
default: true,
},
},
},
})
@ApiOkResponse({
@ApiBadRequestResponse({
description: 'Invalid options or email already registered',
schema: {
type: 'object',
properties: {
ok: {
type: 'boolean',
default: true,
reason: {
type: 'string',
default: 'Invalid email or password',
},
errorType: {
type: 'string',
default: 'VALIDATION_ERROR',
},
},
},
})
// @UseGuards(AuthGuard('local'))
@ApiInternalServerErrorResponse({
description: 'Internal Server Error - unrelated to request body',
})
@Post('/register')
async register(@Request() req, @Body() body: { password: string; email: string }) {
async register(@Body() body: EmailRegisterDto) {
const { email, password } = body;

const existingRecord = await this.userRepository.findOneByEmail(email);

if (existingRecord) {
throw new HttpException(
{ reason: 'Email Password account already created!' },
HttpStatus.BAD_REQUEST,
);
}

const user_id = uuid();

const email_code = await this.authService.createEmailAndPasswordUser(email, password, user_id);

await this.emailService.sendRegistration(email, email_code);
return {
ok: true,
};
// return this.authService.login(req.user);
return await this.authService.registerEmailAndPasswordUser(email, password);
}

@ApiParam({
Expand All @@ -376,6 +366,8 @@ export class AuthController {
return res.redirect('https://3speak.tv');
}

// todo: send new verification code

@ApiHeader({
name: 'Authorization',
description: 'JWT Authorization',
Expand Down
Loading