Skip to content
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
2 changes: 2 additions & 0 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ jobs:
env_file: ".env.heroku"
env:
HD_MONGO_CONNECTION_STRING: ${{secrets.HEROKU_MONGO_CONNECTION_STRING}}
HD_JWT_SECRET_KEY: ${{secrets.JWT_SECRET_KEY}}
HD_GOOGLE_CLIENT_ID: ${{secrets.GOOGLE_CLIENT_ID}}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Our backend application follows the **TDD approach** and is almost **fully cover
- React
- React-DOM
- Router
- Hooks (useState, useEffect, useContext,useHistory)
- Hooks (useState, useEffect, useHistory)
- Material-UI
- Axios

Expand Down
1 change: 1 addition & 0 deletions backend/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
REST_API_PORT=5000
API_DOCS_ENDPOINT_URL=/rest-api-docs
GOOGLE_CLIENT_ID=1052788207529-fjnskiqrsm09i9kbujp3gmtasnp21su7.apps.googleusercontent.com
JWT_SECRET_KEY=67A83E4F684CD7DF4402F42CEC6B01AF6AC0BB0DB23ED29361B6A031794BEC02

MONGO_REPOSITORIES=DISABLED
#MONGO_REPOSITORIES=ENABLED
Expand Down
12,241 changes: 107 additions & 12,134 deletions backend/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"dependencies": {
"axios": "^0.21.1",
"bcrypt": "^5.0.0",
"bcrypt": "^5.0.1",
"body-parser": "^1.19.0",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
Expand Down Expand Up @@ -63,6 +63,7 @@
"@types/cors": "^2.8.10",
"@types/express": "^4.17.11",
"@types/jest": "^26.0.20",
"@types/jsonwebtoken": "^8.5.1",
"@types/mongoose": "^5.10.3",
"@types/node": "^14.14.22",
"@types/nodemailer": "^6.4.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import { ModuleCore } from '../../../shared/core/ModuleCore';
import { SetPassword } from './application/command/SetPassword';
import { SetPasswordCommandHandler } from './application/command/SetPasswordCommandHandler';
import { AuthenticationRepository } from './application/AuthenticationRepository';
import { IPasswordEncryptor } from '../infrastructure/password/IPasswordEncryptor';

export function AuthenticationModuleCore(
eventPublisher: DomainEventPublisher,
commandPublisher: CommandPublisher,
currentTimeProvider: CurrentTimeProvider,
repository: AuthenticationRepository,
passwordEncryptor: IPasswordEncryptor,
): ModuleCore {
return {
commandHandlers: [
{
commandType: SetPassword,
handler: new SetPasswordCommandHandler(eventPublisher, currentTimeProvider, repository),
handler: new SetPasswordCommandHandler(eventPublisher, currentTimeProvider, repository, passwordEncryptor),
},
],
eventHandlers: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ import { UserAccount } from '../domain/UserAccount';
export interface AuthenticationRepository {
save(userAccount: UserAccount): Promise<void>;

findByEmail(email: string): Promise<UserAccount>;
findById(userId: string): Promise<UserAccount>;

findByEmail(email: string): Promise<UserAccount | undefined>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class GenerateToken {
readonly email: string;
readonly password: string;

constructor(email: string, password: string) {
this.email = email;
this.password = password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CommandHandler } from '../../../../../shared/core/application/command/CommandHandler';
import { DomainEventPublisher } from '../../../../../shared/core/application/event/DomainEventBus';
import { CurrentTimeProvider } from '../../../../../shared/core/CurrentTimeProvider';
import { AuthenticationRepository } from '../AuthenticationRepository';
import { CommandResult } from '../../../../../shared/core/application/command/CommandResult';
import { GenerateToken } from './GenerateToken';
import { authenticateUser } from '../../domain/UserAccount';
import { ITokenGenerator } from '../../../infrastructure/token/ITokenGenerator';
import { IPasswordEncryptor } from '../../../infrastructure/password/IPasswordEncryptor';

export class GenerateTokenCommandHandler implements CommandHandler<GenerateToken> {
constructor(
private readonly eventPublisher: DomainEventPublisher,
private readonly currentTimeProvider: CurrentTimeProvider,
private readonly repository: AuthenticationRepository,
private readonly tokenGenerator: ITokenGenerator,
private readonly passwordEncryptor: IPasswordEncryptor,
) {}

async execute(command: GenerateToken): Promise<CommandResult> {
const userAccount = await this.repository.findByEmail(command.email);
let isPasswordCorrect = false;
let token = '';
if (userAccount) {
isPasswordCorrect = await this.passwordEncryptor.comparePasswords(command.password, userAccount.password.raw);
token = this.tokenGenerator.generateToken(command.email, userAccount.userId.raw);
}
const { state, events } = await authenticateUser(userAccount, command, isPasswordCorrect, token, this.currentTimeProvider());
this.eventPublisher.publishAll(events);
return CommandResult.success(state);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export class SetPassword {
readonly email: string;
readonly userId: string;
readonly password: string;

constructor(email: string, password: string) {
this.email = email;
constructor(userId: string, password: string) {
this.userId = userId;
this.password = password;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import { CommandResult } from '../../../../../shared/core/application/command/Co
import { SetPassword } from './SetPassword';
import { AuthenticationRepository } from '../AuthenticationRepository';
import { setPasswordForUserAccount } from '../../domain/UserAccount';
import { IPasswordEncryptor } from '../../../infrastructure/password/IPasswordEncryptor';

export class SetPasswordCommandHandler implements CommandHandler<SetPassword> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaa, w ogole kiedy to bedzie uzywane, mi przypomnij :) ? Jaki jest use case tego?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eee jeszcze nie, ale pewnie będzie ^_^, chyba fajnie mieć możliwość zmiany hasło :-P

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

było w miro i to zaczęła Ania wgl robić, a ja tam trochę grzebię

constructor(
private readonly eventPublisher: DomainEventPublisher,
private readonly currentTimeProvider: CurrentTimeProvider,
private readonly repository: AuthenticationRepository,
private readonly passwordEncryptor: IPasswordEncryptor,
) {}

async execute(command: SetPassword): Promise<CommandResult> {
const userAccount = await this.repository.findByEmail(command.email);
const { state, events } = setPasswordForUserAccount(userAccount, command, this.currentTimeProvider());
const userAccount = await this.repository.findById(command.userId);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Czy kazdy user moze kazdemu ustawic haslo? Tutaj sie przyda sprawdzenie wlasnie aktualnego usera. Ale to moze byc kolejny PR :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

czyli, że w metodzie setPasswordForUserAccount by się przydało jeszcze sprawdzenie np. tokena? Jeśli tak to kumam o co biega, ale nie mam zielonego pojęcia jak to zrobić .... to by musiało iść przez ten middleware do sprawdzania poprawności tokena, a to trzeba osobno skodzić, Si?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm... no to zalezy :P Najprosciej mozna to zrobic w metodzie execute, ale niezbyt to reuzywalne. Middleware wydaje sie lepszy. Chyba byly na kursach jakies takie rzeczy, poszukaj jak zrobic autoryzacje poprzez middleware. Ta autoryzacja mialaby tak dzialac, ze tylko user moze zmienic haslo dla samego siebie.

const hashedPassword = await this.passwordEncryptor.encryptPassword(command.password);
const { state, events } = await setPasswordForUserAccount(userAccount, command, hashedPassword, this.currentTimeProvider());
await this.repository.save(state);
this.eventPublisher.publishAll(events);
return CommandResult.success();
Expand Down
20 changes: 20 additions & 0 deletions backend/src/modules/authentication/core/domain/Email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class Email {
private readonly TYPE = 'Email';

private constructor(readonly raw: string) {}

static from(email: string): Email {
if (email.length <= 0) {
throw new Error('Email cannot be empty!');
}

const regexp = new RegExp(
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
);
if (!regexp.test(email)) {
throw new Error('Email format is wrong!');
}

return new Email(email);
}
}
13 changes: 13 additions & 0 deletions backend/src/modules/authentication/core/domain/Password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class Password {
private readonly TYPE = 'Password';

private constructor(readonly raw: string) {}

static from(password: string): Password {
if (password.length <= 5) {
throw new Error('Password cannot be shorter than 5 signs!');
}

return new Password(password);
}
}
64 changes: 51 additions & 13 deletions backend/src/modules/authentication/core/domain/UserAccount.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import { DomainCommandResult } from '../../../../shared/core/domain/DomainCommandResult';
import { PasswordWasSet } from './event/PasswordWasSet';
import { TokenGenerated } from './event/TokenGenerated';
import { UserId } from './UserId';
import { Email } from './Email';
import { Password } from './Password';

export class UserAccount {
readonly email: string;
readonly password: string;
readonly userId: UserId;
readonly email: Email;
readonly password: Password;

constructor(props: { email: string; password: string }) {
constructor(props: { userId: UserId; email: Email; password: Password }) {
this.userId = props.userId;
this.email = props.email;
this.password = props.password;
}
}

export function setPasswordForUserAccount(
export async function setPasswordForUserAccount(
state: UserAccount | undefined,
command: { email: string; password: string },
command: { userId: string; password: string },
hashedPassword: string,
currentTime: Date,
): DomainCommandResult<UserAccount> {
if (state) {
throw new Error('Account with this email address already exists.');
): Promise<DomainCommandResult<UserAccount>> {
if (!state) {
throw new Error('Account with this id does not exists.');
}

const passwordWasSet = new PasswordWasSet({
occurredAt: currentTime,
email: command.email,
password: command.password,
userId: command.userId,
password: hashedPassword,
});

const accountWithPasswordSet = onPasswordWasSet(state, passwordWasSet);
Expand All @@ -34,9 +41,40 @@ export function setPasswordForUserAccount(
};
}

function onPasswordWasSet(state: UserAccount | undefined, event: PasswordWasSet): UserAccount {
function onPasswordWasSet(state: UserAccount, event: PasswordWasSet): UserAccount {
return new UserAccount({
email: event.email,
password: event.password,
userId: UserId.from(event.userId),
email: state.email,
password: Password.from(event.password),
});
}

export async function authenticateUser(
state: UserAccount | undefined,
command: { email: string; password: string },
isPasswordCorrect: boolean,
token: string,
currentTime: Date,
): Promise<DomainCommandResult<string>> {
if (!state) {
throw new Error('Such email address does not exists.');
}

if (!isPasswordCorrect) {
throw new Error('Wrong password.');
}

if (token.length == 0) {
throw new Error('Empty token.');
}

const tokenWasGenerated: TokenGenerated = new TokenGenerated({
occurredAt: currentTime,
email: command.email,
});

return {
state: token,
events: [tokenWasGenerated],
};
}
12 changes: 12 additions & 0 deletions backend/src/modules/authentication/core/domain/UserId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class UserId {
private readonly TYPE = 'UserId';

private constructor(readonly raw: string) {}

static from(userId: string): UserId {
if (userId.length <= 0) {
throw new Error('UserId cannot be empty!');
}
return new UserId(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { DomainEvent } from '../../../../../shared/domain/event/DomainEvent';

export class PasswordWasSet implements DomainEvent {
readonly occurredAt: Date;
readonly email: string;
readonly userId: string;
readonly password: string;

constructor(props: { occurredAt: Date; email: string; password: string }) {
constructor(props: { occurredAt: Date; userId: string; password: string }) {
this.occurredAt = props.occurredAt;
this.email = props.email;
this.userId = props.userId;
this.password = props.password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DomainEvent } from '../../../../../shared/domain/event/DomainEvent';

export class TokenGenerated implements DomainEvent {
readonly occurredAt: Date;
readonly email: string;

constructor(props: { occurredAt: Date; email: string }) {
this.occurredAt = props.occurredAt;
this.email = props.email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DomainEvent } from '../../../../../shared/domain/event/DomainEvent';

export class TokenGenerationFailed implements DomainEvent {
readonly occurredAt: Date;
readonly email: string;

constructor(props: { occurredAt: Date; email: string }) {
this.occurredAt = props.occurredAt;
this.email = props.email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IPasswordEncryptor {
encryptPassword(password: string): Promise<string>;

comparePasswords(firstPassword: string, secondPassword: string): Promise<boolean>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IPasswordEncryptor } from './IPasswordEncryptor';
import bcrypt from 'bcrypt';

export class PasswordEncryptor implements IPasswordEncryptor {
async encryptPassword(password: string): Promise<string> {
return await bcrypt.hash(password, 12);
}

async comparePasswords(firstPassword: string, secondPassword: string): Promise<boolean> {
return await bcrypt.compare(firstPassword, secondPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AuthenticationRepository } from '../../../core/application/AuthenticationRepository';
import { UserAccount } from '../../../core/domain/UserAccount';

export class InMemoryAuthenticationRepository implements AuthenticationRepository {
private readonly entities: { [id: string]: UserAccount } = {};

findByEmail(email: string): Promise<UserAccount | undefined> {
return Promise.resolve(Object.values(this.entities).find((userAccount) => userAccount.email.raw === email));
}

findById(userId: string): Promise<UserAccount> {
return Promise.resolve(this.entities[userId]);
}

async save(userAccount: UserAccount): Promise<void> {
this.entities[userAccount.userId.raw] = userAccount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ITokenGenerator {
generateToken(email: string, userId: string): string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ITokenGenerator } from './ITokenGenerator';
import jwt from 'jsonwebtoken';

export class TokenGenerator implements ITokenGenerator {
generateToken(email: string, userId: string): string {
return jwt.sign({ email: email, userId: userId }, `${process.env.JWT_SECRET_KEY}`, { expiresIn: '1h' });
}
}
Loading