Skip to content

huytran17/cheerup

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

Credits

In this code base, the genuis of the application has been contributed by the following engineers:

  • huytran17
  • tungnt1405

Real Artist sign their work

Website

http://cheerup.blog/

Technology stack

Database => mongoose

File storage => FS, AWS S3

Cache management => Redis

Virtual platform => Docker

JWT issuer => jsonwebtoken npm

Authentication => passport.js

API validator => validatorjs

Backend server => NodeJS, ExpressJS

Backend logs => Winston

Unit testing => Jest

Dashboard => NuxtJS

UI/UX => Vuetify

2FA => Google Authenticator

Optimizer => Gulp, Webpack


Clean Architecture

Clean Architecture

Read more at Clean Coder Blog

Benefits of Clean Architecture:

  • Independent of Frameworks
  • Testability
  • Independent of UI
  • Independent of Database
  • Independent of External

Dependencies Rules:

  • Dependencies can only point inwards. That means inner layer should not know/calls outer layer

Layer description:

  • Entities: Contain enterprise business model/object
  • Use Cases: Contain application business rules/ logic
  • Interface Adapter: Contains a set of adapters that convert data from entities/use-case layer to external dependencies such as DB or Web/HTTP
  • Frameworks/ Driver: Compose of frameworks and tools (DB, Web Frameworks)

Cross boundaries between layers:


Yarn dependency upgrade


Style guide

Dependency injection

  1. Create interface/type for an injectable class/function
// <folder>/injectable.ts
export type IInjectable = (params: any) => Promise<string>;
  1. Create a factory function and export it
export default function makeInjectable({
  //Factory function
  dependencies1,
  dependencies2,
}: {
  dependencies1: IDependencies1, // Inject dependencies
  dependencies2: IDependencies2,
}): IInjectable {
  // injectable must implement IInjectable
  return function injectable({
    params1,
  }: {
    params1: Params1Type, // Pass in parameters
  }): string {};
}
  1. Initialize the function by calling the factory in index.ts file
// <folder>/index.ts
import makeInjectable, { IInjectable } from "./injectable.ts"; // Import type and factory

const injectable = makeInjectable({ dependencies1 }); // Pass in instances of dependencies

export default Object.freeze({
  injectable,
});

export { injectable };

How to make a controller:

Must follow the rules of Clean Architecture and Dependencies Injection

Controller sit in Controllers layer of Clean

Reference other controllers if possible, then rename the makeController and Controller

Bad:

export default function makeSomeController() {
  return async function getSomeController() {};
}

Good

// Controller
export default function makeGetSomeController() {
  return async function getSomeController() {
    // make function and internal function must be the same name
  };
}

Dependencies must be injected in via dependency injections

Bad:

import getExample from "../../../use-cases/get-example";

export default function makeGetExampleController() {
  return async function getExampleController() {
    await getExample(); // Directly call use-case
  };
}

Good

// Controller
import { IGetExample } from "../../../use-cases/get-example"; // Import interface

export default function makeGetBlogController({
  getExample,
}: {
  getExample: IGetExample, // Inject dependencies
}) {
  return async function getBlogController() {
    await getExample(); // Directly call use-case
  };
}

Controller functions should take only httpRequest parameter

*Bad

export default function makeGetExampleController() {
  return async function getExampleController(
    httpRequest: {
      context: {
        validated: null,
      },
    },
    someDeps: any
  ) {
    someDeps();
  };
}

*Good

export default function makeGetExampleController({
  someDep,
}: {
  someDep: ISomeDep, // Pass in dependency
}) {
  return async function getExampleController(httpRequest: {
    context: {
      validated: null,
    },
  }) {};
}

All controllers should have try-catch-finally block

Bad:

function controllerFunction(httpRequest) {
  const param = httpRequest.context.validated.param;
  return {
    statusCode: HttpStatusCode.OK,
  };
}

Good:

function controllerFunction(httpRequest) {
  try {
    const param = httpRequest.context.validated.param;
    return {
      statusCode: HttpStatusCode.OK,
    };
  } catch (error) {
    return {
      headers: {
        "Content-Type": "application/json",
      },
      statusCode: error.status.code,
      body: {
        error: error.message,
      },
    };
  }
}

For request validations, create a file in validations folder. Reference src/controllers/user/blog/validators/get-blog.ts

  • controller/<module>/get-example.ts
  • controller/<module>/get-example-rules.ts => validation file

How to make use-case

Use-cases sit in "Use Cases" layer of Clean Architecture. So use-cases should only know about entity layer.

Following the rules of dependencies injection

How to make an entity

These are steps to create a new entity

Entity should not know about / call outer layer function / class

  • Create an interface for the entity. Ex: src/database/entities/interfaces/user.ts
  • Create the class that implement this interface, this is the data object class. Ex: src/database/entities/user.ts
  • Create mongoose schema model for the entity. Ex: src/data-access/schemas/user.ts
  • Register the mongoose schema model in src/data-access/models/index.ts. Should follow registered models
  • Create data-access interface. Ex: src/data-access/interfaces/user-db.ts
  • Create data-access function & factory src/data-access/make-user-db.ts
  • Instantiate data-acess function in index.ts file and inject dependencies (if-have). Ex: src/data-access/index.ts

Naming convention

Variables shall all be kebab_case

Bad:

const hashedPassword = await hashPassword("plain_password_string");

Good:

const hashed_password = await hashPassword("plain_password_string");

Functions shall be camelCased

Bad:

const hashed_password = await hash_password("plain_password_string");

Good:

const hashed_password = await hashPassword("plain_password_string");

All variables should be in kebab casing

Bad

const hasPendingRecommendation = recommendation_exists && is_pending;

Good

const has_pending_recommendation = recommendation_exists && is_pending;

All functions should be in camel casing and start with small letter

Bad

await Apply_referralCode_in_background({ referral_code });

Good

await applyReferralCodeInBackground({ referral_code });

How to run server

Refer to /server folder > README.md for more instructions

  1. cd server folder
  2. Get the .env file
  3. Start the docker containers: yarn dc:up
  4. type yarn install on server home directory if you have not done it before.
  5. type yarn dev
  6. Server will run on http://localhost:3000
  7. If you wish to stop the docker containers, yarn dc:down

How to run user-dashboard

  1. cd user-dashboard folder
  2. type yarn install if you have not done it before.
  3. type yarn dev
  4. access it at http://localhost:8082

How to run admin-dashboard

  1. cd admin-dashboard folder
  2. type yarn install if you have not done it before.
  3. type yarn dev
  4. access it at http://localhost:8080

Getting the server pod id

k -n staging get pod -o jsonpath='{.items[0].metadata.name}' | pbcopy

How to cap a mongodb collection

  1. access the DB via shell
  2. check db.<collection.name>.isCapped();
  3. db.runCommand({"convertToCapped": "logs", size: 80000000})
  4. run db.<collection.name>.isCapped();

Reference: https://www.geeksforgeeks.org/capped-collections-in-mongodb/

Clean architecture

References

Database transaction references

Database queries

Date queries

Mongoose on docker compose

Mongoose on pagination

Github repo references

Typescript readings

Email templates

Authentication readings

Security readings

Process to issue refresh and access token

  1. login with username and password
  2. Find db for this user, and hash the password to check if the password matches.
  3. If match, sign the user object with accessTokenSecret, with expiry of 20m for example, and refreshToken
  4. Return this accessToken and refreshToken back to user

Notes about session and JWT

Session will then store all the sessionId in memory or database. But if you are using JWT, all your information of the user is all contained in the JWT.

You want to validate your JWT so you know the data passed in that JWT is valid for consumption.

Database readings

Express documentation

Phone number regex

References for mobile UI/UX

Lint

CICD

Logging

Express related


Requisite for node version

  • min v14.16