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 553c5bf commit 8b92b95
Show file tree
Hide file tree
Showing 55 changed files with 1,711 additions and 18 deletions.
244 changes: 236 additions & 8 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"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",
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 8b92b95

Please sign in to comment.