Skip to content

Commit

Permalink
[GH-38] feat(User): add register login and validate email WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
pablojvritx committed Dec 12, 2023
1 parent 6d8ca7f commit b7dc5a0
Show file tree
Hide file tree
Showing 60 changed files with 1,864 additions and 19 deletions.
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ MONGO_URL=localhost:27017
MONGO_DB=test
MONGO_USERNAME=localUser
MONGO_PASSWORD=localPassword
JWT_SECRET=secret
244 changes: 236 additions & 8 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"url": "https://github.com/vinjatovix/ts-api/issues"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"bson": "^6.1.0",
"cors": "^2.8.5",
Expand All @@ -74,18 +75,21 @@
"glob": "^10.3.10",
"helmet": "^7.0.0",
"http-status": "^1.7.0",
"jsonwebtoken": "^9.0.2",
"mongodb": "^4.0.0",
"node-dependency-injection": "^2.7.3",
"swagger-ui-express": "^5.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"uuid": "^9.0.1",
"uuid-validate": "^0.0.3",
"winston": "^3.11.0",
"winston-mongodb": "^5.1.1",
"yamljs": "^0.3.0"
},
"devDependencies": {
"@cucumber/cucumber": "^10.0.0",
"@types/bcryptjs": "^2.4.6",
"@types/chai": "^4.3.8",
"@types/chance": "^1.1.4",
"@types/convict": "^6.1.4",
Expand All @@ -94,6 +98,7 @@
"@types/express": "^4.17.19",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.5",
"@types/jsonwebtoken": "^9.0.5",
"@types/supertest": "^2.0.14",
"@types/swagger-ui-express": "^4.1.5",
"@types/uuid-validate": "^0.0.1",
Expand Down
45 changes: 45 additions & 0 deletions src/Contexts/apiApp/Auth/application/LoginUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { EncrypterTool } from '../../../shared/plugins/EncrypterTool';
import { Nullable } from '../../../shared/domain/Nullable';
import { AuthError } from '../../../shared/domain/errors/AuthError';
import { buildLogger } from '../../../shared/plugins/logger.plugin';

import { UserRepository } from '../domain';

import { LoginUserRequest } from './LoginUserRequest';

const logger = buildLogger('loginUser');

export class LoginUser {
private readonly repository: UserRepository;
private readonly encrypter: EncrypterTool;

constructor(repository: UserRepository, encrypter: EncrypterTool) {
this.repository = repository;
this.encrypter = encrypter;
}

async run({ email, password }: LoginUserRequest): Promise<Nullable<string>> {
const storedUser = await this.repository.search(email);
if (!storedUser) {
throw new AuthError('Invalid credentials');
}

const success = await this.encrypter.compare(
password,
storedUser.password.value
);
if (!success) {
throw new AuthError('Invalid credentials');
}

const token = await this.encrypter.generateToken({
id: storedUser.id.value,
email: storedUser.email.value,
username: storedUser.username.value,
roles: storedUser.roles.value
});

logger.info(`User <${storedUser.username.value}> logged in`);
return token;
}
}
4 changes: 4 additions & 0 deletions src/Contexts/apiApp/Auth/application/LoginUserRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface LoginUserRequest {
email: string;
password: string;
}
42 changes: 42 additions & 0 deletions src/Contexts/apiApp/Auth/application/RegisterUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { EncrypterTool } from '../../../shared/plugins/EncrypterTool';
import { InvalidArgumentError } from '../../../shared/domain/errors/InvalidArgumentError';
import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject';
import { Uuid } from '../../../shared/domain/value-object/Uuid';
import { buildLogger } from '../../../shared/plugins/logger.plugin';

import { User, Email, UserRepository, UserRoles, Username } from '../domain';

import { RegisterUserRequest } from './RegisterUserRequest';

const logger = buildLogger('registerUser');

export class RegisterUser {
private readonly repository: UserRepository;
private readonly encrypter: EncrypterTool;

constructor(repository: UserRepository, encrypter: EncrypterTool) {
this.repository = repository;
this.encrypter = encrypter;
}

async run({ password, username, email }: RegisterUserRequest): Promise<void> {
const storedUser = await this.repository.search(email);
if (storedUser) {
throw new InvalidArgumentError(`User <${email}> already exists`);
}

const encryptedPassword = this.encrypter.hash(password);

const user = new User({
id: Uuid.random(),
email: new Email(email),
username: new Username(username),
password: new StringValueObject(encryptedPassword),
emailValidated: false,
roles: new UserRoles(['user'])
});

await this.repository.save(user);
logger.info(`User <${user.username.value}> registered`);
}
}
5 changes: 5 additions & 0 deletions src/Contexts/apiApp/Auth/application/RegisterUserRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LoginUserRequest } from './LoginUserRequest';

export interface RegisterUserRequest extends LoginUserRequest {
username: string;
}
40 changes: 40 additions & 0 deletions src/Contexts/apiApp/Auth/application/ValidateMail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { EncrypterTool } from '../../../shared/plugins/EncrypterTool';
import { UserRepository } from '../domain';
import { UserPatch } from '../domain/UserPatch';
import { buildLogger } from '../../../shared/plugins/logger.plugin';
import { Nullable } from '../../../shared/domain/Nullable';
import { AuthError } from '../../../shared/domain/errors/AuthError';

const logger = buildLogger('validateMail');

export class ValidateMail {
private readonly repository: UserRepository;
private readonly encrypter: EncrypterTool;

constructor(repository: UserRepository, encrypter: EncrypterTool) {
this.repository = repository;
this.encrypter = encrypter;
}

async run({ token }: { token: string }): Promise<Nullable<string>> {
const validToken = await this.encrypter.verifyToken(token);
if (!validToken) {
throw new AuthError('Invalid token');
}
const { email } = validToken as unknown as { email: string };

const storedUser = await this.repository.search(email);
if (!storedUser) {
throw new AuthError('Invalid token');
}
const userToPatch = UserPatch.fromPrimitives({
id: storedUser.id.value,
emailValidated: true
});

await this.repository.update(userToPatch);
logger.info(`User <${storedUser.username.value}> validated email`);

return this.encrypter.refreshToken(token);
}
}
5 changes: 5 additions & 0 deletions src/Contexts/apiApp/Auth/application/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './LoginUser';
export * from './RegisterUser';
export * from './ValidateMail';

export * from './LoginUserRequest';
75 changes: 75 additions & 0 deletions src/Contexts/apiApp/Auth/domain/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { AggregateRoot } from '../../../shared/domain/AggregateRoot';
import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject';
import { Uuid } from '../../../shared/domain/value-object/Uuid';
import { Email } from '../../../shared/domain/value-object/Email';
import { UserRoles } from './UserRoles';
import { Username } from './Username';

export class User extends AggregateRoot {
readonly id: Uuid;
readonly email: Email;
readonly username: Username;
readonly password: StringValueObject;
readonly emailValidated: boolean;
readonly roles: UserRoles;

constructor({
id,
email,
username,
password,
emailValidated,
roles
}: {
id: Uuid;
email: Email;
username: Username;
password: StringValueObject;
emailValidated: boolean;
roles: UserRoles;
}) {
super();
this.id = id;
this.email = email;
this.username = username;
this.password = password;
this.emailValidated = emailValidated;
this.roles = roles;
}

toPrimitives(): Record<string, unknown> {
return {
id: this.id.value,
email: this.email.value,
username: this.username.value,
password: this.password.value,
emailValidated: this.emailValidated,
roles: this.roles.value
};
}

static fromPrimitives({
id,
email,
username,
password,
emailValidated,
roles
}: {
id: string;
email: string;
username: string;
password: string;
emailValidated: boolean;
roles: string[];
}): User {
return new User({
id: new Uuid(id),
email: new Email(email),
username: new Username(username),
password: new StringValueObject(password),
emailValidated,
roles: new UserRoles(roles)
});
}
}
57 changes: 57 additions & 0 deletions src/Contexts/apiApp/Auth/domain/UserPatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { AggregateRoot } from '../../../shared/domain/AggregateRoot';
import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject';
import { Uuid } from '../../../shared/domain/value-object/Uuid';
import { UserRoles } from './UserRoles';

export class UserPatch extends AggregateRoot {
readonly id: Uuid;
readonly password?: StringValueObject;
readonly emailValidated?: boolean;
readonly roles?: UserRoles;

constructor({
id,
password,
emailValidated,
roles
}: {
id: Uuid;
password?: StringValueObject;
emailValidated?: boolean;
roles?: UserRoles;
}) {
super();
this.id = id;
password && (this.password = password);
emailValidated && (this.emailValidated = emailValidated);
roles && (this.roles = roles);
}

toPrimitives() {
return {
id: this.id.value,
...(this.password?.value && { password: this.password.value }),
...(this.emailValidated && { emailValidated: this.emailValidated }),
...(this.roles?.value && { roles: this.roles.value })
};
}

static fromPrimitives({
id,
password,
emailValidated,
roles
}: {
id: string;
password?: string;
emailValidated?: boolean;
roles?: string[];
}) {
return new UserPatch({
id: new Uuid(id),
...(password && { password: new StringValueObject(password) }),
...(emailValidated && { emailValidated }),
...(roles && { roles: new UserRoles(roles) })
});
}
}
11 changes: 11 additions & 0 deletions src/Contexts/apiApp/Auth/domain/UserRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Nullable } from '../../../shared/domain/Nullable';
import { User } from './User';
import { UserPatch } from './UserPatch';

export interface UserRepository {
save(user: User): Promise<void>;

update(user: UserPatch): Promise<void>;

search(email: string): Promise<Nullable<User>>;
}
21 changes: 21 additions & 0 deletions src/Contexts/apiApp/Auth/domain/UserRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { InvalidArgumentError } from '../../../shared/domain/errors/InvalidArgumentError';

export class UserRoles {
private static validRoles = ['admin', 'user'];
readonly value: string[];

constructor(value: string[]) {
this.ensureRoles(value);
this.value = value;
}

private ensureRoles(value: string[]): void {
value.forEach((role) => {
if (!UserRoles.validRoles.includes(role)) {
throw new InvalidArgumentError(
`<${this.constructor.name}> does not allow the value <${role}>`
);
}
});
}
}
25 changes: 25 additions & 0 deletions src/Contexts/apiApp/Auth/domain/Username.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { InvalidArgumentError } from '../../../shared/domain/errors/InvalidArgumentError';
import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject';

export class Username extends StringValueObject {
constructor(value: string) {
super(value);
this.ensureLength(value);
}

private ensureLength(value: string): string {
if (value.length < 4) {
throw new InvalidArgumentError(
'<Username> must be at least 4 characters long'
);
}

if (value.length > 20) {
throw new InvalidArgumentError(
'<Username> must be less than 20 characters long'
);
}

return value;
}
}
7 changes: 7 additions & 0 deletions src/Contexts/apiApp/Auth/domain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './User';
export * from '../../../shared/domain/value-object/Email';
export * from './UserRoles';
export * from './Username';
export * from './UserPatch';

export * from './UserRepository';
Loading

0 comments on commit b7dc5a0

Please sign in to comment.