Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / Node.js

Movin' In - Rental Property Management Platform with Mobile App

4.99/5 (21 votes)
16 Sep 2024MIT41 min read 63.9K   1.3K  
Rental Property Management Platform with a Mobile App
In this article, you will learn about Movin' In. A rental property management platform with a mobile app. You will get a brief overview of the platform introducing the frontend, the backend and the mobile app. You will also learn how it was made, including a description of the key parts of the source code and software architecture, how to build Docker image and the mobile app, and how to run the source code.

Table of Contents

  1. Introduction
  2. Features
  3. Live Demo
  4. Prerequisites
  5. Quick Overview
    1. Frontend
    2. Mobile App
    3. Backend
  6. Installing (Self-hosted)
  7. Installing (VPS)
  8. Installing (Docker)
    1. Docker Image
    2. SSL
  9. Setup Stripe
  10. Change Currency
  11. Add New Language
  12. Demo Database
    1. Windows, Linux, macOS
    2. Docker
  13. Build Mobile App
  14. Run from Source
  15. Run Mobile App
  16. Unit Tests and Coverage
  17. Logs
  18. Using the Code
    1. API
    2. Frontend
    3. Mobile App
    4. Backend
  19. Points of Interest
  20. History

Introduction

Movin' In is a Rental Property Management Platform, agency-oriented with a backend for managing properties, customers and bookings, a frontend and a mobile app for renting properties.

Movin' In is designed to work with multiple agencies. Agencies can manage their properties and bookings from the backend. Movin' In can also work with only one agency and can be used as a property rental aggregator.

From the backend, administrators can create and manage agencies, properties, locations, users and bookings.

When new agencies are created, they receive an email prompting them to create their account to access the backend and manage their properties, customers and bookings.

Customers can sign up from the frontend or the mobile app, search for available properties based on location point and time, choose a property and complete the checkout process.

A key design decision was made to use TypeScript instead of JavaScript due to its numerous advantages. TypeScript offers strong typing, tooling, and integration, resulting in high-quality, scalable, more readable and maintainable code that is easy to debug and test.

Movin' In can run in a Docker container. In this article, you can find how to build Movin' In Docker image and run it in a Docker container.

In this article, you will learn how Movin' In was made including a description of the main parts of the source code and the software architecture, how to deploy it, and how to run the source code. But before we dig in, we'll start with a quick overview of the platform.

Features

  • Agency management
  • Ready for one or multiple agencies
  • Property management
  • Booking management
  • Payment management
  • Customer management
  • Multiple payment methods (Credit Card, PayPal, Google Pay, Apple Pay, Link, Pay Later)
  • Operational Stripe Payment Gateway
  • Multiple language support (English, French)
  • Multiple pagination options (Classic pagination with next and previous buttons, infinite scroll)
  • Responsive backend and frontend
  • Native Mobile app for Android and iOS with single codebase
  • Push notifications
  • Secure against XSS, XST, CSRF and MITM
  • Supported Platforms: iOS, Android, Web, Docker

Live Demo

Frontend

Backend

Mobile App

You can install the Android app on any Android device.

Scan this code with a device

Open the Camera app and point it at this code. Then tap the notification that appears.

Image 1

How to install the Mobile App on Android

  • On devices running Android 8.0 (API level 26) and higher, you must navigate to the Install unknown apps system settings screen to enable app installations from a particular location (i.e. the web browser you are downloading the app from).

  • On devices running Android 7.1.1 (API level 25) and lower, you should enable the Unknown sources system setting, found in Settings > Security on your device.

Alternative Way

You can also install the Android App by directly downloading the APK and installing it on any Android device.

Prerequisites

  • TypeScript
  • Node.js
  • Express
  • MongoDB
  • React
  • MUI
  • React Native
  • Expo
  • JWT
  • MVC
  • Docker
  • NGINX
  • Git

Quick Overview

In this section, you'll see a quick overview of the main pages of the frontend, the backend and the mobile app.

Frontend

From the frontend, customers can search for available properties, choose a property and checkout.

Below is the main page of the frontend where the customer can choose a location point and time, and search for available properties.

Image 2

Below is the search result of the main page where the customer can choose a property for rental.

Image 3

Below is the page where the customer can view the details of the property:

Image 4

Below is a view of the images of the property:

Image 5

Below is the checkout page where the customer can set rental options and checkout. If the customer is not registered, he can checkout and register at the same time. He will receive a confirmation and activation email prompting him to set his password if he is not registered yet.

Image 6

Below is the sign in page. On production, authentication cookies are httpOnly, signed, secure and strict sameSite. These options prevent XSS, CSRF and MITM attacks. Authentication cookies are protected against XST attacks as well via a custom middleware.

Image 7

Below is the sign up page.

Image 8

Below is the page where the customer can see and manage his bookings.

Image 9

Below is the page where the customer can see a booking in detail.

Image 10

Below is the page where the customer can see and manage his notifications.

Image 11

Below is the page where the customer can manage his settings.

Image 12

Below is the page where the customer can change his password.

Image 13

There are other pages but these are the main pages of the frontend.

Mobile App

From the mobile app, customers can search for available properties, choose a property and checkout.

The customer receives push notifications, if the status of his booking is updated from the backend.

Below are the main pages of the mobile app where the customer can choose a location point and time, and search for available properties.

Image 14 Image 15 Image 16Image 17Image 18

Below is the search result of the main page where the customer can choose a property for rental and checkout.

Image 19 Image 20 Image 21 Image 22Image 23Image 24

Below are sign in and sign up pages.

Image 25 Image 26 Image 27

Below are the pages where the customer can see and manage his bookings.

Image 28 Image 29 Image 30 Image 31

Below are the pages where the customer can update his profile information, change his password and manage his notifications.

Image 32 Image 33 Image 34 Image 35

That's it for the main pages of the mobile app.

Backend

Movin' In is agency-oriented. This means that there are three types of users:

  • Administrators: They have full access to the backend. They can do everything.
  • Agencies: They have limited access to the backend. They can only manage their properties, bookings and customers.
  • Customers: They have access to the frontend and the mobile app only. They cannot access the backend.

Movin' In is designed to work with multiple agencies. Agencies can manage their properties, customers and bookings from the backend. Movin' In can also work with only one agency as well.

From the backend, administrators can create and manage agencies, properties, locations, customers and bookings.

When new agencies are created, they receive an email prompting them to create an account in order to access the backend and manage their properties and bookings.

Below is the sign in page of the backend.

Image 36

Below is the dashboard page of the backend where administrators and agencies can see and manage bookings.

Image 37

If the status of a booking changes, the related customer will receive a push notification and an email.

Below is the page where properties are displayed and can be managed.

Image 38

Below is the page where administrators and agencies can create new properties by providing images and property info. To include cancellation for free, set it to 0. Otherwise, set the price of the option or leave it empty if you don't want to include it.

Image 39

Below is the page where administrators and agencies can edit properties.

Image 40

Below is the page where administrators can manage customers.

Image 41

Below is the page where agencies and administrators can create bookings if they want. Otherwise, bookings are created automatically when the checkout process is completed from the frontend or the mobile app.

Image 42

Below is the page where to edit bookings.

Image 43

Below is the page where to manage agencies.

Image 44

Below is the page where to create new agencies.

Image 45

Below is the page where to edit agencies.

Image 46

Below is the page where to see agencies' properties.

Image 47

Below is the page where to see customer's bookings.

Image 48

Below is the page where administrators and agencies can manage their settings.

Image 49

There are other pages but these are the main pages of the backend.

Installing (Self-hosted)

Movin' In is cross-platform and can run and be installed on Windows, Linux and macOS.

Below are the installation instructions on Linux.

Prerequisites

  1. Install git, Node.js, NGINX, MongoDB and mongosh. If you want to use MongoDB Atlas, you can skip installing and configuring MongoDB.

  2. Configure MongoDB:

    Shell
    mongosh

    Create admin user:

    db = db.getSiblingDB('admin')
    db.createUser({ user: "admin" , pwd: "PASSWORD", 
    roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

    Replace PASSWORD with a strong password.

    Secure MongoDB:

    Shell
    sudo nano /etc/mongod.conf

    Change configuration as follows:

    net:
      port: 27017
      bindIp: 0.0.0.0
    
    security:
      authorization: enabled

    Restart MongoDB service:

    Shell
    sudo systemctl restart mongod.service
    sudo systemctl status mongod.service

Instructions

  1. Clone movinin repo:
    Shell
    cd /opt
    sudo git clone https://github.com/aelassas/movinin.git
  2. Add permissions:
    Shell
    sudo chown -R $USER:$USER /opt/movinin
    sudo chmod -R +x /opt/movinin/__scripts
  3. Create deployment shortcut:
    Shell
    sudo ln -s /opt/movinin/__scripts/mi-deploy.sh /usr/local/bin/mi-deploy
  4. Create movinin service:
    Shell
    sudo cp /opt/movinin/__services/movinin.service /etc/systemd/system
    sudo systemctl enable movinin.service
  5. Create /opt/movinin/api/.env file:
    NODE_ENV=production
    MI_PORT=4004
    MI_HTTPS=false
    MI_PRIVATE_KEY=/etc/ssl/movinin.key
    MI_CERTIFICATE=/etc/ssl/movinin.pem
    MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin
    MI_DB_SSL=false
    MI_DB_SSL_CERT=/etc/ssl/movinin.pem
    MI_DB_SSL_CA=/etc/ssl/movinin.pem
    MI_DB_DEBUG=false
    MI_COOKIE_SECRET=COOKIE_SECRET
    MI_AUTH_COOKIE_DOMAIN=localhost
    MI_JWT_SECRET=JWT_SECRET
    MI_JWT_EXPIRE_AT=86400
    MI_TOKEN_EXPIRE_AT=86400
    MI_SMTP_HOST=smtp.sendgrid.net
    MI_SMTP_PORT=587
    MI_SMTP_USER=apikey
    MI_SMTP_PASS=PASSWORD
    MI_SMTP_FROM=no-reply@movinin.io
    MI_CDN_USERS=/var/www/cdn/movinin/users
    MI_CDN_TEMP_USERS=/var/www/cdn/movinin/temp/users
    MI_CDN_PROPERTIES=/var/www/cdn/movinin/properties
    MI_CDN_TEMP_PROPERTIES=/var/www/cdn/temp/movinin/properties
    MI_CDN_LOCATIONS=/var/www/cdn/movinin/locations
    MI_CDN_TEMP_LOCATIONS=/var/www/cdn/movinin/temp/locations
    MI_DEFAULT_LANGUAGE=en
    MI_BACKEND_HOST=http://localhost:3003/
    MI_FRONTEND_HOST=http://localhost/
    MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    MI_MINIMUM_AGE=21
    MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    MI_ADMIN_EMAIL=admin@movinin.io
    MI_RECAPTCHA_SECRET=RECAPTCHA_SECRET

    You need to configure the following options:

    MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin
    MI_COOKIE_SECRET=COOKIE_SECRET
    MI_AUTH_COOKIE_DOMAIN=localhost
    MI_JWT_SECRET=JWT_SECRET
    MI_SMTP_HOST=smtp.sendgrid.net
    MI_SMTP_PORT=587
    MI_SMTP_USER=apikey
    MI_SMTP_PASS=PASSWORD
    MI_SMTP_FROM=no-reply@movinin.io
    MI_BACKEND_HOST=http://localhost:3004/
    MI_FRONTEND_HOST=http://localhost/

    If you want to use MongoDB Atlas, put you MongoDB Atlas URI in MI_DB_URI otherwise replace PASSWORD in MI_DB_URI with your MongoDB password. Replace JWT_SECRET with a secret token. Finally, set the SMTP options. SMTP options are necessary for sign up. You can use sendgrid or any other transactional email provider.

    If you choose sendgrid, create an account on sendgrid.com, login and go to the dashboard. On the left panel, click on Email API, then on Integration Guide. Then, choose SMTP Relay and follow the steps. You will be prompted to create an API Key. Once you create the API Key and verify the smtp relay, copy the API key in MI_SMTP_PASS in ./api/.env. Sendgrid's free plan allows to send up to 100 emails/day. If you need to send more than 100 emails/day, switch to a paid plan or choose another transactional email provider.

    COOKIE_SECRET and JWT_SECRET should at least be 32 characters long, but the longer the better. You can use an online password generator and set the password length to 32 or longer.

    The following settings are very important and if they are not set properly, authentication won't work:

    MI_AUTH_COOKIE_DOMAIN=localhost
    MI_BACKEND_HOST=http://localhost:3001/
    MI_FRONTEND_HOST=http://localhost/

    Replace localhost with an IP or FQDN. That is if you access the backend from http://<FQDN>:3001/. MI_BACKEND_HOST should be http://<FQDN>:3001/. The same goes for MI_FRONTEND_HOST. And MI_AUTH_COOKIE_DOMAIN should be FQDN.

    If you want to enable push notifications in the mobile app, follow these instructions and set the following option:

    MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

    If you want to enable stripe payment gateway, sign up for a stripe account, fill the forms and save the publishable key and the secret key from stripe dashboard. Then, set the secret key in the following option in api/.env:

    MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY

    Don't expose stripe secret key on a website or embed it in a mobile application. It must be secret and stored securely in the server-side.

    In stripe, all accounts have a total of four API keys by default - two for test mode and two for live mode:

    • Test mode secret key: Use this key to authenticate requests on your server when in test mode. By default, you can use this key to perform any API request without restriction.
    • Test mode publishable key: Use this key for testing purposes in your web or mobile app’s client-side code.
    • Live mode secret key: Use this key to authenticate requests on your server when in live mode. By default, you can use this key to perform any API request without restriction.
    • Live mode publishable key: Use this key, when you’re ready to launch your app, in your web or mobile app’s client-side code.

    Use only your test API keys for testing. This ensures that you don't accidentally modify your live customers or charges.

    If you want to enable HTTPS, you need to set the following options:

    MI_HTTPS = true
    MI_PRIVATE_KEY=/etc/ssl/movinin.key
    MI_CERTIFICATE=/etc/ssl/movinin.pem
  6. Create /opt/movinin/backend/.env file:
    VITE_NODE_ENV=production
    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_DEFAULT_LANGUAGE=en
    VITE_MI_PAGE_SIZE=30
    VITE_MI_PROPERTIES_PAGE_SIZE=15
    VITE_MI_BOOKINGS_PAGE_SIZE=20
    VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_TEMP_USERS=http://localhost/cdn/movinin/temp/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_TEMP_PROPERTIES=http://localhost/cdn/movinin/temp/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_CDN_TEMP_LOCATIONS=http://localhost/cdn/movinin/temp/locations
    VITE_MI_AGENCY_IMAGE_WIDTH=60
    VITE_MI_AGENCY_IMAGE_HEIGHT=30
    VITE_MI_PROPERTY_IMAGE_WIDTH=300
    VITE_MI_PROPERTY_IMAGE_HEIGHT=200
    VITE_MI_MINIMUM_AGE=21
    VITE_MI_PAGINATION_MODE=classic

    You need to configure the following options:

    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_TEMP_USERS=http://localhost/cdn/movinin/temp/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_TEMP_PROPERTIES=http://localhost/cdn/movinin/temp/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_CDN_TEMP_LOCATIONS=http://localhost/cdn/movinin/temp/locations

    Leave localhost if you want to test locally or replace it with an IP, hostname or FQDN.

    VITE_MI_PAGINATION_MODE: You can choose between classic or infinite_scroll. This option defaults to classic. If you choose classic, you will get a classic pagination with next and previous buttons on desktop and infinite scroll on mobile. If you choose infinite_scroll, you will get infinite scroll on desktop and mobile.

  7. Create /opt/movinin/frontend/.env file:
    VITE_NODE_ENV=production
    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_RECAPTCHA_ENABLED=false
    VITE_MI_RECAPTCHA_SITE_KEY=GOOGLE_RECAPTCHA_SITE_KEY
    VITE_MI_DEFAULT_LANGUAGE=en
    VITE_MI_PAGE_SIZE=30
    VITE_MI_PROPERTIES_PAGE_SIZE=15
    VITE_MI_BOOKINGS_PAGE_SIZE=20
    VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_AGENCY_IMAGE_WIDTH=60
    VITE_MI_AGENCY_IMAGE_HEIGHT=30
    VITE_MI_PROPERTY_IMAGE_WIDTH=300
    VITE_MI_PROPERTY_IMAGE_HEIGHT=200
    VITE_MI_MINIMUM_AGE=21
    VITE_MI_PAGINATION_MODE=classic
    VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_MI_STRIPE_CURRENCY_CODE=USD
    VITE_MI_CURRENCY=$
    VITE_MI_SET_LANGUAGE_FROM_IP=false
    VITE_MI_GOOGLE_ANALYTICS_ENABLED=false
    VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX
    VITE_MI_FB_APP_ID=XXXXXXXXXX
    VITE_MI_APPLE_ID=XXXXXXXXXX
    VITE_MI_GG_APP_ID=XXXXXXXXXX

    You need to configure the following options:

    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_MI_STRIPE_CURRENCY_CODE=USD
    VITE_MI_CURRENCY=$
    VITE_MI_SET_LANGUAGE_FROM_IP=false
    VITE_MI_GOOGLE_ANALYTICS_ENABLED=false
    VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX
    VITE_MI_FB_APP_ID=XXXXXXXXXX
    VITE_MI_APPLE_ID=XXXXXXXXXX
    VITE_MI_GG_APP_ID=XXXXXXXXXX

    Leave localhost if you want to test locally or replace it with an IP, hostname or FQDN.

    If you want to enable stripe payment gateway, set stripe publishable key in VITE_MI_STRIPE_PUBLISHABLE_KEY. You can retrieve it from stripe dashboard.

    VITE_MI_STRIPE_CURRENCY_CODE is the three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR". Required for Stripe payments. Must be a supported currency: https://docs.stripe.com/currencies

    reCAPTCHA is by default disabled. If you want to enable it, you need to set VITE_MI_RECAPTCHA_ENABLED to true and VITE_MI_RECAPTCHA_SITE_KEY to Google reCAPTCHA site key.

  8. If you want to run or build the mobile app, you need to create mobile/.env:
    MI_API_HOST=https://movinin.io:4004
    MI_DEFAULT_LANGUAGE=en
    MI_PAGE_SIZE=20
    MI_PROPERTIES_PAGE_SIZE=8
    MI_BOOKINGS_PAGE_SIZE=8
    MI_CDN_USERS=https://movinin.io/cdn/movinin/users
    MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
    MI_AGENCY_IMAGE_WIDTH=60
    MI_AGENCY_IMAGE_HEIGHT=30
    MI_PROPERTY_IMAGE_WIDTH=300
    MI_PROPERTY_IMAGE_HEIGHT=200
    MI_MINIMUM_AGE=21
    MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    MI_STRIPE_COUNTRY_CODE=US
    MI_STRIPE_CURRENCY_CODE=USD

    You need to configure the following options:

    MI_API_HOST=https://movinin.io:4004
    MI_CDN_USERS=https://movinin.io/cdn/movinin/users
    MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
    MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    MI_STRIPE_COUNTRY_CODE=US
    MI_STRIPE_CURRENCY_CODE=USD

    Replace https://movinin.io with an IP, hostname or FQDN.

    If you want to enable stripe payment gateway, set stripe publishable key in MI_STRIPE_PUBLISHABLE_KEY. You can retrieve it from stripe dashboard.

    MI_STRIPE_MERCHANT_IDENTIFIER is the merchant identifier you registered with Apple for use with Apple Pay.

    MI_STRIPE_COUNTRY_CODE is the two-letter ISO 3166 code of the country of your business, e.g. "US". Required for Stripe payments.

    MI_STRIPE_CURRENCY_CODE is the three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR". Required for Stripe payments. Must be a supported currency: https://docs.stripe.com/currencies

  9. Configure NGINX:
    Shell
    sudo nano /etc/nginx/sites-available/default

    Change the configuration as follows for the frontend:

    server {
        root /var/www/movinin/frontend;
        #listen 443 http2 ssl default_server;
        listen 80 default_server;
        server_name _;
        
        #ssl_certificate_key /etc/ssl/movinin.key;
        #ssl_certificate /etc/ssl/movinin.pem;
    
        access_log /var/log/nginx/movinin.frontend.access.log;
        error_log /var/log/nginx/movinin.frontend.error.log;
    
        index index.html;
    
        location / {
          # First attempt to serve request as file, then as directory,
          # then as index.html, then fall back to displaying a 404.
          try_files $uri $uri/ /index.html =404;
        }
    
        location /cdn {
          alias /var/www/cdn;
        }
    }

    If you want to enable SSL, uncomment and set these lines:

    #listen 443 http2 ssl default_server
    #ssl_certificate_key /etc/ssl/movinin.com.key
    #ssl_certificate /etc/ssl/movinin.com.pem;

    Add the following configuration for the backend:

    server {
        root /var/www/movinin/backend;
        #listen 3003 http2 ssl default_server;
        listen 3003 default_server;
        server_name _;
    
        #ssl_certificate_key /etc/ssl/movinin.key;
        #ssl_certificate /etc/ssl/movinin.pem;
    
        #error_page 497 301 =307 https://$host:$server_port$request_uri;
    
        access_log /var/log/nginx/movinin.backend.access.log;
        error_log /var/log/nginx/movinin.backend.error.log;
    
        index index.html;
    
        location / {
          # First attempt to serve request as file, then as directory,
          # then as index.html, then fall back to displaying a 404.
          try_files $uri $uri/ /index.html =404;
        }
    }

    Create /var/www/cdn/movinin folder and add full access permissions to the user who is running movinin service on /var/www/cdn/movinin.

    If you want to enable SSL, uncomment and set these lines:

    #listen 3003 http2 ssl default_server
    #ssl_certificate_key /etc/ssl/movinin.com.key
    #ssl_certificate /etc/ssl/movinin.com.pem
    #error_page 497 301 =307 https://$host:$server_port$request_uri;

    Then, check NGINX configuration and restart NGINX service:

    Shell
    sudo nginx -t
    sudo systemctl restart nginx.service
    sudo systemctl status nginx.service
  10. Enable the firewall and open movinin ports:
    Shell
    sudo ufw enable
    sudo ufw allow 4004/tcp
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw allow 3003/tcp
    sudo ufw allow 27017/tcp
  11. Start movinin service:
    Shell
    cd /opt/movinin/api
    npm install --omit=dev
    sudo systemctl start movinin.service

    Make sure that movinin service is running with the following command:

    Shell
    sudo systemctl status movinin.service

    Make sure that the database connection is established by checking the logs with the following command:

    Shell
    tail -f /var/log/movinin.log

    Or this one:

    Shell
    sudo journalctl -xfu movinin.service

    Or by opening this file:

    Shell
    tail -f /opt/movinin/api/logs/all.log

    Error logs are written in:

    Shell
    tail -f /opt/movinin/api/logs/error.log
  12. Deploy movinin with the following command:
    Shell
    mi-deploy all

    Movin' In backend is accessible on the port 3003 and the frontend on the port 80.

    If you want to install Movin' In on a VPS with small amount of RAM, you may encounter memory issues while running mi-deploy all. In that case, you need to proceed as follow:

    • Run mi-deploy api to install and run the API.
    • On your desktop PC, set up frontend/.env as described previously, then run the following commands from frontend folder:
      Shell
      npm install
      npm run build
    • Copy the content of frontend/build from your desktop PC to /var/www/movinin/frontend on your VPS.
    • On your desktop PC, set up backend/.env as described previously, then run the following commands from backend folder:
      Shell
      npm install
      npm run build
    • Copy the content of backend/build from your desktop PC to /var/www/movinin/backend on your VPS.
    • Restart NGINX:
      Shell
      sudo rm -rf /var/cache/nginx
      sudo systemctl restart nginx
      sudo systemctl status nginx
  13. If you don't want to use the demo database, create an admin by navigating to hostname:3003/sign-up

  14. Open backend/src/App.tsx and comment these lines to secure the backend:

    const SignUp = lazy(() => import('./pages/SignUp'))
    <Route exact path='/sign-up' element={<SignUp />} />

    And run backend deployment again:

    Shell
    mi-deploy backend

If you want to deploy the frontend only, run the following command:

Shell
mi-deploy frontend

If you want to deploy the api only, run the following command:

Shell
mi-deploy api

If you want to deploy the api, the backend and the frontend, run the following command:

Shell
mi-deploy all

If you want to change the currency, follow these instructions.

Installing (VPS)

This walkthrough shows how to install BookCars on a VPS running under Ubuntu 22.04 with at least 512MB of RAM and one CPU.

Installing (Docker)

Movin' In can run in a Docker container on Linux and Docker Desktop for Windows or Mac.

Docker Image

This section describes how to build Movin' In Docker image and run it in a Docker container.

  1. Clone Movin' In repo:
    Shell
    git clone https://github.com/aelassas/movinin.git
  2. Create ./api/.env.docker file with the following content:
    NODE_ENV=production
    MI_PORT=4004
    MI_HTTPS=false
    MI_PRIVATE_KEY=/etc/ssl/movinin.key
    MI_CERTIFICATE=/etc/ssl/movinin.crt
    MI_DB_URI=mongodb://admin:PASSWORD@mongo:27017/movinin?authSource=admin&appName=movinin
    MI_DB_SSL=false
    MI_DB_SSL_CERT=/etc/ssl/movinin.crt
    MI_DB_SSL_CA=/etc/ssl/movinin.ca.pem
    MI_DB_DEBUG=false
    MI_COOKIE_SECRET=COOKIE_SECRET
    MI_AUTH_COOKIE_DOMAIN=localhost
    MI_JWT_SECRET=JWT_SECRET
    MI_JWT_EXPIRE_AT=86400
    MI_TOKEN_EXPIRE_AT=86400
    MI_SMTP_HOST=smtp.sendgrid.net
    MI_SMTP_PORT=587
    MI_SMTP_USER=apikey
    MI_SMTP_PASS=PASSWORD
    MI_SMTP_FROM=admin@movinin.io
    MI_CDN_USERS=/var/www/cdn/movinin/users
    MI_CDN_TEMP_USERS=/var/www/cdn/movinin/temp/users
    MI_CDN_PROPERTIES=/var/www/cdn/movinin/properties
    MI_CDN_TEMP_PROPERTIES=/var/www/cdn/movinin/temp/properties
    MI_CDN_LOCATIONS=/var/www/cdn/movinin/locations
    MI_CDN_TEMP_LOCATIONS=/var/www/cdn/movinin/temp/locations
    MI_DEFAULT_LANGUAGE=en
    MI_BACKEND_HOST=http://localhost:3003/
    MI_FRONTEND_HOST=http://localhost/
    MI_MINIMUM_AGE=21
    MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    MI_ADMIN_EMAIL=admin@movinin.io
    MI_RECAPTCHA_SECRET=RECAPTCHA_SECRET

    Set the following options:

    MI_DB_URI=mongodb://admin:PASSWORD@mongo:27017/movinin?authSource=admin&appName=movinin
    MI_COOKIE_SECRET=COOKIE_SECRET
    MI_AUTH_COOKIE_DOMAIN=localhost
    MI_JWT_SECRET=JWT_SECRET
    MI_SMTP_HOST=smtp.sendgrid.net
    MI_SMTP_PORT=587
    MI_SMTP_USER=apikey
    MI_SMTP_PASS=PASSWORD
    MI_SMTP_FROM=admin@movinin.io
    MI_BACKEND_HOST=http://localhost:3003/
    MI_FRONTEND_HOST=http://localhost/ 

    If you want to use MongoDB Atlas, put you MongoDB Atlas URI in MI_DB_URI otherwise replace PASSWORD in MI_DB_URI with your MongoDB password. Replace JWT_SECRET with a secret token. Finally, set the SMTP options. SMTP options are necessary for sign up. You can use sendgrid or any other transactional email provider.

    If you choose sendgrid, create an account on sendgrid.com, login and go to the dashboard. On the left panel, click on Email API, then on Integration Guide. Then, choose SMTP Relay and follow the steps. You will be prompted to create an API Key. Once you create the API Key and verify the smtp relay, copy the API key in MI_SMTP_PASS in ./api/.env. Sendgrid's free plan allows to send up to 100 emails/day. If you need to send more than 100 emails/day, switch to a paid plan or choose another transactional email provider.

    COOKIE_SECRET and JWT_SECRET should at least be 32 characters long, but the longer the better. You can use an online password generator and set the password length to 32 or longer.

    MI_AUTH_COOKIE_DOMAIN=localhost
    MI_BACKEND_HOST=http://localhost:3001/
    MI_FRONTEND_HOST=http://localhost/

    Replace localhost with an IP or FQDN. That is if you access the backend from http://<FQDN>:3001/. MI_BACKEND_HOST should be http://<FQDN>:3001/. The same goes for MI_FRONTEND_HOST. And MI_AUTH_COOKIE_DOMAIN should be FQDN.

    Leave localhost if you want to test locally.

    If you want to enable push notifications in the mobile app, follow these instructions and set the following option:

    MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN 

    If you want to enable stripe payment gateway, sign up for a stripe account, fill the forms and save the publishable key and the secret key from stripe dashboard. Then, set the secret key in the following option in api/.env:

    MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY

    Don't expose stripe secret key on a website or embed it in a mobile application. It must be secret and stored securely in the server-side.

    In stripe, all accounts have a total of four API keys by default - two for test mode and two for live mode:

    • Test mode secret key: Use this key to authenticate requests on your server when in test mode. By default, you can use this key to perform any API request without restriction.
    • Test mode publishable key: Use this key for testing purposes in your web or mobile app’s client-side code.
    • Live mode secret key: Use this key to authenticate requests on your server when in live mode. By default, you can use this key to perform any API request without restriction.
    • Live mode publishable key: Use this key, when you’re ready to launch your app, in your web or mobile app’s client-side code.

    Use only your test API keys for testing. This ensures that you don't accidentally modify your live customers or charges.

  3. Create ./backend/.env.docker file with the following content:
    VITE_NODE_ENV=production
    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_DEFAULT_LANGUAGE=en
    VITE_MI_PAGE_SIZE=30
    VITE_MI_PROPERTIES_PAGE_SIZE=15
    VITE_MI_BOOKINGS_PAGE_SIZE=20
    VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_TEMP_USERS=http://localhost/cdn/movinin/temp/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_TEMP_PROPERTIES=http://localhost/cdn/movinin/temp/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_CDN_TEMP_LOCATIONS=http://localhost/cdn/movinin/temp/locations
    VITE_MI_AGENCY_IMAGE_WIDTH=60
    VITE_MI_AGENCY_IMAGE_HEIGHT=30
    VITE_MI_PROPERTY_IMAGE_WIDTH=300
    VITE_MI_PROPERTY_IMAGE_HEIGHT=200
    VITE_MI_MINIMUM_AGE=21
    VITE_MI_PAGINATION_MODE=classic 

    Set the following options:

    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_TEMP_USERS=http://localhost/cdn/movinin/temp/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_TEMP_PROPERTIES=http://localhost/cdn/movinin/temp/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_CDN_TEMP_LOCATIONS=http://localhost/cdn/movinin/temp/locations

    Leave localhost if you want to test locally or Replace it with an IP, hostname or FQDN.

    If you want to change the pagination mode, change VITE_MI_PAGINATION_MODE option. You can choose between classic or infinite_scroll. This option defaults to classic. If you choose classic, you will get a classic pagination with next and previous buttons on desktop and infinite scroll on mobile. If you choose infinite_scroll, you will get infinite scroll on desktop and mobile.

  4. Create ./frontend/.env.docker file with the following content:
    VITE_NODE_ENV=production
    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_RECAPTCHA_ENABLED=false
    VITE_MI_DEFAULT_LANGUAGE=en
    VITE_MI_PAGE_SIZE=30
    VITE_MI_PROPERTIES_PAGE_SIZE=15
    VITE_MI_BOOKINGS_PAGE_SIZE=20
    VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_AGENCY_IMAGE_WIDTH=60
    VITE_MI_AGENCY_IMAGE_HEIGHT=30
    VITE_MI_PROPERTY_IMAGE_WIDTH=300
    VITE_MI_PROPERTY_IMAGE_HEIGHT=200
    VITE_MI_MINIMUM_AGE=21
    VITE_MI_PAGINATION_MODE=classic 
    VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_MI_STRIPE_CURRENCY_CODE=USD
    VITE_MI_CURRENCY=$
    VITE_MI_SET_LANGUAGE_FROM_IP=false
    VITE_MI_GOOGLE_ANALYTICS_ENABLED=false
    VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX
    VITE_MI_FB_APP_ID=XXXXXXXXXX
    VITE_MI_APPLE_ID=XXXXXXXXXX
    VITE_MI_GG_APP_ID=XXXXXXXXXX

    Set the following options:

    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_STRIPE_CURRENCY_CODE=USD
    VITE_MI_CURRENCY=$
    VITE_MI_SET_LANGUAGE_FROM_IP=false
    VITE_MI_GOOGLE_ANALYTICS_ENABLED=false
    VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX
    VITE_MI_FB_APP_ID=XXXXXXXXXX
    VITE_MI_APPLE_ID=XXXXXXXXXX
    VITE_MI_GG_APP_ID=XXXXXXXXXX

    Leave localhost if you want to test locally or Replace it with an IP, hostname or FQDN.

    If you want to enable stripe payment gateway, set stripe publishable key in VITE_MI_STRIPE_PUBLISHABLE_KEY. You can retrieve it from stripe dashboard.

    VITE_MI_STRIPE_CURRENCY_CODE is the three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR". Required for Stripe payments. Must be a supported currency: https://docs.stripe.com/currencies

    If you want to change pagination mode, change VITE_MI_PAGINATION_MODE option.

    reCAPTCHA is by default disabled on the frontend. If you want to enable it, you have to set VITE_MI_RECAPTCHA_ENABLED to true and VITE_MI_RECAPTCHA_SITE_KEY to Google reCAPTCHA site key.

  5. Open ./docker-compose.yml and set MongoDB password:
    version: "3.8"
    services:
      api:
        build: 
          context: .
          dockerfile: ./api/Dockerfile
        env_file: ./api/.env.docker
        restart: always
        ports:
          - 4004:4004
        depends_on:
          - mongo
        volumes:
          - cdn:/var/www/cdn/movinin
    
      mongo:
        image: mongo:latest
        command: mongod --quiet --logpath /dev/null
        restart: always
        environment:
          # Provide your credentials here
          MONGO_INITDB_ROOT_USERNAME: admin
          MONGO_INITDB_ROOT_PASSWORD: PASSWORD
        ports:
          - 27017:27017
    
      backend:
        build: 
          context: .
          dockerfile: ./backend/Dockerfile
        depends_on:
          - api
        ports:
          - 3003:3003
    
      frontend:
        build: 
          context: .
          dockerfile: ./frontend/Dockerfile
        depends_on:
          - api
        ports:
          - 80:80
        volumes:
          - cdn:/var/www/cdn/movinin
    
    volumes:
      cdn:

    If you want to use MongoDB Atlas, remove mongo container. Otherwise, replace PASSWORD with the password that you have set in MI_DB_URI in ./api/.env.docker.

  6. Build and run Docker image:
    Shell
    sudo docker compose up

    To run the compose in background, add the -d option with the command:

    Shell
    sudo docker compose up -d

    If you want to rebuild, use the following command:

    Shell
    docker compose up --build --force-recreate --no-deps api backend frontend

    If you want to check the logs of the containers for troubleshooting, use the following command:

    Shell
    sudo docker compose logs

    If you want to rebuild without cache, use the following commands:

    Shell
    docker compose build --no-cache api backend frontend
    docker compose up --force-recreate --no-deps api backend frontend

     

That's it! Movin' In backend is accessible from http://<hostname>:3003 and Movin' In frontend is accessible from http://<hostname>.

If you run Movin' In for the first time, you'll start from an empty database. So you have to create an administrator from the backend by filling the from http://<hostname>:3003/sign-up. SMTP settings need to be configured to process sign up. Then, secure the backend by opening backend/src/App.tsx and commenting the following lines:

const SignUp = lazy(() => import('./pages/SignUp'))
<Route exact path='/sign-up' element={<Signup />} /> 

You'll need to rebuild and run Docker image:

Shell
sudo docker compose build --no-cache
sudo docker compose up

Once you create the administrator user, do the following:

  • Go to the agencies page and create one or multiple agencies.
  • Go to the locations page and create one or multiple locations.
  • Go to the properties page and create one or multiple properties.
  • Go to the frontend, sign up, choose a property and checkout.

Finally, you will see bookings listed in the backend dashboard.

You can use the demo database if you want.

Below are Docker configuration files:

That's it! You can explore the other pages in the backend and the frontend.

SSL

This section will walk you through how to enable SSL in the API, the backend and the frontend in a docker container.

Copy your private key movinin.key and your certificate movinin.crt in ./ next to docker-compose.yml.

movinin.key will be loaded as /etc/ssl/movinin.key and movinin.crt will be loaded as /etc/ssl/movinin.crt in ./docker-compose.yml.

API

For the API, update ./api/.env.docker as follows to enable SSL:

MI_HTTPS=true
MI_PRIVATE_KEY=/etc/ssl/movinin.key
MI_CERTIFICATE=/etc/ssl/movinin.crt
MI_BACKEND_HOST=http://localhost:3003/
MI_FRONTEND_HOST=http://localhost/ 

Replace http://localhost with https://<fqdn>.

Backend

For the backend, update the following options in ./backend/.env.docker:

VITE_MI_API_HOST=http://localhost:4004
VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
VITE_MI_CDN_TEMP_USERS=http://localhost/cdn/movinin/temp/users
VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
VITE_MI_CDN_TEMP_PROPERTIES=http://localhost/cdn/movinin/temp/properties 

Replace http://localhost with https://<fqdn>.

Then, update ./backend/nginx.conf as follows to enable SSL:

server {
    listen 3003 ssl;
    root /usr/share/nginx/html;
    index index.html;

    ssl_certificate_key /etc/ssl/movinin.key;
    ssl_certificate /etc/ssl/movinin.crt;

    error_page 497 301 =307 https://$host:$server_port$request_uri;

    access_log /var/log/nginx/backend.access.log;
    error_log /var/log/nginx/backend.error.log;

    location / {
        # First attempt to serve request as file, then as directory,
        # then as index.html, then fall back to displaying a 404.
        try_files $uri $uri/ /index.html =404;
    }
} 

Frontend

For the frontend, update the following options in ./frontend/.env.docker:

VITE_MI_API_HOST=http://localhost:4004
VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties 

Replace http://localhost with https://<fqdn>.

Add the port 443 to ./frontend/Dokerfile as follows:

# syntax=docker/dockerfile:1

FROM node:lts-alpine as build
WORKDIR /movinin/frontend
COPY ./frontend ./
COPY ./frontend/.env.docker .env
COPY ./packages /movinin/packages
RUN npm install
RUN npm run build

FROM nginx:stable-alpine
WORKDIR /usr/share/nginx/html
RUN rm -rf -- *
COPY --from=build /movinin/frontend/build .
COPY ./frontend/nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
EXPOSE 80
EXPOSE 443 

Then, update ./frontend/nginx.conf as follows to enable SSL:

server {
    listen 80;
    return 301 https://$host$request_uri;
}
server {
    listen 443 ssl;
    root /usr/share/nginx/html;
    index index.html;

    ssl_certificate_key /etc/ssl/movinin.key;
    ssl_certificate /etc/ssl/movinin.crt;

    access_log /var/log/nginx/frontend.access.log;
    error_log /var/log/nginx/frontend.error.log;

    location / {
      # First attempt to serve request as file, then as directory,
      # then as index.html, then fall back to displaying a 404.
      try_files $uri $uri/ /index.html =404;
    }

    location /cdn {
      alias /var/www/cdn;
    }
} 

docker-compose.yml

Update ./docker-compose.yml to load your private key movinin.key and your certificate movinin.crt, and add the port 443 to the frontend as follows:

version: "3.8"
services:
  api:
    build: 
      context: .
      dockerfile: ./api/Dockerfile
    env_file: ./api/.env.docker
    restart: always
    ports:
      - 4004:4004
    depends_on:
      - mongo
    volumes:
      - cdn:/var/www/cdn/movinin
      - ./movinin.key:/etc/ssl/movinin.key
      - ./movinin.crt:/etc/ssl/movinin.crt

  mongo:
    image: mongo:latest
    restart: always
    environment:
      # Provide your credentials here
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: PASSWORD
    ports:
      - 27017:27017

  backend:
    build: 
      context: .
      dockerfile: ./backend/Dockerfile
    depends_on:
      - api
    ports:
      - 3003:3003
    volumes:
      - ./movinin.key:/etc/ssl/movinin.key
      - ./movinin.crt:/etc/ssl/movinin.crt

  frontend:
    build: 
      context: .
      dockerfile: ./frontend/Dockerfile
    depends_on:
      - api
    ports:
      - 80:80
      - 443:443
    volumes:
      - cdn:/var/www/cdn/movinin
      - ./movinin.key:/etc/ssl/movinin.key
      - ./movinin.crt:/etc/ssl/movinin.crt

volumes:
  cdn:

Rebuild and run Docker image:

Shell
sudo docker compose build --no-cache
sudo docker compose up

Setup Stripe

If you want to enable stripe payment gateway, sign up for a stripe account, fill the forms and save the publishable key and the secret key from Stripe Developers Dashboard.

Don't expose stripe secret key on a website or embed it in a mobile application. It must be secret and stored securely in the server-side.

In stripe, all accounts have a total of four API keys by default-two for test mode and two for live mode:

  • Test mode secret key: Use this key to authenticate requests on your server when in test mode. By default, you can use this key to perform any API request without restriction.
  • Test mode publishable key: Use this key for testing purposes in your web or mobile app’s client-side code.
  • Live mode secret key: Use this key to authenticate requests on your server when in live mode. By default, you can use this key to perform any API request without restriction.
  • Live mode publishable key: Use this key, when you’re ready to launch your app, in your web or mobile app’s client-side code.

You can find your secret and publishable keys on the API keys page in Stripe Developers Dashboard.

Use only your test API keys for testing and development. This ensures that you don't accidentally modify your live customers or charges.

On production, use HTTPS in the API, the backend, the frontend and the mobile app to be able to use stripe payment gateway.

API

Set stripe secret key in the following option in api/.env:

MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY

Frontend

Set Stripe publishable key and currency in the following options in frontend/.env:

VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
VITE_MI_STRIPE_CURRENCY_CODE=USD

Mobile App

Set Stripe publishable key and other Stripe settings in mobile/.env:

MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
MI_STRIPE_COUNTRY_CODE=US
MI_STRIPE_CURRENCY_CODE=USD

MI_STRIPE_MERCHANT_IDENTIFIER is the merchant identifier you registered with Apple for use with Apple Pay.

MI_STRIPE_COUNTRY_CODE is the two-letter ISO 3166 code of the country of your business, e.g. "US". Required for Stripe payments.

MI_STRIPE_CURRENCY_CODE is the three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR". Required for Stripe payments. Must be a supported currency: https://docs.stripe.com/currencies

You also need to set merchantIdentifier in plugins sections in mobile/app.json if you want to enable Apple Pay.

Google Pay

Google Pay is not supported in Expo Go. To use Google Pay, you must create a development build. This can be done with EAS Build, or locally by running npx expo run:android.

Apple Pay

Apple Pay is not supported in Expo Go. To use Apple Pay, you must create a development build. This can be done with EAS Build, or locally by running npx expo run:ios.

Change Currency

To change the currency, follow these instructions:

Frontend

Open frontend/.env and change VITE_MI_CURRENCY and VITE_MI_STRIPE_CURRENCY_CODE settings.

By default, it is set to:

VITE_MI_CURRENCY=$
VITE_MI_STRIPE_CURRENCY_CODE=USD

For example, if you want to change to euro:

VITE_MI_CURRENCY=€
VITE_MI_STRIPE_CURRENCY_CODE=EUR

On production, you need to rebuild the frontend to apply changes.

Backend

Open backend/.env and change VITE_MI_CURRENCY setting.

By default, it is set to:

VITE_MI_CURRENCY=$

On production, you need to rebuild the backend to apply changes.

Mobile App

Open mobile/.env and change MI_CURRENCY and MI_STRIPE_CURRENCY_CODE settings.

By default, it is set to:

MI_CURRENCY=$
MI_STRIPE_CURRENCY_CODE=USD

For example, if you want to change to euro:

MI_CURRENCY=€
MI_STRIPE_CURRENCY_CODE=EUR

On production, you need to rebuild the mobile app to apply changes.

Add New Language

To add a new language proceed as follow:

API

  1. Add the new language ISO 639-1 code to LANGUAGES setting in api/src/config/env.config.ts.
  2. Create a new file <ISO 639-1 code>.ts in src/lang folder and add the translations in it.
  3. Add your translations to src/lang/i18n.ts

Backend and frontend

  1. Add the new language ISO 639-1 code and its label in src/config/env.config.ts in LANGUAGES constant.
  2. Add the translations in src/lang/*.ts.

Mobile App

  1. Add the new language ISO 639-1 code and its label in config/env.config.ts in LANGUAGES constant.
  2. Create a new file <ISO 639-1 code>.ts in lang folder and add the translations in it.
  3. Add your translations to lang/i18n.ts

Demo Database

Windows, Linux and macOS

  • Download and install MongoDB Command Line Database Tools.
  • On Windows, add MongoDB Command Line Database Tools folder to Path environment variable.
  • Download movinin-db.zip down to your machine, unzip it and go to the unzipped folder from a terminal.
  • Restore Movin' In demo database by using the following command:
    Shell
    mongorestore --verbose --drop --gzip --host=127.0.0.1 --port=27017 --username=admin --password=$PASSWORD --authenticationDatabase=admin --nsInclude="movinin.*" --archive=movinin.gz

Replace $PASSWORD with your MongoDB password.

If you are using MongoDB Atlas, put your MongoDB Atlas URI in --uri= command line argument:

mongorestore --verbose --drop --gzip --uri=mongodb://admin:$PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin --nsInclude="movinin.*" --nsFrom="movinin.*" --nsTo="movinin.*" --archive=movinin.gz

Copy the content of cdn folder on your web server so that the files will be accessible through http://localhost/cdn/movinin/.

cdn folder contains the following folders:

  • users: This folder contains users’ avatars and agencies’ images.
  • properties: This folder contains properties’ images.
  • temp: This folder contains temporary files.

If you want to run Movin' In from the source code or install it on Windows or Linux without using Docker, proceed as follows:

  • On Windows, install IIS and copy the content of cdn folder in C:\inetpub\wwwroot\cdn\movinin. Finally, add full access permissions to the user who is running Movin' In API on C:\inetpub\wwwroot\cdn\movinin.
  • On Linux, install NGINX and copy content of cdn folder in /var/www/cdn/movinin. Then, update /etc/nginx/sites-enabled/default as follows:
    server {
        listen 80 default_server;
        server_name _;
        
        ...
    
        location /cdn {
          alias /var/www/cdn;
        }
    } 

    Finally, add full access permissions to the user who is running Movin' In API on /var/www/cdn/movinin.

Backend credentials:

Frontend and mobile app credentials:

Docker

To restore Movin' In demo database in Docker container, proceed as follows:

  1. Make sure that the ports 80, 3003, 4004 and 27017 are not used by any application.
  2. Download and install MongoDB Command Line Database Tools on your local machine.
  3. Add MongoDB Command Line Database Tools folder to Path environment variable in your local machine.
  4. Download movinin-db.zip down to your local machine and unzip it.
  5. Run the compose:
    docker compose up
  6. Go to movinin-db folder and restore the demo database with the following command:
    mongorestore --verbose --drop --gzip --host=127.0.0.1 --port=27017 --username=admin --password=$PASSWORD --authenticationDatabase=admin --nsInclude="movinin.*" --archive=movinin.gz
    Replace $PASSWORD with your MongoDB password set in your docker-compose.yml.
  7. Get API Docker container name with the following command:
    docker container ls
    The name should be something like this: src-api-1
  8. Go to movinin-db/cdn folder and copy the contents of the folder in API container with the following commands:
    docker cp ./cdn/users src-api-1:/var/www/cdn/movinin
    docker cp ./cdn/properties src-api-1:/var/www/cdn/movinin
    Replace src-api-1 with your API container name.
  9. Go to the backend http://localhost:3003 and login with the following credentials:
    Username: admin@movinin.io
    Password: M00vinin
  10. Go to the frontend http://localhost and login with the following credentials:
    Username: jdoe@movinin.io
    Password: M00vinin

Build Mobile App

Prerequisites

To build Movin' In mobile app, you need to have the following tools installed on your machine:

Install eas-cli with the following command:

Shell
npm i -g eas-cli 

Configuration

  • You need to download the google-services.json file and place it in ./mobile root directory for push notifications. Otherwise, the mobile app won't build. Don't forget to set Firebase Server key in expo.dev > Credentials > Service Credentials > Google Cloud Messaging Token as stated in the documentation.

  • If you don't have an Expo account, you'll need to create one to build Movin' In mobile app.

  • Go to expo.dev, Click on Projects then Create a Project. Set Movin' In as project name and click on Create.

  • Go to Movin' In project and copy the project ID. Open ./mobile/app.json and paste the project ID in extra.eas.projectId.

  • Create an Expo Access Token from expo.dev (Account Settings > Access Tokens) and set api/.env MI_EXPO_ACCESS_TOKEN setting:

    MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN 
  • Create mobile/.env file with the following content:
    MI_API_HOST=https://movinin.io:4004
    MI_DEFAULT_LANGUAGE=en
    MI_PAGE_SIZE=20
    MI_PROPERTIES_PAGE_SIZE=8
    MI_BOOKINGS_PAGE_SIZE=8
    MI_CDN_USERS=https://movinin.io/cdn/movinin/users
    MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
    MI_AGENCY_IMAGE_WIDTH=60
    MI_AGENCY_IMAGE_HEIGHT=30
    MI_PROPERTY_IMAGE_WIDTH=300
    MI_PROPERTY_IMAGE_HEIGHT=200
    MI_MINIMUM_AGE=21
    MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    MI_STRIPE_COUNTRY_CODE=US
    MI_STRIPE_CURRENCY_CODE=USD

    You need to configure the following options:

    MI_API_HOST=https://movinin.io:4004
    MI_CDN_USERS=https://movinin.io/cdn/movinin/users
    MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
    MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    MI_STRIPE_COUNTRY_CODE=US
    MI_STRIPE_CURRENCY_CODE=USD

    Replace https://movinin.io with an IP, hostname or FQDN.

    If you want to enable stripe payment gateway, set stripe publishable key in MI_STRIPE_PUBLISHABLE_KEY. You can retrieve it from stripe dashboard.

    MI_STRIPE_MERCHANT_IDENTIFIER is the merchant identifier you registered with Apple for use with Apple Pay.

    MI_STRIPE_COUNTRY_CODE is the two-letter ISO 3166 code of the country of your business, e.g. "US". Required for Stripe payments.

    MI_STRIPE_CURRENCY_CODE is the three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR". Required for Stripe payments. Must be a supported currency: https://docs.stripe.com/currencies

Production Build

If you want to use Movin' In mobile app on production, you should use HTTPS in Movin' In API and disable usesCleartextTraffic expo plugin in ./mobile/app.json by removing the line "./plugins/usesCleartextTraffic" in plugins section.

Instructions

  • Clone the source down to your machine:
    Shell
    git clone https://github.com/aelassas/movinin.git 
  • Go to mobile folder:
    Shell
    cd ./mobile 
  • Run the following command:
    Shell
    npm install

Android

EAS Build

To build Movin' In Android app with EAS Build hosted service, run the following command:

Shell
npm run build:android
Local Build

macOS or Linux are required for local builds. For Windows, you should use EAS Builds.

You need to install Android Studio, openjdk-17, and set ANDROID_HOME and JAVA_HOME environment variables. Then, run the following command:

Shell
npm run build:android:local 

On macOS, if you face some issues regarding local build, try to set ANDROID_HOME and JAVA_HOME environment variables in ./mobile/eas.json as follows:

JavaScript
{
  "cli": {
    "version": ">= 0.53.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "android": {
        "image": "latest",
        "gradleCommand": ":app:assembleDebug"
      },
      "ios": {
        "image": "latest",
        "buildConfiguration": "Debug"
      }
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {
      "env": {
        "ANDROID_HOME": "/path/to/android/sdk",
        "JAVA_HOME": "/path/to/java/home"
      },
      "android": {
        "image": "latest",
        "buildType": "apk"
      },
      "ios": {
        "image": "latest"
      }
    }
  },
  "submit": {
    "production": {}
  }
}

iOS

You need a paid Apple Developer account to build the iOS app for both EAS builds and local builds.

EAS Build

To build Movin' In iOS app, run the following command:

Shell
npm run build:ios
Local Build

You need to install fastlane and CocoaPods on macOS.

To build Movin' In iOS app locally, run the following command:

npm run build:ios:local

Run from Source

Below are the instructions to run Movin' In from source code.

Prerequisites

  1. Install git, Node.js, NGINX or IIS on Windows, MongoDB and mongosh. If you want to use MongoDB Atlas, you can skip installing and configuring MongoDB.

  2. Configure MongoDB:

    Shell
    mongosh

    Create administrator user:

    db = db.getSiblingDB('admin')
    db.createUser({ user: "admin" , pwd: "PASSWORD", 
    roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

    Replace PASSWORD with a strong password.

    Secure MongoDB by changing mongod.conf as follows:

    net:
      port: 27017
      bindIp: 0.0.0.0
    
    security:
      authorization: enabled

    Restart MongoDB service.

Instructions

  1. Clone Movin' In repo:
    Shell
    git clone https://github.com/aelassas/movinin.git
  2. Create api/.env file with the following content:
    NODE_ENV=development
    MI_PORT=4004
    MI_HTTPS=false
    MI_PRIVATE_KEY=/etc/ssl/movinin.key
    MI_CERTIFICATE=/etc/ssl/movinin.crt
    MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin
    MI_DB_SSL=false
    MI_DB_SSL_CERT=/etc/ssl/movinin.crt
    MI_DB_SSL_CA=/etc/ssl/movinin.ca.pem
    MI_DB_DEBUG=false
    MI_COOKIE_SECRET=COOKIE_SECRET
    MI_AUTH_COOKIE_DOMAIN=localhost
    MI_JWT_SECRET=JWT_SECRET
    MI_JWT_EXPIRE_AT=86400
    MI_TOKEN_EXPIRE_AT=86400
    MI_SMTP_HOST=smtp.sendgrid.net
    MI_SMTP_PORT=587
    MI_SMTP_USER=apikey
    MI_SMTP_PASS=PASSWORD
    MI_SMTP_FROM=admin@movinin.io
    MI_CDN_USERS=/var/www/cdn/movinin/users
    MI_CDN_TEMP_USERS=/var/www/cdn/movinin/temp/users
    MI_CDN_PROPERTIES=/var/www/cdn/movinin/properties
    MI_CDN_TEMP_PROPERTIES=/var/www/cdn/movinin/temp/properties
    MI_CDN_LOCATIONS=/var/www/cdn/movinin/locations
    MI_CDN_TEMP_LOCATIONS=/var/www/cdn/movinin/temp/locations
    MI_DEFAULT_LANGUAGE=en
    MI_BACKEND_HOST=http://localhost:3003/
    MI_FRONTEND_HOST=http://localhost:3004/
    MI_MINIMUM_AGE=21
    MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    MI_ADMIN_EMAIL=admin@movinin.io
    MI_RECAPTCHA_SECRET=RECAPTCHA_SECRET

    On Windows, install IIS and update the following settings with these values:

    MI_CDN_USERS=C:\inetpub\wwwroot\cdn\movinin\users
    MI_CDN_TEMP_USERS=C:\inetpub\wwwroot\cdn\movinin\temp\users
    MI_CDN_PROPERTIES=C:\inetpub\wwwroot\cdn\movinin\properties
    MI_CDN_TEMP_PROPERTIES=C:\inetpub\wwwroot\cdn\movinin\temp\properties 

    Add full access permissions to the user who is running Movin' In API on C:\inetpub\wwwroot\cdn\movinin.

    You need to configure the following options:

    MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin
    MI_COOKIE_SECRET=COOKIE_SECRET
    MI_JWT_SECRET=JWT_SECRET
    MI_SMTP_HOST=smtp.sendgrid.net
    MI_SMTP_PORT=587
    MI_SMTP_USER=apikey
    MI_SMTP_PASS=PASSWORD
    MI_SMTP_FROM=admin@movinin.io

    If you want to use MongoDB Atlas, put you MongoDB Atlas URI in MI_DB_URI otherwise replace PASSWORD in MI_DB_URI with your MongoDB password. Replace JWT_SECRET with a secret token. Finally, set the SMTP options. SMTP options are necessary for sign up. You can use sendgrid or any other transactional email provider.

    If you choose sendgrid, create an account on sendgrid.com, login and go to the dashboard. On the left panel, click on Email API, then on Integration Guide. Then, choose SMTP Relay and follow the steps. You will be prompted to create an API Key. Once you create the API Key and verify the smtp relay, copy the API key in MI_SMTP_PASS in ./api/.env. Sendgrid's free plan allows to send up to 100 emails/day. If you need to send more than 100 emails/day, switch to a paid plan or choose another transactional email provider.

    COOKIE_SECRET and JWT_SECRET should at least be 32 characters long, but the longer the better. You can use an online password generator and set the password length to 32 or longer.

    If you want to enable push notifications in the mobile app, follow these instructions and set the following option:

    MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

    If you want to enable stripe payment gateway, sign up for a stripe account, fill the forms and save the publishable key and the secret key from stripe dashboard. Then, set the secret key in the following option in api/.env:

    MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY

    Don't expose stripe secret key on a website or embed it in a mobile application. It must be secret and stored securely in the server-side. Use stripe in test mode.

    Use only your test API keys for testing. This ensures that you don't accidentally modify your live customers or charges.

    To run the api, use the following commands:

    Shell
    cd ./api
    npm install
    npm run dev
  3. Add backend/.env file:
    VITE_PORT=3003
    VITE_NODE_ENV=development
    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_DEFAULT_LANGUAGE=en
    VITE_MI_PAGE_SIZE=30
    VITE_MI_PROPERTIES_PAGE_SIZE=15
    VITE_MI_BOOKINGS_PAGE_SIZE=20
    VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_TEMP_USERS=http://localhost/cdn/movinin/temp/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_TEMP_PROPERTIES=http://localhost/cdn/movinin/temp/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_CDN_TEMP_LOCATIONS=http://localhost/cdn/movinin/temp/locations
    VITE_MI_AGENCY_IMAGE_WIDTH=60
    VITE_MI_AGENCY_IMAGE_HEIGHT=30
    VITE_MI_PROPERTY_IMAGE_WIDTH=300
    VITE_MI_PROPERTY_IMAGE_HEIGHT=200
    VITE_MI_MINIMUM_AGE=21
    VITE_MI_PAGINATION_MODE=classic 

    To run the backend, use the following commands:

    Shell
    cd ./backend
    npm install
    npm start
  4. Add frontend/.env file:
    VITE_PORT=3004
    VITE_NODE_ENV=development
    VITE_MI_API_HOST=http://localhost:4004
    VITE_MI_RECAPTCHA_ENABLED=false
    VITE_MI_RECAPTCHA_SITE_KEY=GOOGLE_RECAPTCHA_SITE_KEY
    VITE_MI_DEFAULT_LANGUAGE=en
    VITE_MI_PAGE_SIZE=30
    VITE_MI_PROPERTIES_PAGE_SIZE=15
    VITE_MI_BOOKINGS_PAGE_SIZE=20
    VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_MI_CDN_USERS=http://localhost/cdn/movinin/users
    VITE_MI_CDN_PROPERTIES=http://localhost/cdn/movinin/properties
    VITE_MI_CDN_LOCATIONS=http://localhost/cdn/movinin/locations
    VITE_MI_AGENCY_IMAGE_WIDTH=60
    VITE_MI_AGENCY_IMAGE_HEIGHT=30
    VITE_MI_PROPERTY_IMAGE_WIDTH=300
    VITE_MI_PROPERTY_IMAGE_HEIGHT=200
    VITE_MI_MINIMUM_AGE=21
    VITE_MI_PAGINATION_MODE=classic
    VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_MI_STRIPE_CURRENCY_CODE=USD
    VITE_MI_CURRENCY=$
    VITE_MI_SET_LANGUAGE_FROM_IP=false
    VITE_MI_GOOGLE_ANALYTICS_ENABLED=false
    VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX
    VITE_MI_FB_APP_ID=XXXXXXXXXX
    VITE_MI_APPLE_ID=XXXXXXXXXX
    VITE_MI_GG_APP_ID=XXXXXXXXXX

    If you want to enable stripe payment gateway, set stripe publishable key in VITE_MI_STRIPE_PUBLISHABLE_KEY. You can retrieve it from stripe dashboard.

    VITE_MI_STRIPE_CURRENCY_CODE is the three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR". Required for Stripe payments. Must be a supported currency: https://docs.stripe.com/currencies

    reCAPTCHA is by default disabled. If you want to enable it, you have to set VITE_MI_RECAPTCHA_ENABLED to true and VITE_MI_RECAPTCHA_SITE_KEY to Google reCAPTCHA site key.

    To run the frontend, use the following commands:

    Shell
    cd ./frontend
    npm install
    npm run dev
  5. If you want to run the mobile app, you need to add mobile/.env:
    MI_API_HOST=https://movinin.io:4004
    MI_DEFAULT_LANGUAGE=en
    MI_PAGE_SIZE=20
    MI_PROPERTIES_PAGE_SIZE=8
    MI_BOOKINGS_PAGE_SIZE=8
    MI_CDN_USERS=https://movinin.io/cdn/movinin/users
    MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
    MI_AGENCY_IMAGE_WIDTH=60
    MI_AGENCY_IMAGE_HEIGHT=30
    MI_PROPERTY_IMAGE_WIDTH=300
    MI_PROPERTY_IMAGE_HEIGHT=200
    MI_MINIMUM_AGE=21
    MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    MI_STRIPE_COUNTRY_CODE=US
    MI_STRIPE_CURRENCY_CODE=USD

    You need to configure the following options:

    MI_API_HOST=https://movinin.io:4004
    MI_CDN_USERS=https://movinin.io/cdn/movinin/users
    MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
    MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    MI_STRIPE_COUNTRY_CODE=US
    MI_STRIPE_CURRENCY_CODE=USD

    You need to replace https://movinin.io with an IP or hostname.

    If you want to enable stripe payment gateway, set stripe publishable key in MI_STRIPE_PUBLISHABLE_KEY. You can retrieve it from stripe dashboard. Use stripe in test mode.

  6. Configure http://localhost/cdn
    • On Windows, install IIS, create C:\inetpub\wwwroot\cdn\movinin folder and add full access permissions to the user who is running Movin' In API on movininC:\inetpub\wwwroot\cdn\movinin folder.
    • On Linux, install NGINX, create /var/www/cdn/movinin folder, add full access permissions to the user who is running Movin' In API on /var/www/cdn/movinin folder, and add cdn folder to NGINX by changing /etc/nginx/sites-available/default as follows:
    server {
        listen 80 default_server;
        server_name _;
        
        ...
    
        location /cdn {
          alias /var/www/cdn;
        }
    }
  7. Create an administrator user from http://localhost:3003/sign-up
  8. To run the mobile app, simply download Expo app on your device and run the following commands from ./mobile folder:
    Shell
    npm install
    npm run dev

You need to download the google-services.json file and place it in ./mobile root directory for push notifications.

You can find detailed instructions about running the mobile app here.

To change the currency, follow these instructions.

Run Mobile App

To run the mobile app, create ./mobile/.env file with the following options:

MI_API_HOST=https://movinin.io:4004
MI_DEFAULT_LANGUAGE=en
MI_PAGE_SIZE=20
MI_PROPERTIES_PAGE_SIZE=8
MI_BOOKINGS_PAGE_SIZE=8
MI_CDN_USERS=https://movinin.io/cdn/movinin/users
MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
MI_AGENCY_IMAGE_WIDTH=60
MI_AGENCY_IMAGE_HEIGHT=30
MI_PROPERTY_IMAGE_WIDTH=300
MI_PROPERTY_IMAGE_HEIGHT=200
MI_MINIMUM_AGE=21
MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
MI_STRIPE_COUNTRY_CODE=US
MI_STRIPE_CURRENCY_CODE=USD

You need to configure the following options:

MI_API_HOST=https://movinin.io:4004
MI_CDN_USERS=https://movinin.io/cdn/movinin/users
MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
MI_STRIPE_COUNTRY_CODE=US
MI_STRIPE_CURRENCY_CODE=USD

Replace https://movinin.io with an IP, hostname or FQDN.

If you want to enable stripe payment gateway, set stripe publishable key in MI_STRIPE_PUBLISHABLE_KEY. You can retrieve it from stripe dashboard. Use stripe in test mode.

MI_STRIPE_MERCHANT_IDENTIFIER is the merchant identifier you registered with Apple for use with Apple Pay.

MI_STRIPE_COUNTRY_CODE is the two-letter ISO 3166 code of the country of your business, e.g. "US". Required for Stripe payments.

MI_STRIPE_CURRENCY_CODE is the three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR". Required for Stripe payments. Must be a supported currency: https://docs.stripe.com/currencies

Install demo database by following these instructions.

Configure http://localhost/cdn

On Windows, install IIS and add full access permissions to the user who is running Movin' In API on C:\inetpub\wwwroot\cdn\movinin.

On Linux, install NGINX and update /etc/nginx/sites-enabled/default as follows:

server {
    listen 80 default_server;
    server_name _;
    
    ...

    location /cdn {
      alias /var/www/cdn;
    }
} 

Finally, add full access permissions to the user who is running Movin' In API on /var/www/cdn/movinin.

Configure ./api by following these instructions.

Run ./api with the following command:

Shell
cd ./api
npm run dev 

Run mobile app by simply downloading Expo app on your device and running the following commands from ./mobile folder:

Shell
cd ./mobile
npm install
npm start 

Open Expo app on your device and scan the QR code to run Movin' In mobile app.

Push Notifications

If you want to enable BookCars push notifications, Download google-services.json file and place it in ./mobile root directory for push notifications. Its path can be configured from ./mobile/app.json configuration file through googleServicesFile setting option. Don't forget to generate a new private key in the Firebase Console and upload the JSON file to Expo dashboard.

Unit Tests and Coverage

Below are the instructions to run unit tests and build coverage report.

Prerequisites

  1. Install git, Node.js, NGINX or IIS, MongoDB and mongosh.

  2. Configure MongoDB:

    Shell
    mongosh
  3. Create admin user:

    db = db.getSiblingDB('admin')
    db.createUser({ user: "admin" , pwd: "PASSWORD", roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

    Replace PASSWORD with a strong password.

  4. Secure MongoDB by changing mongod.conf as follows:

    net:
    	port: 27017
    	bindIp: 0.0.0.0
    
    security:
    	authorization: enabled
  5. Restart MongoDB service.

Instructions

  1. Clone Movin' In repo:
    Shell
    git clone https://github.com/aelassas/movinin.git
  1. Create api/.env file with the following content:
    NODE_ENV=development
    MI_PORT=4004
    MI_HTTPS=false
    MI_PRIVATE_KEY=/etc/ssl/movinin.key
    MI_CERTIFICATE=/etc/ssl/movinin.crt
    MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin
    MI_DB_SSL=false
    MI_DB_SSL_CERT=/etc/ssl/movinin.crt
    MI_DB_SSL_CA=/etc/ssl/movinin.ca.pem
    MI_DB_DEBUG=false
    MI_COOKIE_SECRET=COOKIE_SECRET
    MI_AUTH_COOKIE_DOMAIN=localhost
    MI_JWT_SECRET=JWT_SECRET
    MI_JWT_EXPIRE_AT=86400
    MI_TOKEN_EXPIRE_AT=86400
    MI_SMTP_HOST=in-v3.mailjet.com
    MI_SMTP_PORT=587
    MI_SMTP_USER=USER
    MI_SMTP_PASS=PASSWORD
    MI_SMTP_FROM=admin@movinin.io
    MI_CDN_USERS=/var/www/cdn/movinin/users
    MI_CDN_TEMP_USERS=/var/www/cdn/movinin/temp/users
    MI_CDN_PROPERTIES=/var/www/cdn/movinin/properties
    MI_CDN_TEMP_PROPERTIES=/var/www/cdn/movinin/temp/properties
    MI_DEFAULT_LANGUAGE=en
    MI_BACKEND_HOST=http://localhost:3003/
    MI_FRONTEND_HOST=http://localhost:3004/
    MI_MINIMUM_AGE=21
    MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY

On Windows, install IIS and update the following settings with these values:

MI_CDN_USERS=C:\inetpub\wwwroot\cdn\movinin\users
MI_CDN_TEMP_USERS=C:\inetpub\wwwroot\cdn\movinin\temp\users
MI_CDN_PROPERTIES=C:\inetpub\wwwroot\cdn\movinin\properties
MI_CDN_TEMP_PROPERTIES=C:\inetpub\wwwroot\cdn\movinin\temp\properties

Add full access permissions to the user who is running Movin' In API on C:\inetpub\wwwroot\cdn\movinin.

Then, set the following options:

MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin
MI_COOKIE_SECRET=COOKIE_SECRET
MI_JWT_SECRET=JWT_SECRET
MI_SMTP_HOST=in-v3.mailjet.com
MI_SMTP_PORT=587
MI_SMTP_USER=USER
MI_SMTP_PASS=PASSWORD
MI_SMTP_FROM=admin@movinin.io
MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY

Replace PASSWORD in MI_DB_URI with MongoDB password and JWT_SECRET with a secret token. Finally, set the SMTP options. SMTP options are necessary for sign up. You can use mailjet, sendgrid, brevo or any other transactional email provider.

Replace STRIPE_SECRET_KEY with your Stripe secret key in test mode. You can find it in Stripe Developers Dashboard.

COOKIE_SECRET and JWT_SECRET should at least be 32 characters long, but the longer the better. You can use an online password generator and set the password length to 32 or longer.

If you want to enable push notifications in the mobile app, follow these instructions and set the following option:

MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

Run Movin' In API unit tests:

Shell
cd ./api
npm install
npm test

Coverage

Once you run unit tests, a coverage report is automatically built in:

./api/coverage

You can also view the coverage report on codecov.

Logs

All API logs are written in ./api/logs/all.log.

API Error logs are also written in ./api/logs/error.log.

Using the Code

Image 50

This section describes the software architecture of Movin' In including the API, the frontend, the mobile app and the backend.

Movin' In API is a Node.js server application that exposes a RESTful API using Express which gives access to Movin' In MongoDB database.

Movin' In frontend is a React web application that is the main web interface for booking properties.

Movin' In backend is a React web application that lets administrators and agencies manage properties, bookings and customers.

Movin' In mobile app is a React Native application that is the main mobile app for booking properties.

A key design decision was made to use TypeScript instead of JavaScript due to its numerous advantages. TypeScript offers strong typing, tooling, and integration, resulting in high-quality, scalable, more readable and maintainable code that is easy to debug and test.

API

Movin' In API exposes all Movin' In functions needed for the backend, the frontend and the mobile app. The API follows the MVC design pattern. JWT is used for authentication. There are some functions that need authentication such as functions related to managing properties, bookings and customers, and others that do not need authentication such as retrieving locations and available properties for non authenticated users.

  • ./api/src/models/ folder contains MongoDB models.
  • ./api/src/routes/ folder contains Express routes.
  • ./api/src/controllers/ folder contains controllers.
  • ./api/src/middlewares/ folder contains middlewares.
  • ./api/src/config/env.config.ts contains the configuration and TypeScript type definitions.
  • ./api/src/lang/ folder contains localization.
  • ./api/src/app.ts is the main server where routes are loaded.
  • ./api/index.ts is the main entry point of Movin' In API.

index.ts

index.ts is the main entry point of Movin' In API:

TypeScript
import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import http from 'node:http'
import https from 'node:https'
import app from './app'
import * as DatabaseHelper from './common/DatabaseHelper'
import * as env from './config/env.config'

if (await DatabaseHelper.Connect(env.DB_DEBUG)) {
    let server: http.Server | https.Server

    if (env.HTTPS) {
        https.globalAgent.maxSockets = Number.POSITIVE_INFINITY
        const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8')
        const certificate = await fs.readFile(env.CERTIFICATE, 'utf8')
        const credentials = { key: privateKey, cert: certificate }
        server = https.createServer(credentials, app)

        server.listen(env.PORT, () => {
            console.log('HTTPS server is running on Port', env.PORT)
        })
    } else {
        server = app.listen(env.PORT, () => {
            console.log('HTTP server is running on Port', env.PORT)
        })
    }

    const close = () => {
        console.log('\nGracefully stopping...')
        server.close(async () => {
            console.log(`HTTP${env.HTTPS ? 'S' : ''} server closed`)
            await DatabaseHelper.Close(true)
            console.log('MongoDB connection closed')
            process.exit(0)
        })
    }

    ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close))
}

This is a TypeScript file that starts a server using Node.js and Express. It imports several modules including dotenv, process, fs, http, https, mongoose, and app. It then checks if the HTTPS environment variable is set to true, and if so, creates an HTTPS server using the https module and the provided private key and certificate. Otherwise, it creates an HTTP server using the http module. The server listens on the port specified in the PORT environment variable.

The close function is defined to gracefully stop the server when a termination signal is received. It closes the server and the MongoDB connection, and then exits the process with a status code of 0. Finally, it registers the close function to be called when the process receives a SIGINT, SIGTERM, or SIGQUIT signal.

app.ts

app.ts is in the main server:

TypeScript
import express, { Express } from 'express'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import cookieParser from 'cookie-parser'
import strings from './config/app.config'
import * as env from './config/env.config'
import cors from './middlewares/cors'
import allowedMethods from './middlewares/allowedMethods'
import agencyRoutes from './routes/agencyRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import propertyRoutes from './routes/propertyRoutes'
import userRoutes from './routes/userRoutes'
import * as Helper from './common/Helper'

const app: Express = express()

app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())

app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))

app.use(cors())
app.options('*', cors())
app.use(cookieParser(env.COOKIE_SECRET))
app.use(allowedMethods)

app.use('/', agencyRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', propertyRoutes)
app.use('/', userRoutes)

strings.setLanguage(env.DEFAULT_LANGUAGE)

Helper.mkdir(env.CDN_USERS)
Helper.mkdir(env.CDN_TEMP_USERS)
Helper.mkdir(env.CDN_PROPERTIES)
Helper.mkdir(env.CDN_TEMP_PROPERTIES)

export default app

First of all, we retrieve MongoDB connection string, then we establish a connection with Movin' In MongoDB database. Then we create an Express app and load middlewares such as cors, compression, helmet, and nocache. We set up various security measures using the helmet middleware library. we also import various route files for different parts of the application such as supplierRoutes, bookingRoutes, locationRoutes, notificationRoutes, propertyRoutes, and userRoutes. Finally, we load Express routes and export app.

Routes

There are six routes in Movin' In API. Each route has its own controller following the MVC design pattern and SOLID principles. Below are the main routes:

  • userRoutes: Provides REST functions related to users
  • agencyRoutes: Provides REST functions related to agencies
  • locationRoutes: Provides REST functions related to locations
  • propertyRoutes: Provides REST functions related to properties
  • bookingRoutes: Provides REST functions related to bookings
  • notificationRoutes: Provides REST functions related to notifications

Below is Property type:

TypeScript
import { Document, Types } from 'mongoose'

export interface Property extends Document {
    name: string
    type: movininTypes.PropertyType
    agency: Types.ObjectId
    description: string
    image: string
    images?: string[]
    bedrooms: number
    bathrooms: number
    kitchens?: number
    parkingSpaces?: number,
    size?: number
    petsAllowed: boolean
    furnished: boolean
    minimumAge: number
    location: Types.ObjectId
    address?: string
    price: number
    hidden?: boolean
    cancellation?: number
    aircon?: boolean
    available?: boolean
    rentalTerm: movininTypes.RentalTerm
} 

A property is composed of:

  • A name
  • A type (Apartment, Commercial, Farm, House, Industrial, Plot, Townhouse)
  • A reference to the agency who created it
  • A description
  • A main image
  • Additional images
  • Number of bedrooms
  • Number of bathrooms
  • Number of kitchens
  • Number of parking spaces
  • Size
  • Minimum age for rental
  • A location
  • An address (optional)
  • A price
  • A rental term (Monthly, Weekly, Daily, Yearly)
  • Cancellation price (set it to 0 to be included for free, leave it empty if you don't want to include it, or set the price for cancellation)
  • A flag that indicates whether pets are allowed or not
  • A flag that indicates whether the property is furnished or not
  • A flag that indicates whether the property is hidden or not
  • A flag that indicates whether aircon is available or not
  • A flag that indicates whether the property is available for rental or not

Below is create controller function:

TypeScript
export const create = async (req: Request, res: Response) => {
  const body: movininTypes.CreatePropertyPayload = req.body

  try {
    if (!body.image) {
      console.error(`[property.create] ${strings.PROPERTY_IMAGE_REQUIRED} ${body}`)
      return res.status(400).send(strings.PROPERTY_IMAGE_REQUIRED)
    }

    const {
      name,
      type,
      agency,
      description,
      image: imageFile,
      images,
      bedrooms,
      bathrooms,
      kitchens,
      parkingSpaces,
      size,
      petsAllowed,
      furnished,
      minimumAge,
      location,
      address,
      price,
      hidden,
      cancellation,
      aircon,
      rentalTerm
    } = body

    const _property = {
      name,
      type,
      agency,
      description,
      bedrooms,
      bathrooms,
      kitchens,
      parkingSpaces,
      size,
      petsAllowed,
      furnished,
      minimumAge,
      location,
      address,
      price,
      hidden,
      cancellation,
      aircon,
      rentalTerm
    }

    const property = new Property(_property)
    await property.save()

    // image
    if (!await Helper.exists(env.CDN_PROPERTIES)) {
      await fs.mkdir(env.CDN_PROPERTIES, { recursive: true })
    }

    const _image = path.join(env.CDN_TEMP_PROPERTIES, imageFile)
    if (await Helper.exists(_image)) {
      const filename = `${property._id}_${Date.now()}${path.extname(imageFile)}`
      const newPath = path.join(env.CDN_PROPERTIES, filename)

      await fs.rename(_image, newPath)
      property.image = filename
    } else {
      await Property.deleteOne({ _id: property._id })
      const err = 'Image file not found'
      console.error(strings.ERROR, err)
      return res.status(400).send(strings.ERROR + err)
    }

    // images
    property.images = []
    if (images) {
      for (let i = 0; i < images.length; i++) {
        const imageFile = images[i]
        const _image = path.join(env.CDN_TEMP_PROPERTIES, imageFile)

        if (await Helper.exists(_image)) {
          const filename = 
           `${property._id}_${uuid()}_${Date.now()}_${i}${path.extname(imageFile)}`
          const newPath = path.join(env.CDN_PROPERTIES, filename)

          await fs.rename(_image, newPath)
          property.images.push(filename)
        } else {
          await Property.deleteOne({ _id: property._id })
          const err = 'Image file not found'
          console.error(strings.ERROR, err)
          return res.status(400).send(strings.ERROR + err)
        }
      }
    }

    await property.save()

    return res.json(property)
  } catch (err) {
    console.error(`[property.create] ${strings.DB_ERROR} ${body}`, err)
    return res.status(400).send(strings.ERROR + err)
  }
}

In this function, we create the property from HTTP request body and build the main image as well as additional images.

Below is Booking type:

TypeScript
import { Document, Types } from 'mongoose'

export interface Booking extends Document {
    agency: Types.ObjectId
    location: Types.ObjectId
    property: Types.ObjectId
    renter: Types.ObjectId
    from: Date
    to: Date
    status: movininTypes.BookingStatus
    cancellation?: boolean
    cancelRequest?: boolean
    price: number
}

A booking is composed of:

  • A reference to the agency related to the booking
  • A reference to the location related to the booking
  • A reference to the property related to the booking
  • A reference to the customer related to the booking
  • A rental start date
  • A rental end date
  • A price
  • A status (Void, Pending, Deposit, Paid, Reserved, Cancelled)
  • A flag that indicates whether cancellation is available or not
  • A flag that indicates whether a cancel request was made by the customer or not

Below is create controller function:

TypeScript
export const create = async (req: Request, res: Response) => {
  try {
    const body: movininTypes.Booking = req.body
    const booking = new Booking(body)

    await booking.save()
    return res.json(booking)
  } catch (err) {
    console.error(`[booking.create]  ${strings.DB_ERROR} ${req.body}`, err)
    return res.status(400).send(strings.DB_ERROR + err)
  }
}

In this function, we create a Booking from the body of the HTTP request.

Below is update controller function:

TypeScript
const notifyRenter = async (booking: env.Booking) => {
  const renter = await User.findById(booking.renter)
  if (!renter) {
    console.log(`Renter ${booking.renter} not found`)
    return
  }
  if (renter.language) {
    strings.setLanguage(renter.language)
  }

  const message = `${strings.BOOKING_UPDATED_NOTIFICATION_PART1} ${booking._id} 
                   ${strings.BOOKING_UPDATED_NOTIFICATION_PART2}`
  const notification = new Notification({
    user: renter._id,
    message,
    booking: booking._id,
  })
  await notification.save()

  let counter = await NotificationCounter.findOne({ user: renter._id })
  if (counter && typeof counter.count !== 'undefined') {
    counter.count++
    await counter.save()
  } else {
    counter = new NotificationCounter({ user: renter._id, count: 1 })
    await counter.save()
  }

  // mail
  const mailOptions = {
    from: env.SMTP_FROM,
    to: renter.email,
    subject: message,
    html: `<p>${strings.HELLO}${renter.fullName},<br><br>
    ${message}<br><br>
    ${Helper.joinURL(env.FRONTEND_HOST, `booking?b=${booking._id}`)}<br><br>
    ${strings.REGARDS}<br></p>`,
  }
  await MailHelper.sendMail(mailOptions)

  // push notification
  const pushNotification = await PushNotification.findOne({ user: renter._id })
  if (pushNotification) {
    const pushToken = pushNotification.token
    const expo = new Expo({ accessToken: env.EXPO_ACCESS_TOKEN })

    if (!Expo.isExpoPushToken(pushToken)) {
      console.log(`Push token ${pushToken} is not a valid Expo push token.`)
      return
    }

    const messages: ExpoPushMessage[] = [
      {
        to: pushToken,
        sound: 'default',
        body: message,
        data: {
          user: renter._id,
          notification: notification._id,
          booking: booking._id,
        },
      },
    ]

    // The Expo push notification service accepts batches of notifications so
    // that you don't need to send 1000 requests to send 1000 notifications. We
    // recommend you batch your notifications to reduce the number of requests
    // and to compress them (notifications with similar content will get
    // compressed).
    const chunks = expo.chunkPushNotifications(messages)
    const tickets = [];

    (async () => {
      // Send the chunks to the Expo push notification service. There are
      // different strategies you could use. A simple one is to send one chunk at a
      // time, which nicely spreads the load out over time:
      for (const chunk of chunks) {
        try {
          const ticketChunk = await expo.sendPushNotificationsAsync(chunk)

          tickets.push(...ticketChunk)
          // NOTE: If a ticket contains an error code in ticket.details.error, you
          // must handle it appropriately. The error codes are listed in the Expo
          // documentation:
          // https://docs.expo.io/push-notifications/sending-notifications/#individual-errors
        } catch (error) {
          console.error(error)
        }
      }
    })()
  }
}

export const update = async (req: Request, res: Response) => {
  try {
    const body: movininTypes.Booking = req.body
    const booking = await Booking.findById(body._id)

    if (booking) {
      const {
        agency,
        location,
        property,
        renter,
        from,
        to,
        status,
        cancellation,
        price,
      } = req.body

      const previousStatus = booking.status

      booking.agency = agency
      booking.location = location
      booking.property = property
      booking.renter = renter
      booking.from = from
      booking.to = to
      booking.status = status
      booking.cancellation = cancellation
      booking.price = price

      await booking.save()

      if (previousStatus !== status) {
        // notify renter
        await notifyRenter(booking)
      }

      return res.sendStatus(200)
    } else {
      console.error('[booking.update] Booking not found:', req.body._id)
      return res.sendStatus(204)
    }
  } catch (err) {
    console.error(`[booking.update]  ${strings.DB_ERROR} ${req.body}`, err)
    return res.status(400).send(strings.DB_ERROR + err)
  }
}

In this function, we update the Booking from the body of the HTTP request and notify the customer by email and push notifications.

We are not going to explain each route one by one. We'll take, for example, locationRoutes and see how it was made:

TypeScript
import express from 'express'
import routeNames from '../config/locationRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as locationController from '../controllers/locationController'

const routes = express.Router()

routes
.route(routeNames.validate)
.post(authJwt.verifyToken, locationController.validate)

routes
.route(routeNames.create)
.post(authJwt.verifyToken, locationController.create)

routes
.route(routeNames.update)
.put(authJwt.verifyToken, locationController.update)

routes
.route(routeNames.delete)
.delete(authJwt.verifyToken, locationController.deleteLocation)

routes
.route(routeNames.getLocation)
.get(locationController.getLocation)

routes
.route(routeNames.getLocations)
.get(locationController.getLocations)

routes
.route(routeNames.checkLocation)
.get(authJwt.verifyToken, locationController.checkLocation)

export default routes

First of all, we create an Express Router. Then, we create the routes using their name, method, middlewares and controllers.

routeNames contains locationRoutes route names:

TypeScript
export default {
    validate: '/api/validate-location',
    create: '/api/create-location',
    update: '/api/update-location/:id',
    delete: '/api/delete-location/:id',
    getLocation: '/api/location/:id/:language',
    getLocations: '/api/locations/:page/:size/:language',
    checkLocation: '/api/check-location/:id',
}

locationController contains the main business logic regarding locations. We are not going to see all the source code of the controller since it's quite large but we'll take create and getLocations controller functions for example.

Below is Location model:

TypeScript
import { Schema, model } from 'mongoose'
import * as env from '../config/env.config'

const locationSchema = new Schema<env.Location>(
  {
    values: {
      type: [Schema.Types.ObjectId],
      ref: 'LocationValue',
      validate: (value: any): boolean => Array.isArray(value) && value.length > 1,
    },
  },
  {
    timestamps: true,
    strict: true,
    collection: 'Location',
  },
)

const locationModel = model<env.Location>('Location', locationSchema)

locationModel.on('index', (err) => {
  if (err) {
    console.error('Location index error: %s', err)
  } else {
    console.info('Location indexing complete')
  }
})

export default locationModel

Below is env.Location TypeScript type:

TypeScript
export interface Location extends Document {
    values: Types.ObjectId[]
    name?: string
}

A Location has multiple values. One per language. By default, English and French languages are supported.

Below is LocationValue model:

TypeScript
import { Schema, model } from 'mongoose'
import * as env from '../config/env.config'

const locationValueSchema = new Schema<env.LocationValue>(
  {
    language: {
      type: String,
      required: [true, "can't be blank"],
      index: true,
      trim: true,
      lowercase: true,
      minLength: 2,
      maxLength: 2,
    },
    value: {
      type: String,
      required: [true, "can't be blank"],
      index: true,
      trim: true,
    },
  },
  {
    timestamps: true,
    strict: true,
    collection: 'LocationValue',
  },
)

const locationValueModel = model<env.LocationValue>('LocationValue', locationValueSchema)

locationValueModel.on('index', (err) => {
  if (err) {
    console.error('LocationValue index error: %s', err)
  } else {
    console.info('LocationValue indexing complete')
  }
})

export default locationValueModel

Below is env.LocationValue TypeScript type:

TypeScript
export interface LocationValue extends Document {
    language: string
    value: string
}

A LocationValue has a language code (ISO 639-1) and a string value.

Below is create controller function:

TypeScript
export const create = async (req: Request, res: Response) => {
  const body: movininTypes.LocationName[] = req.body
  const names = body

  try {
    const values = []
    for (let i = 0; i < names.length; i++) {
      const name = names[i]
      const locationValue = new LocationValue({
        language: name.language,
        value: name.name,
      })
      await locationValue.save()
      values.push(locationValue._id)
    }

    const location = new Location({ values })
    await location.save()
    return res.sendStatus(200)
  } catch (err) {
    console.error(`[location.create] ${strings.DB_ERROR} ${req.body}`, err)
    return res.status(400).send(strings.DB_ERROR + err)
  }
}

In this function, we retrieve the body of the request, we iterate through the values provided in the body (one value per language) and we create a LocationValue. Finally, we create the location depending on the created location values.

Below is getLocations controller function:

TypeScript
export const getLocations = async (req: Request, res: Response) => {
  try {
    const page = Number.parseInt(req.params.page)
    const size = Number.parseInt(req.params.size)
    const language = req.params.language
    const keyword = escapeStringRegexp(String(req.query.s || ''))
    const options = 'i'

    const locations = await Location.aggregate(
      [
        {
          $lookup: {
            from: 'LocationValue',
            let: { values: '$values' },
            pipeline: [
              {
                $match: {
                  $and: [
                    { $expr: { $in: ['$_id', '$$values'] } },
                    { $expr: { $eq: ['$language', language] } },
                    { $expr: { $regexMatch: { input: '$value', regex: keyword, options } } },
                  ],
                },
              },
            ],
            as: 'value',
          },
        },
        { $unwind: { path: '$value', preserveNullAndEmptyArrays: false } },
        { $addFields: { name: '$value.value' } },
        {
          $facet: {
            resultData: [{ $sort: { name: 1 } }, 
                         { $skip: (page - 1) * size }, { $limit: size }],
            pageInfo: [
              {
                $count: 'totalRecords',
              },
            ],
          },
        },
      ],
      { collation: { locale: env.DEFAULT_LANGUAGE, strength: 2 } },
    )

    return res.json(locations)
  } catch (err) {
    console.error(`[location.getLocations] ${strings.DB_ERROR} ${req.query.s}`, err)
    return res.status(400).send(strings.DB_ERROR + err)
  }
}

In this controller function, we retrieve locations from the database using aggregate MongoDB function and facet to implement pagination.

Below is another simple route, notificationRoutes:

TypeScript
import express from 'express'
import routeNames from '../config/notificationRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as notificationController from '../controllers/notificationController'

const routes = express.Router()

routes
.route(routeNames.notificationCounter)
.get(authJwt.verifyToken, notificationController.notificationCounter)

routes
.route(routeNames.notify)
.post(authJwt.verifyToken, notificationController.notify)

routes
.route(routeNames.getNotifications)
.get(authJwt.verifyToken, notificationController.getNotifications)

routes
.route(routeNames.markAsRead)
.post(authJwt.verifyToken, notificationController.markAsRead)

routes
.route(routeNames.markAsUnRead)
.post(authJwt.verifyToken, notificationController.markAsUnRead)

routes
.route(routeNames.delete)
.post(authJwt.verifyToken, notificationController.deleteNotifications)

export default routes

Below is the Notification model:

TypeScript
import { Schema, model } from 'mongoose'
import * as env from '../config/env.config'

const notificationSchema = new Schema<env.Notification>(
  {
    user: {
      type: Schema.Types.ObjectId,
      required: [true, "can't be blank"],
      ref: 'User',
      index: true,
    },
    message: {
      type: String,
      required: [true, "can't be blank"],
    },
    booking: {
      type: Schema.Types.ObjectId,
      ref: 'Booking',
    },
    isRead: {
      type: Boolean,
      default: false,
    },
  },
  {
    timestamps: true,
    strict: true,
    collection: 'Notification',
  },
)

const notificationModel = model<env.Notification>('Notification', notificationSchema)

notificationModel.on('index', (err) => {
  if (err) {
    console.error('Notification index error: %s', err)
  } else {
    console.info('Notification indexing complete')
  }
})

export default notificationModel 

Below is env.Notification TypeScript type:

TypeScript
export interface Notification extends Document {
    user: Types.ObjectId
    message: string
    booking: Types.ObjectId
    isRead?: boolean
}

A Notification is composed of a reference to a user, a message, a reference to a booking and isRead flag.

Below is getNotifications controller function:

TypeScript
export const getNotifications = async (req: Request, res: Response) => {
  const { userId: _userId, page: _page, size: _size } = req.params

  try {
    const userId = new mongoose.Types.ObjectId(_userId)
    const page = Number.parseInt(_page)
    const size = Number.parseInt(_size)

    const notifications = await Notification.aggregate([
      { $match: { user: userId } },
      {
        $facet: {
          resultData: [{ $sort: { createdAt: -1 } }, 
                       { $skip: (page - 1) * size }, { $limit: size }],
          pageInfo: [
            {
              $count: 'totalRecords',
            },
          ],
        },
      },
    ])

    return res.json(notifications)
  } catch (err) {
    console.error(`[notification.getNotifications] ${strings.DB_ERROR} ${_userId}`, err)
    return res.status(400).send(strings.DB_ERROR + err)
  }
}

In this simple controller function, we retrieve notifications using MongoDB aggregate function, page and size parameters.

Below is markAsRead controller function:

TypeScript
export const markAsRead = async (req: Request, res: Response) => {
  try {
    const body: { ids: string[] } = req.body
    const { ids: _ids } = body
    const ids = _ids.map((id) => new mongoose.Types.ObjectId(id))
    const { userId: _userId } = req.params
    const userId = new mongoose.Types.ObjectId(_userId)

    const bulk = Notification.collection.initializeOrderedBulkOp()
    const notifications = await Notification.find({
      _id: { $in: ids },
      isRead: false,
    })
    const length = notifications.length

    bulk.find({ _id: { $in: ids }, isRead: false }).update({ $set: { isRead: true } })
    const result = await bulk.execute()

    if (result.modifiedCount !== length) {
      console.error(`[notification.markAsRead] ${strings.DB_ERROR}`)
      return res.status(400).send(strings.DB_ERROR)
    }
    const counter = await NotificationCounter.findOne({ user: userId })
    if (!counter || typeof counter.count === 'undefined') {
      return res.sendStatus(204)
    }
    counter.count -= length
    await counter.save()

    return res.sendStatus(200)
  } catch (err) {
    console.error(`[notification.markAsRead] ${strings.DB_ERROR}`, err)
    return res.status(400).send(strings.DB_ERROR + err)
  }
}

In this controller function, we bulk update notifications and mark them as read.

Frontend

The frontend is a web application built with Node.js, React, MUI and TypeScript. From the frontend, the customer can search for available properties depending on location points and time, choose a property and proceed to checkout.

  • ./frontend/src/assets/ folder contains CSS and images.
  • ./frontend/src/pages/ folder contains React pages.
  • ./frontend/src/components/ folder contains React components.
  • ./frontend/src/services/ contains Movin' In API client services.
  • ./frontend/src/App.tsx is the main React App that contains routes.
  • ./frontend/src/index.tsx is the main entry point of the frontend.

TypeScript type definitions are defined in the package ./packages/movinin-types.

App.tsx is the main react App:

TypeScript
import React, { lazy, Suspense } from 'react'
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'

const SignIn = lazy(() => import('./pages/SignIn'))
const SignUp = lazy(() => import('./pages/SignUp'))
const Activate = lazy(() => import('./pages/Activate'))
const ForgotPassword = lazy(() => import('./pages/ForgotPassword'))
const ResetPassword = lazy(() => import('./pages/ResetPassword'))
const Home = lazy(() => import('./pages/Home'))
const Properties = lazy(() => import('./pages/Properties'))
const Property = lazy(() => import('./pages/Property'))
const Checkout = lazy(() => import('./pages/Checkout'))
const Bookings = lazy(() => import('./pages/Bookings'))
const Booking = lazy(() => import('./pages/Booking'))
const Settings = lazy(() => import('./pages/Settings'))
const Notifications = lazy(() => import('./pages/Notifications'))
const ToS = lazy(() => import('./pages/ToS'))
const About = lazy(() => import('./pages/About'))
const ChangePassword = lazy(() => import('./pages/ChangePassword'))
const Contact = lazy(() => import('./pages/Contact'))
const NoMatch = lazy(() => import('./pages/NoMatch'))

const App = () => (
  <Router>
    <div className="App">
      <Suspense fallback={<></>}>
        <Routes>
          <Route path="/sign-in" element={<SignIn />} />
          <Route path="/sign-up" element={<SignUp />} />
          <Route path="/activate" element={<Activate />} />
          <Route path="/forgot-password" element={<ForgotPassword />} />
          <Route path="/reset-password" element={<ResetPassword />} />
          <Route path="/" element={<Home />} />
          <Route path="/properties" element={<Properties />} />
          <Route path="/property" element={<Property />} />
          <Route path="/checkout" element={<Checkout />} />
          <Route path="/bookings" element={<Bookings />} />
          <Route path="/booking" element={<Booking />} />
          <Route path="/settings" element={<Settings />} />
          <Route path="/notifications" element={<Notifications />} />
          <Route path="/change-password" element={<ChangePassword />} />
          <Route path="/about" element={<About />} />
          <Route path="/tos" element={<ToS />} />
          <Route path="/contact" element={<Contact />} />

          <Route path="*" element={<NoMatch />} />
        </Routes>
      </Suspense>
    </div>
  </Router>
  )

export default App

We are using React lazy loading to load each route.

We are not going to cover each page of the frontend, but you can open the source code and see each one if you want.

Mobile App

Movin' In provides a native mobile app for Android and iOS. The mobile app is built with React Native, Expo and TypeScript. Like for the frontend, the mobile app allows the customer to search for available properties depending on location points and time, choose a property and proceed to checkout.

The customer receives push notifications if his booking is updated from the backend. Push notifications are built with Node.js, Expo Server SDK and Firebase.

  • ./mobile/assets/ folder contains images.
  • ./mobile/screens/ folder contains main React Native screens.
  • ./mobile/components/ folder contains React Native components.
  • ./mobile/services/ contains Movin' In API client services.
  • ./mobile/App.tsx is the main React Native App.

TypeScript type definitions are defined in:

  • ./mobile/types/index.d.ts
  • ./mobile/types/env.d.ts
  • ./mobile/miscellaneous/movininTypes.ts

./mobile/types/ is loaded in ./mobile/tsconfig.json as follows:

JavaScript
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "typeRoots": [
      "./types"
    ]
  }
}

App.tsx is the main React Native app:

TypeScript
import 'react-native-gesture-handler'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { RootSiblingParent } from 'react-native-root-siblings'
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'
import { StatusBar as ExpoStatusBar } from 'expo-status-bar'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { Provider } from 'react-native-paper'
import * as SplashScreen from 'expo-splash-screen'
import * as Notifications from 'expo-notifications'
import DrawerNavigator from './components/DrawerNavigator'
import * as Helper from './common/Helper'
import * as NotificationService from './services/NotificationService'
import * as UserService from './services/UserService'

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
})

// Prevent native splash screen from autohiding before App component declaration
SplashScreen.preventAutoHideAsync()
  .then((result) => console.log(`SplashScreen.preventAutoHideAsync() succeeded: ${result}`))
  .catch(console.warn) // it's good to explicitly catch and inspect any error

const App = () => {
  const [appIsReady, setAppIsReady] = useState(false)
  const responseListener = useRef<Notifications.Subscription>()
  const navigationRef = useRef<NavigationContainerRef<StackParams>>(null)

  useEffect(() => {
    const register = async () => {
      const loggedIn = await UserService.loggedIn()
      if (loggedIn) {
        const currentUser = await UserService.getCurrentUser()
        if (currentUser?._id) {
          await Helper.registerPushToken(currentUser._id)
        } else {
          Helper.error()
        }
      }
    }

    // Register push notifiations token
    register()

    // This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed)
    responseListener.current = Notifications.addNotificationResponseReceivedListener(async (response) => {
      try {
        if (navigationRef.current) {
          const { data } = response.notification.request.content

          if (data.booking) {
            if (data.user && data.notification) {
              await NotificationService.markAsRead(data.user, [data.notification])
            }
            navigationRef.current.navigate('Booking', { id: data.booking })
          } else {
            navigationRef.current.navigate('Notifications', {})
          }
        }
      } catch (err) {
        Helper.error(err, false)
      }
    })

    return () => {
      if (responseListener.current) {
        Notifications.removeNotificationSubscription(responseListener.current)
      }
    }
  }, [])

  setTimeout(() => {
    setAppIsReady(true)
  }, 500)

  const onReady = useCallback(async () => {
    if (appIsReady) {
      // This tells the splash screen to hide immediately! If we call this after
      // `setAppIsReady`, then we may see a blank screen while the app is
      // loading its initial state and rendering its first pixels. So instead,
      // we hide the splash screen once we know the root view has already
      // performed layout.
      await SplashScreen.hideAsync()
    }
  }, [appIsReady])

  if (!appIsReady) {
    return null
  }

  return (
    <SafeAreaProvider>
      <Provider>
        <RootSiblingParent>
          <NavigationContainer ref={navigationRef} onReady={onReady}>
            <ExpoStatusBar style="light" backgroundColor="rgba(0, 0, 0, .9)" />
            <DrawerNavigator />
          </NavigationContainer>
        </RootSiblingParent>
      </Provider>
    </SafeAreaProvider>
  )
}

export default App

We are not going to cover each screen of the mobile app, but you can open the source code and see each one if you want.

Backend

The backend is a web application built with Node.js, React, MUI and TypeScript. From the backend, administrators can create and manage agencies, properties, locations, customers and bookings. When new agencies are created from the backend, they will receive an email prompting them to create an account in order to access the backend and manage their properties and bookings.

  • ./backend/src/assets/ folder contains CSS and images.
  • ./backend/src/pages/ folder contains React pages.
  • ./backend/src/components/ folder contains React components.
  • ./backend/src/services/ contains Movin' In API client services.
  • ./backend/src/App.tsx is the main React App that contains routes.
  • ./backend/src/index.tsx is the main entry point of the backend.

TypeScript type definitions are defined in the package ./packages/movinin-types.

App.tsx of the backend is similar to App.tsx of the frontend.

We are not going to cover each page of the backend but you can open the source code and see each one if you want.

Points of Interest

I built a lot of useful React and React Native components in this project for manipulating images, text, dropdowns, autocomplete, radio buttons, and so on. You can use these components in your React or React Native projects if you want.

Building the mobile app with React Native and Expo is straightforward. Expo makes mobile development with React Native very simple.

Using the same language (TypeScript) for backend, frontend and mobile development is very convenient.

TypeScript is a very interesting language and has many advantages. By adding static typing to JavaScript, we can avoid many bugs and produce high quality, scalable, more readable and maintainable code that is easy to debug and test.

So, that's it! I hope you enjoyed reading this article.

History

License

This article, along with any associated source code and files, is licensed under The MIT License