Home Linux Construct your individual SaaS on Linux with Vely

Construct your individual SaaS on Linux with Vely

0
Construct your individual SaaS on Linux with Vely

[ad_1]

Vely combines excessive efficiency and the low footprint of C with the convenience of use and improved security of languages like PHP. It is free and open supply software program, licensed below GPLv3 and LGPL 3 for libraries, so you’ll be able to even construct industrial software program with it.

Utilizing Vely for SaaS

You should utilize Vely to create a multitenant internet software that you could run on the Web as Software program-as-a-Service (SaaS). Every person has a totally separate information house from every other.

On this instance internet software, a person can join a pocket book service to create notes after which view and delete them. It demonstrates a number of expertise integrations in simply 310 traces of code throughout seven supply recordsdata. The applied sciences embrace:

  • MariaDB
  • Net browser
  • Apache
  • Unix sockets

The way it works

This is how the appliance works from a person’s perspective. A code walk-through follows the photographs.

The app permits a person to create a brand new login by specifying an e-mail deal with and password. You may type these any manner you want, similar to with CSS:

(Sergio Mijatovic, CC BY-SA 4.0)

Confirm the person’s e-mail:

(Sergio Mijatovic, CC BY-SA 4.0)

Every person logs in with their distinctive username and password:

(Sergio Mijatovic, CC BY-SA 4.0)

As soon as logged in, a person can add a notice:

(Sergio Mijatovic, CC BY-SA 4.0)

A person can get an inventory of notes:

(Sergio Mijatovic, CC BY-SA 4.0)

The app asks for affirmation earlier than deleting a notice:

(Sergio Mijatovic, CC BY-SA 4.0)

After the person confirms, the notice is deleted:

(Sergio Mijatovic, CC BY-SA 4.0)

Setup stipulations

Observe the set up directions on Vely.dev. It is a fast course of that makes use of commonplace packaging instruments, similar to DNF, APT, Pacman, or Zypper.

As a result of they’re a part of this instance, you need to set up Apache as an online server and MariaDB as a database.

After putting in Vely, activate syntax highlighting in Vim if you happen to’re utilizing it:

vv -m

Get the supply code

The supply code for this demonstration SaaS app is a part of the Vely set up. It is a good suggestion to create a separate supply code listing for every software (and you may identify it no matter you want). On this case, unpacking the supply code does that for you:

$ tar xvf $(vv -o)/examples/multitenant_SaaS.tar.gz
$ cd multitenant_SaaS

By default, the appliance is called multitenant_SaaS, however you’ll be able to name it something (if you happen to do this, change it all over the place).

Arrange the appliance

The very first step is to create an software. It is easy to do with Vely’s vf utility:

$ sudo vf -i -u $(whoami) multitenant_SaaS

This command creates a brand new software dwelling (/var/lib/vv/multitenant_SaaS) and performs the appliance setup for you. Largely, which means creating varied subdirectories within the dwelling folder and assigning privileges. On this case, solely the present person (the results of whoami) owns the directories, with 0700 privileges, which ensures that nobody else has entry to the recordsdata.

Arrange the database

Earlier than doing any coding, you want a spot to retailer the knowledge utilized by the appliance. First, create a MariaDB database known as db_multitenant_SaaS, owned by the person vely with password your_password. You may change any of those values, however keep in mind to alter them all over the place throughout this instance.

Logged in as root within the MySQL utility:

CREATE DATABASE IF NOT EXISTS db_multitenant_SaaS;
CREATE USER IF NOT EXISTS vely IDENTIFIED BY 'your_password';
GRANT CREATE,ALTER,DROP,SELECT,INSERT,DELETE,UPDATE ON db_multitenant_SaaS.* TO vely;

Then create database objects (tables and information and so forth) within the database:

USE db_multitenant_SaaS;
SOURCE setup.sql;
exit

Join Vely to a database

To let Vely know the place your database is and the way to log into it, create a database config file named db_multitenant_SaaS. (That is the identify utilized by the database statements within the supply code, so if you happen to change it, be sure you change it all over the place.)

Vely makes use of native MariaDB database connectivity, so you’ll be able to specify any choices {that a} given database permits you to:

$ echo '[client]
person=vely
password=your_password
database=db_multitenant_SaaS
protocol=TCP
host=127.0.0.1
port=3306'
> db_multitenant_SaaS

Construct the appliance

Use the vv utility to make the appliance, utilizing the --db choice to specify the MariaDB database and the database config file:

$ vv -q --db=mariadb:db_multitenant_SaaS

Programming and growth

Begin the appliance server

To begin the appliance server in your internet software, use the vf FastCGI course of supervisor. The appliance server makes use of a Unix socket to speak with the net server (making a reverse proxy):

$ vf -w 3 multitenant_SaaS

This begins three daemon processes to serve the incoming requests. You can even begin an adaptive server that will increase the variety of processes to serve extra requests and steadily cut back the variety of processes once they’re not wanted:

$ vf multitenant_SaaS

See vf for extra choices that can assist you obtain the very best efficiency.

When you could cease your software server, use the -m give up choice:

$ vf -m give up multitenant_SaaS

Arrange the net server

It is a internet software, so the appliance wants an online server. This instance makes use of Apache by the use of a Unix socket listener.

1. Arrange Apache

To configure Apache as a reverse proxy and join your software to it, you could allow FastCGI proxy assist, which typically means utilizing the proxy and proxy_fcgi modules.

For Fedora programs (or others, like Arch) allow the proxy and proxy_fcgi modules by including (or uncommenting) the suitable LoadModule directives within the /and many others/httpd/conf/httpd.conf Apache configuration file.

For Debian, Ubuntu, and related programs, allow the proxy and proxy_fcgi modules:

$ sudo a2enmod proxy
$ sudo a2enmod proxy_fcgi

For OpenSUSE, add these traces to the tip of /and many others/apache2/httpd.conf:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

2. Configure Apache

Now you need to add the proxy data to the Apache configuration file:

ProxyPass "/multitenant_SaaS" unix:///var/lib/vv/multitenant_SaaS/sock/sock|fcgi://localhost/multitenant_SaaS

The placement of your configuration might fluctuate, relying in your Linux distribution:

  • Fedora, CentOS, Mageia, and Arch: /and many others/httpd/conf/httpd.conf
  • Debian, Ubuntu, Mint: /and many others/apache2/apache2.conf
  • OpenSUSE: /and many others/apache2/httpd.conf

3. Restart

Lastly, restart Apache. On Fedora and related programs, in addition to Arch Linux:

$ sudo systemctl restart httpd

On Debian and Debian-based programs, in addition to OpenSUSE:

$ sudo systemctl restart apache2

Arrange native mail

This instance makes use of e-mail as part of its operate. In case your server can already ship e-mail, you’ll be able to skip this. In any other case, you need to use native mail (myuser@localhost) simply to check it out. To try this, set up Sendmail.

On Fedora and related:

$ sudo dnf set up sendmail
$ sudo systemctl begin sendmail

On Debian programs (like Ubuntu):

$ sudo apt set up sendmail
$ sudo systemctl begin sendmail

When the appliance sends an e-mail to a neighborhood person, similar to OS_user@localhost, then you’ll be able to confirm that the e-mail was despatched by /var/mail/ (the “mail spool”).

Entry the appliance server from the browser

Assuming you are working the appliance regionally, use http://127.0.0.1/multitenant_SaaS?req=notes&motion=start to entry your software server out of your internet browser. If you happen to’re working this on a reside server on the Web, it’s possible you’ll want to regulate your firewall settings to permit HTTP visitors.

Supply code

This instance software accommodates seven supply recordsdata. You may assessment the code your self (keep in mind, it is simply 310 traces throughout these recordsdata), however this is an summary of every one.

SQL setup (setup.sql)

The 2 tables created are:

  • customers: Details about every person. Every person within the customers desk has its personal distinctive ID (userId column) together with different data similar to e-mail deal with and whether or not it is verified. There’s additionally a hashed password. An precise password is rarely saved in plain textual content (or in any other case); a one-way hash is used to test the password.
  • notes: Notes entered by the person. The notes desk accommodates the notes, every together with userId column that states which person owns them. The userId column’s worth matches the namesake column from customers desk. This manner, each notice clearly belongs to a single person.

The file contents:

CREATE TABLE IF NOT EXISTS notes (dateOf datetime, noteId BIGINT AUTO_INCREMENT PRIMARY KEY, userId BIGINT, notice VARCHAR(1000));
CREATE TABLE IF NOT EXISTS customers (userId BIGINT AUTO_INCREMENT PRIMARY KEY, e-mail VARCHAR(100), hashed_pwd VARCHAR(100), verified SMALLINT, verify_token VARCHAR(30), SESSION VARCHAR(100));
CREATE UNIQUE INDEX IF NOT EXISTS users1 ON customers (e-mail);

Run-time information (login.h)

To correctly show the Login, Signal Up, and Logout hyperlinks, you want some flags which can be obtainable wherever within the software. Additionally, the appliance makes use of cookies to keep up a session, so this must be obtainable wherever, for instance, to confirm that the session is legitimate. Each request despatched to the appliance is confirmed that manner. Solely requests that include verifiable cookies are permitted.

So to that impact, you have got a global_request_data kind reqdata (request information) and in it there’s sess_userId (ID of person) and sess_id (person’s present session ID). You even have quite self-explanatory flags that assist render pages:

#ifndef _VV_LOGIN
#outline _VV_LOGIN

typedef struct s_reqdata {
    bool displayed_logout; // true if Logout hyperlink displayed
    bool is_logged_in; // true if session verified logged-in
    char *sess_userId; // person ID of present session
    char *sess_id; // session ID
} reqdata;

void login_or_signup ();

#endif

Session checking and session information (_before.vely)

Vely has a notion of a before_request_handler. The code you write executes earlier than every other code that handles a request. To do that, all you want is to jot down this code in a file named _before.vely, and the remainder is routinely dealt with.

Something {that a} SaaS software does, similar to dealing with requests despatched to an software, have to be validated for safety. This manner, the appliance is aware of whether or not the caller has the permissions wanted to carry out an motion.

Checking for permission is completed right here in a before-request handler. That manner, no matter different code you have got dealing with a request, you have already got the session data.

To maintain session information (like session ID and person ID) obtainable wherever in your code, you employ global_request_data. It is only a generic pointer (void*) to reminiscence that any code that handles requests can entry. That is excellent for dealing with periods, as proven beneath:

#embrace "vely.h"
#embrace "login.h"

// _before() is a before-request-handler. It all the time executes earlier than
// every other code that handles a request. It is a good place for any
// type of request-wide setting or information initialization
void _before() {
    // Output HTTP header
    out-header default
    reqdata *rd; // that is world request information, see login.h
    // allocate reminiscence for world request information, might be routinely deallocated
    // on the finish of request
    new-mem rd dimension sizeof(reqdata)
    // initialize flags
    rd->displayed_logout = false;
    rd->is_logged_in = false;
    // set the information we created to be world request information, accessible
    // from any code that handles a request
    set-req information rd
    // test if session exists (primarily based on cookies from the shopper)
    // this executes earlier than every other request-handling code, making it
    // simpler to simply have session data prepared
    _check_session ();
}

Checking if the session is legitimate (_check_session.vely)

One of the vital essential duties in a multitenant SaaS software is to test (as quickly as doable) if the session is legitimate by checking whether or not a person is logged in. It is performed by getting the session ID and person ID cookies from the shopper (similar to an online browser) and checking these towards the database the place periods are saved:

#embrace "vely.h"
#embrace "login.h"

// Test if session is legitimate
void _check_session () {
    // Get world request information
    reqdata *rd;
    get-req information to rd
    // Get cookies from person browser
    get-cookie rd->sess_userId="sess_userId"
    get-cookie rd->sess_id="sess_id"
    if (rd->sess_id[0] != 0) {
        // Test if session ID is right for given person ID
        char *e-mail;
        run-query @db_multitenant_SaaS = "choose e-mail from customers the place userId='%s' and session='%s'" output e-mail : rd->sess_userId, rd->sess_id row-count outline rcount
            query-result e-mail to e-mail
        end-query
        if (rcount == 1) {
            // if right, set logged-in flag
            rd->is_logged_in = true;
            // if Logout hyperlink not show, then show it
            if (rd->displayed_logout == false) {
                @Hello <<p-out e-mail>>! <a href="https://opensource.com/?req=login&motion=logout">Logout</a><br/>
                rd->displayed_logout = true;
            }
        } else rd->is_logged_in = false;
    }
}

Signing up, Logging in, Logging out (login.vely)

The idea of any multitenant system is the flexibility for a person to enroll, log in, and sign off. Sometimes, signing up entails verifying the e-mail deal with; most of the time, the identical e-mail deal with is used as a username. That is the case right here.

There are a number of subrequests applied right here which can be essential to carry out the performance:

  • When Signing Up a brand new person, show the HTML type to gather the knowledge. The URL request signature for that is req=login&motion=newuser.
  • As a response to the Signal Up type, create a brand new person. The URL request signature is req=login&motion=createuser. The input-param sign obtains an e-mail and pwd POST type fields. The password worth is a one-way hash, and an e-mail verification token is created as a random five-digit quantity. These are inserted into the customers desk, creating a brand new person. A verification e-mail is distributed, and the person is prompted to learn the e-mail and enter the code.
  • Confirm the e-mail by coming into the verification code despatched to that e-mail. The URL request signature is req=login&motion=confirm.
  • Show a Login type for the person to log in. The URL request signature is req=login (as an example, motion is empty.)
  • Log in by verifying the e-mail deal with (username) and password. The URL request signature is req=login&motion=login.
  • Logout on the person’s request. The URL request signature is req=login&motion=logout.
  • Touchdown web page for the appliance. The URL request signature is req=login&motion=start.
  • If the person is presently logged in, go to the appliance’s touchdown web page.

See examples of those beneath:

#embrace "vely.h"
#embrace "login.h"

// Deal with session upkeep, login, logout, session verification
// for any multitenant Cloud software
void login () {
    // Get URL enter parameter "motion"
    input-param motion

    // Get world request information, we file session data in it, so it is useful
    reqdata *rd;
    get-req information to rd

    // If session is already established, the one purpose why we cannot proceed to
    // software dwelling is that if we're logging out
    if (rd->is_logged_in) {
        if (strcmp(motion, "logout")) {
            _show_home();
            exit-request
        }
    }

    // Utility display screen to get began. Present hyperlinks to login or signup and present
    // dwelling display screen applicable for this
    if (!strcmp (motion, "start")) {
        _show_home();
        exit-request

    // Begin creating new person. Ask for e-mail and password, then proceed to create person
    // when this way is submitted.
    } else if (!strcmp (motion, "newuser")) {
        @Create New Person<hr/>
        @<type motion="https://opensource.com/?req=login" technique="POST">
        @<enter identify="motion" kind="hidden" worth="createuser">
        @<enter identify="e-mail" kind="textual content" worth="" dimension="50" maxlength="50" required autofocus placeholder="E mail">
        @<enter identify="pwd" kind="password" worth="" dimension="50" maxlength="50" required placeholder="Password">
        @<enter kind="submit" worth="Signal Up">
        @</type>

    // Confirm code despatched to e-mail by person. The code should match, thus verifying e-mail deal with    
    } else if (!strcmp (motion, "confirm")) {
        input-param code
        input-param e-mail
        // Get confirm token primarily based on e-mail
        run-query @db_multitenant_SaaS = "choose verify_token from customers the place e-mail="%s"" output db_verify : e-mail
            query-result db_verify to outline db_verify
            // Examine token recorded in database with what person supplied
            if (!strcmp (code, db_verify)) {
                @Your e-mail has been verifed. Please <a href="https://opensource.com/?req=login">Login</a>.
                // If matches, replace person data to point it is verified
                run-query @db_multitenant_SaaS no-loop = "replace customers set verified=1 the place e-mail="%s"" : e-mail
                exit-request
            }
        end-query
        @Couldn't confirm the code. Please attempt <a href="https://opensource.com/?req=login">once more</a>.
        exit-request

    // Create person - this runs when person submits type with e-mail and password to create a person    
    } else if (!strcmp (motion, "createuser")) {
        input-param e-mail
        input-param pwd
        // create hashed (one-way) password
        hash-string pwd to outline hashed_pwd
        // generate random 5 digit string for confirm code
        random-string to outline confirm size 5 quantity
        // create person: insert e-mail, hashed password, verification token. Present confirm standing is 0, or not verified
        begin-transaction @db_multitenant_SaaS
        run-query @db_multitenant_SaaS no-loop = "insert into customers (e-mail, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" : e-mail, hashed_pwd, confirm affected-rows outline arows error outline err on-error-continue
        if (strcmp (err, "0") || arows != 1) {
            // if can't add person, it in all probability does not exist. Both manner, we won't proceed.
            login_or_signup();
            @Person with this e-mail already exists.
            rollback-transaction @db_multitenant_SaaS
        } else {
            // Create e-mail with verification code and e-mail it to person
            write-string outline msg
                @From: [email protected]
                @To: <<p-out e-mail>>
                @Topic: confirm your account
                @
                @Your verification code is: <<p-out confirm>>
            end-write-string
            exec-program "/usr/sbin/sendmail" args "-i", "-t" enter msg standing outline st
            if (st != 0) {
                @Couldn't ship e-mail to <<p-out e-mail>>, code is <<p-out confirm>>
                rollback-transaction @db_multitenant_SaaS
                exit-request
            }
            commit-transaction @db_multitenant_SaaS
            // Inform the person to go test e-mail and enter verification code
            @Please test your e-mail and enter verification code right here:
            @<type motion="https://opensource.com/?req=login" technique="POST">
            @<enter identify="motion" kind="hidden" worth="confirm" dimension="50" maxlength="50">
            @<enter identify="e-mail" kind="hidden" worth="<<p-out e-mail>>">
            @<enter identify="code" kind="textual content" worth="" dimension="50" maxlength="50" required autofocus placeholder="Verification code">
            @<button kind="submit">Confirm</button>
            @</type>
        }

    // This runs when logged-in person logs out.    
    } else if (!strcmp (motion, "logout")) {
        // Replace person desk to wipe out session, that means no such person is logged in
        if (rd->is_logged_in) {
            run-query @db_multitenant_SaaS = "replace customers set session='' the place userId='%s'" : rd->sess_userId no-loop affected-rows outline arows
            if (arows == 1) {
                rd->is_logged_in = false; // point out person not logged in
                @You have got been logged out.<hr/>
            }
        }
        _show_home();

    // Login: this runs when person enters person identify and password
    } else if (!strcmp (motion, "login")) {
        input-param pwd
        input-param e-mail
        // create one-way hash with the intention of evaluating with person desk - password is NEVER recorded
        hash-string pwd to outline hashed_pwd
        // create random 30-long string for session ID
        random-string to rd->sess_id size 30
        // Test if person identify and hashed password match
        run-query @db_multitenant_SaaS = "choose userId from customers the place e-mail="%s" and hashed_pwd='%s'" output sess_userId : e-mail, hashed_pwd
            query-result sess_userId to rd->sess_userId
            // If match, replace person desk with session ID
            run-query @db_multitenant_SaaS no-loop = "replace customers set session='%s' the place userId='%s'" : rd->sess_id, rd->sess_userId affected-rows outline arows
            if (arows != 1) {
                @Couldn't create a session. Please attempt once more. <<.login_or_signup();>> <hr/>
                exit-request
            }
            // Set person ID and session ID as cookies. Person's browser will return these to us with each request
            set-cookie "sess_userId" = rd->sess_userId
            set-cookie "sess_id" = rd->sess_id
            // Show dwelling, be certain that session is right first and set flags
            _check_session();
            _show_home();
            exit-request
        end-query
        @E mail or password should not right. <<.login_or_signup();>><hr/>

    // Login display screen, asks person to enter person identify and password    
    } else if (!strcmp (motion, "")) {
        login_or_signup();
        @Please Login:<hr/>
        @<type motion="https://opensource.com/?req=login" technique="POST">
        @<enter identify="motion" kind="hidden" worth="login" dimension="50" maxlength="50">
        @<enter identify="e-mail" kind="textual content" worth="" dimension="50" maxlength="50" required autofocus placeholder="E mail">
        @<enter identify="pwd" kind="password" worth="" dimension="50" maxlength="50" required placeholder="Password">
        @<button kind="submit">Go</button>
        @</type>
    }
}

// Show Login or Signal Up hyperlinks
void login_or_signup() {
        @<a href="https://opensource.com/?req=login">Login</a> & & <a href="https://opensource.com/?req=login&motion=newuser">Signal Up</a><hr/>
}

Common-purpose software (_show_home.vely)

With this tutorial, you’ll be able to create any multitenant SaaS software you need. The multitenant-processing module above (login.vely) calls the _show_home() operate, which may home any code of yours. This instance code reveals the Notes software, however it might be something. The _show_home() operate calls any code you would like and is a general-purpose multitenant software plug-in:

#embrace "vely.h"

void _show_home() {
    notes();
    exit-request
}

Notes software (notes.vely)

The appliance is ready to add, checklist, and delete any given notice:

#embrace "vely.h"
#embrace "login.h"

// Notes software in a multitenant Cloud
void notes () {
    // get world request information
    reqdata *rd;
    get-req information to rd
    // If session invalid, show Login or Signup
    if (!rd->is_logged_in) {
        login_or_signup();
    }
    // Greet the person
    @<h1>Welcome to Notes!</h1><hr/>
    // If not logged in, exit - this ensures safety verification of person's id
    if (!rd->is_logged_in) {
        exit-request
    }
    // Get URL parameter that tells Notes what to do
    input-param subreq
    // Show actions that Notes can do (add or checklist notes)
    @<a href="https://opensource.com/?req=notes&subreq=add">Add Notice</a> <a href="https://opensource.com/?req=notes&subreq=checklist">Checklist Notes</a><hr/>

    // Checklist all notes for this person
    if (!strcmp (subreq, "checklist")) {
        // choose notes for this person ONLY
        run-query @db_multitenant_SaaS = "choose dateOf, notice, noteId from notes the place userId='%s' order by dateOf desc" : rd->sess_userId output dateOf, notice, noteId
            query-result dateOf to outline dateOf
            query-result notice to outline notice
            query-result noteId to outline noteId
            // change new traces to <br/> with quick cached Regex
            match-regex "n" in notice replace-with "<br/>n" consequence outline with_breaks standing outline st cache
            if (st == 0) with_breaks = notice; // nothing was discovered/changed, simply use unique
            // Show a notice
            @Date: <<p-out dateOf>> (<a href="https://opensource.com/?req=notes&subreq=delete_note_ask&note_id=%3Cpercent3Cp-outpercent20noteIdpercent3Epercent3E">delete notice</a>)<br/>
            @Notice: <<p-out with_breaks>><br/>
            @<hr/>
        end-query
    }

    // Ask to delete a notice
    else if (!strcmp (subreq, "delete_note_ask")) {
        input-param note_id
        @Are you certain you wish to delete a notice? Use Again button to return, or <a href="https://opensource.com/?req=notes&subreq=delete_note&note_id=%3Cpercent3Cp-outpercent20note_idpercent3Epercent3E">delete notice now</a>.
    }

    // Delete a notice
    else if (!strcmp (subreq, "delete_note")) {
        input-param note_id
        // Delete notice
        run-query @db_multitenant_SaaS = "delete from notes the place noteId='%s' and userId='%s'" : note_id, rd->sess_userId affected-rows outline arows no-loop error outline errnote
        // Inform person of standing
        if (arows == 1) {
            @Notice deleted
        } else {
            @Couldn't delete notice (<<p-out errnote>>)
        }
    }

    // Add a notice
    else if (!strcmp (subreq, "add_note")) {
        // Get URL POST information from notice type
        input-param notice
        // Insert notice below this person's ID
        run-query @db_multitenant_SaaS = "insert into notes (dateOf, userId, notice) values (now(), '%s', '%s')" : rd->sess_userId, notice affected-rows outline arows no-loop error outline errnote
        // Inform person of standing
        if (arows == 1) {
            @Notice added
        } else {
            @Couldn't add notice (<<p-out errnote>>)
        }
    }

    // Show an HTML type to gather a notice, and ship it again right here (with subreq="add_note" URL param)
    else if (!strcmp (subreq, "add")) {
        @Add New Notice
        @<type motion="https://opensource.com/?req=notes" technique="POST">
        @<enter identify="subreq" kind="hidden" worth="add_note">
        @<textarea identify="notice" rows="5" cols="50" required autofocus placeholder="Enter Notice"></textarea>
        @<button kind="submit">Create</button>
        @</type>
    }
}

SaaS with C efficiency

Vely makes it doable to leverage the facility of C in your internet functions. A multitenant SaaS software is a chief instance of a use case that advantages from that. Check out the code examples, write some code, and provides Vely a attempt.

[ad_2]