saasforge / open-source-saas-boilerpate

Free SaaS boilerplate (Python/PostgreSQL/ReactJS/Webpack)
https://www.saasforge.dev
MIT License
817 stars 120 forks source link
boilerplate dashboard flask free-saas jwt python python3 reactjs saas-boilerpate

SaaS-Boilerpate

SaaS application boilerplate

This free boilerplate allows creating a working SaaS web application. Please file all the information on our website: https://www.saasforge.dev

Technologies

System requirements

Currently, the boilerplate works with Python 3.5.

Features

User authentication

Interface

Project structure

All features/functions are put into separate structures, often front-end and back-end side-by-side. The whole project is divided into the following parts:

Blueprints (back-end applications)

Based on Flask blueprint, it allows separating parts of an application that are significantly different each from another and requires a different level of authentication/authorization.

Modules

Modules are the code that can't be decoupled and should go together: front-end and back-end. They both belong to the same functionality and front-end often calls endpoints from the same module's back-end.

The API endpoints will be imported and registered in the dashboard blueprint automatically.

If back-end requires to initialize some extension using init_api() this function should be provided at the .py code and will be called automatically.

Auth

Contains the server-side API and ReactJS components.

Server-side part
  1. API, user registration/email confirmation logic.
  2. The authentication is implemented with using JWT.
Front-end part
  1. Component with routes working as a SPA (single page application)
  2. Responsive design
  3. Pages:
    • register
    • login
    • finish registration info page
    • confirmation page (with automatic redirection to the login page)
Screenshots

Registration page on a normal computer screen

Registration page on a normal computer screen

Registration page on a mobile screen

Registration page on a mobile screen

Confirmation page on a mobile screen

Confirmation page on a mobile screen

JWT

Is used for the token authentication. The component has the API server-side part and JS file with functions called automatically. In our implementation we use cookie-based token as for it's most safe method of using tokens at the moment.

Server-side API

It has just 4 functions:

Frond-end functions

This file is referred in the Auth jsx component and it's only purpose is to add the interceptor in all the following requests done by axios. You don't need to call it directly.

ComponentsDemo

Contains demo views for alerts.

ErrorPages

Currently, only one error (404, file not found) is currently handled. When a user enters a non-existing URL (for example, /app/blabla), the error will be shown:

404 error

Profile

API

Contains one endpoint /app/api/profile with 2 methods:

Profile view

Simple view to update the current user's username: Profile saved

Components (in alphabetical order)

Components are ReactJS piece of code, sometimes accompanied by CSS. Doesn't have any back-end part. Note. This is just description of components, all documentation can be found for each component in its folder.

Alert

Styled block with and icon and text. There are 4 types of alerts:

Alerts

Tip You can set up if you want an alert to disappear after a required number of seconds (see documentation for details).

Dropdown menu

Dropdown menu is used in the header of the dashboard. It's based on a simple JSON structure, for example this data

const topMenu = [
    {title: 'Profile', url: '/app/profile', component: lazy(() => import('@src/modules/profile/ProfileView'))},
    {title: 'Change password', url: '/app/password', component: lazy(() => import('@src/modules/password/ChangePasswordUI'))},
    {divider: true},
    {title: 'Logout', url: '/api/auth/logout', method: 'post', redirectUrl: '/auth/login'}
];

will generate the following menu:

Example top menu

Features

Left menu

Responsive, collapsible menu with any amount of items on each level. You create it just filling up the JSON data file:

    {
        groupTitle: 'Demo',
        items: [
            {
                title: 'Alerts', 
                icon: 'exclamation-triangle',
                color: 'yellow',
                url: '/app/demo/alerts',
                component: lazy(() => import('@src/modules/componentsDemo/AlertDemoView'))
            }
        ]
    }

Note. If you prefer you can import component separately and assign it excplicitly in the menu data structure.

Menu when expanded and collapsed

Menu when expanded and collapsed

Menu on the small screen

Menu on the small screen

Features

Maker brand

Just small piece of HTML containing link to our website (SaaSForge). You may remove it but you can't replace it wit your own brand.

Services

Email

Can be used for the sending transactional email. Example of using:

from src.shared.services.email import service as email_service

email_service.send_email(user.email, get_config_var('COMPANY_NAME') + ': Confirm your registration', 
                        text_body, 
                        html_body, 
                        get_config_var('COMPANY_NAME'),
                        get_config_var('MAIL_DEFAULT_SENDER'))

Utils

Some important utils are located in /src/shared/utils folder.

DB scaffolding functions

Contains functions to update database based on the current db_models. Automatically finds all models. Don't call this function directly, it can be called implicitly using the following script:

flask dbupdate

Extensions

Contains the instances of extensions to be used in different files. All of them are inited in the app factory.

Global functions

Contains several functions used globally. Some of them are also available for using in jinja.

get_config_var

Safely returns the corresponding app.config[var_name] if found, else None. Example of using in jinja HTML layout:

<title>{{get_config_var('COMPANY_NAME')}}</title>
flat_validation_errors(errors_object)

Takes an errors object (like {'error1': 'Text of error 1', 'error2': 'Error 2'}) and returns a single string containing all the error texts.

Server error handler

If some error (non-handled exception) occured it handles and returns the following page:

Server-side error page

User authentication wrappers

@loginrequired wrapper

To protect endpoints and make them accessible only if user is authenticated, use this wrapper. Currently, it wraps JWT wrapper, but you can easily change it to anything else. Example of using:

from src.shared.utils.user_auth_wrapper import login_required

@profile_api.route('/')
class retrieve_user_profile(Resource):
    @login_required
    def get(self):
        ...
get_current_user_id wrapper function

This function wraps JWT get_jwt_identity and returns the current user's id what can be easily used for getting any data associated with the current user:

from src.shared.utils.user_auth_wrapper import get_current_user_id

@profile_api.route('/')
class retrieve_user_profile(Resource):
    @login_required
    def get(self):
        current_user_id = get_current_user_id()
        current_user = db_user_service.get_user_by_id(current_user_id)
        ...

Database models

Contains the following tables:

When done any changes to the tables including adding new tables run the following command from the terminal:

flask dbupdate

Note. Make sure your environment variables has the following var: FLASK_APP=application else it wount work.

Note 2. Don't forget to add db_url variable with URL to your database to the environment variables.

Tip. When this command runs for the first time it creates all the tables as well as 2 roles: User and Admin.

Dev's features and tips

Conventions / tips

Common

def init_app(app):
    jwt.init_app(app)

Note. If you need to use some extension there, the same create a function init_app that will be called automatically.

Custom static files

Using in Python (in html files): For example, to specify path to a resource in some component:

<link rel="icon" type="image/x-icon" href="https://github.com/saasforge/open-source-saas-boilerpate/blob/master/{{ url_for("custom_static", 
    filename="shared/components/media_component/favicon.ico") }}" />

Services/functions

Config: how to securely configure your variables

  1. Add the variable in config.py directly and assign its value (only if it's not sensitive, like a flag to use SSL or TLS)
  2. Sensitive data put into the environment variables. It could be done in 2 ways:
    • Add them into venv/scripts/activate script
    • add them into .env file in the project folder.
  3. In the code always call get_config_var:
    from src.shared.utils.global_functions import get_config_var
    get_config_var('var_name')

    This function also can be safely used in jinja HTML files:

    <title>{{get_config_var('COMPANY_NAME')}}</title>

Making requests from the front-end code

Use axios(link) for making requests. For example (asynchronous way):

import axios from 'axios';
...
componentDidMount = async()=> {
    // Load user data
    try {
        let response = await axios.get('/app/api/profile');
        if (response.data.result){
            this.setState({ username: response.data.username});
        } else {
            this.setState({ status: 'error', message: response.data.error || 'Some error occured during this request... please try again.' });
        }
    } catch {
        this.setState({ status: 'error', message: 'Some error occured during this request... please try again.' });
    }
}

Installation

Prerequisites

Before start make sure you have installed Python 3 and Node.js. Please follow the official instructions. Also, you need to have a PostgreSQL database handy. If you don't want to install it you can use ElephantSQL service, they have a free plan: https://www.elephantsql.com/plans.html.

Steps to follow

  1. Download the full zip or pull code from the repository, find full instruction in Github documentation

  2. Add necessarily environment variables (you can add them into /venv/scripts/activate file or into .env in the root folder):

    • FLASK_APP=application
    • db_url='postgres://user:password@dbhost:port/database'
    • JWT_SECRET_KEY='your jwt secret key'
    • SECRET_KEY='your secret key'
    • MAIL_SERVER = 'mail server'
    • MAIL_PORT = Number (like 465)
    • MAIL_USE_SSL = Bool (True of False)
    • MAIL_USE_TLS = Bool (True of False)
    • MAIL_USERNAME = 'your email'
    • MAIL_PASSWORD = 'your password'
    • ADMIN_EMAIL = 'your admin email'
    • MAIL_DEFAULT_SENDER = 'the same as your email'

Note. At least 2 first variables MUST be set up (FLASK_APP and db_url) else the installation script wont' work.

JWT_SECRET_KEY and SECRET_KEY are just strings that not easy to guess, for example, it may be 'My Co0LServ_ice'.

Tip. If you are puzzled how and why .env is used please read this explanation on Stackoverflow

  1. Run the command (Windows):
    init

    (Mac):

./init.sh

For any problem happening during execution of this command please see the section Troubleshooting below.

:warning: Warning! This command will first drop ALL tables in your database. (You can comment this part if you wish, see /src/shared/utils/db_scaffold, line 25.)

  1. If everything is going fine you will see the following text in your terminal:
* Serving Flask app "application"
* Environment: production
  WARNING: Do not use the development server in a production environment.
  Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

If you prefer to go through the installation process manually, please refer steps from the according version of init file in the root folder.

Updating database

After you change something in your database models you should update the database itself. It can be done easily with the following command:

flask dbupdate

Note. If you added some code including changes done into your model, and then run flask dbupdate and see some weird errors like Error: no such command "dbupdate" it means you have a compilation error. Try to run your code and see if there any errors, fix them and try to update the database again. To make sure there are not compilation errors, run the following command:

flask run

Note 2. Another reason for this error can be that you didn't add the following environment variable:

FLASK_APP=application

:warning: Warning. If you work on the same codebase from different computers and update the same database you will experience Alembic migration conflicts. The error usually says

alembic.util.exc.CommandError: Target database is not up to date. 

or

alembic.util.exc.CommandError: Can't locate revision identified by 'somenumber' 

If you experience such errors:

  1. In the database remove the record from alembic_version table
  2. Remove any files from your computer under app/migrations.

Troubleshooting

Sometimes, if you use Mac OS you can experience some problems with installing the boilerplate.

You need to fix this problem because script won't execute beyond. To fix this problem run

brew install postgresql
pip uninstall psycopg2
pip install psycopg2

Running with Docker

  1. Add or override necessary environment variables using .env.
  2. Run with docker-compose:
    docker-compose up

Setting your own data

Company name

There are 2 places to set up your company name:

  1. Config.py - change COMPANY_NAME variable
  2. /src/shared/globalVars.js - change globalVars object

Author

This free SaaS app boilerplate was create by SaaS Forge Inc. https://www.saasforge.dev

License

MIT License Copyright (c) 2019 SaaS Forge Inc. https://www.saasforge.dev.

Feedback and support

If you experience any difficulties, or have any questions on using of this product, or find a bug please open an issue or drop us a line at info@saasforge.dev.

Disclaimer of Warranties

We provide this boilerplate as is, and we make no promises or guarantees about it.

Would you like to support us?

There are many ways to do it:

Our supportive sponsors:

Sergio Rovira @sgr0691

:heart: THANKS! :heart: