Merge pull request #30603 from Drwhooooo/newlinux

Update and rename 20221107.2 ️ Build your own SaaS on Linux with…
This commit is contained in:
Xingyu.Wang 2023-12-08 19:21:23 +08:00 committed by GitHub
commit f3a61e1ec6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 652 additions and 652 deletions

View File

@ -1,652 +0,0 @@
[#]: subject: "Build your own SaaS on Linux with Vely"
[#]: via: "https://opensource.com/article/22/11/build-your-own-saas-vely"
[#]: author: "Sergio Mijatovic https://opensource.com/users/vely"
[#]: collector: "lkxed"
[#]: translator: "Drwhooooo"
[#]: reviewer: " "
[#]: publisher: " "
[#]: url: " "
Build your own SaaS on Linux with Vely
======
Vely makes it possible to leverage the power of C in your web applications.
[Vely][1] combines high performance and the low footprint of C with the ease of use and improved safety of languages like PHP. It's free and open source software, licensed under GPLv3 and LGPL 3 for libraries, so you can even build commercial software with it.
### Using Vely for SaaS
You can use Vely to create a multitenant web application that you can run on the Internet as Software-as-a-Service (SaaS). Each user has a completely separate data space from any other.
In this example web application, a user can sign up for a notebook service to create notes and then view and delete them. It demonstrates several technology integrations in just 310 lines of code across seven source files. The technologies include:
- MariaDB
- Web browser
- Apache
- Unix sockets
#### How it works
Here's how the application works from a user's perspective. A code walk-through follows the images.
The app allows a user to create a new login by specifying an email address and password. You can style these any way you like, such as with CSS:
![Create a user account][2]
Verify the user's email:
![Verify the user's email address][3]
Each user logs in with their unique username and password:
![The user logs in][4]
Once logged in, a user can add a note:
![The user can add a note][5]
A user can get a list of notes:
![User lists notes][6]
The app asks for confirmation before deleting a note:
![The app asks for confirmation before deleting a note][7]
After the user confirms, the note is deleted:
![After confirmation, the note is deleted][8]
#### Setup prerequisites
Follow the installation instructions on [Vely.dev][9]. It's a quick process that uses standard packaging tools, such as DNF, APT, Pacman, or Zypper.
Because they are part of this example, you must install Apache as a web server and MariaDB as a database.
After installing Vely, turn on syntax highlighting in Vim if you're using it:
```
vv -m
```
#### Get the source code
The source code for this demonstration SaaS app is part of the Vely installation. It's a good idea to create a separate source code directory for each application (and you can name it whatever you like). In this case, unpacking the source code does that for you:
```
$ tar xvf $(vv -o)/examples/multitenant_SaaS.tar.gz
$ cd multitenant_SaaS
```
By default, the application is named `multitenant_SaaS`, but you can call it anything (if you do that, change it everywhere).
### Set up the application
The very first step is to create an application. It's simple to do with Vely's `vf` utility:
```
$ sudo vf -i-u $(whoami) multitenant_SaaS
```
This command creates a new application home (`/var/lib/vv/multitenant_SaaS`) and performs the application setup for you. Mostly, that means creating various subdirectories in the home folder and assigning privileges. In this case, only the current user (the result of `whoami`) owns the directories, with 0700 privileges, which ensures that no one else has access to the files.
### Set up the database
Before doing any coding, you need a place to store the information used by the application. First, create a MariaDB database called `db_multitenant_SaaS`, owned by the user `vely` with password `your_password`. You can change any of these values, but remember to change them everywhere during this example.
Logged in as root in the MySQL utility:
```
CREATEDATABASEIFNOTEXISTS db_multitenant_SaaS;
CREATEUSERIFNOTEXISTS vely IDENTIFIEDBY'your_password';
GRANTCREATE,ALTER,DROP,SELECT,INSERT,DELETE,UPDATEON db_multitenant_SaaS.*TO vely;
```
Then create database objects (tables and records and so on) in the database:
```
USE db_multitenant_SaaS;
SOURCE setup.sql;
exit
```
### Connect Vely to a database
To let Vely know where your database is and how to log into it, create a database config file named `db_multitenant_SaaS`. (This is the name used by the database statements in the source code, so if you change it, make sure you change it everywhere.)
Vely uses native MariaDB database connectivity, so you can specify any options that a given database lets you:
```
$ echo'[client]
user=vely
password=your_password
database=db_multitenant_SaaS
protocol=TCP
host=127.0.0.1
port=3306'> db_multitenant_SaaS
```
### Build the application
Use the `vv` utility to make the application, using the `--db` option to specify the MariaDB database and the database config file:
```
$ vv -q--db=mariadb:db_multitenant_SaaS
```
### Start the application server
To start the application server for your web application, use the `vf` FastCGI process manager. The application server uses a Unix socket to communicate with the web server (creating a reverse proxy):
```
$ vf -w3 multitenant_SaaS
```
This starts three daemon processes to serve the incoming requests. You can also start an adaptive server that increases the number of processes to serve more requests and gradually reduce the number of processes when they're not needed:
```
$ vf multitenant_SaaS
```
See `vf` for more options to help you achieve the best performance.
When you need to stop your application server, use the `-m quit` option:
```
$ vf -m quit multitenant_SaaS
```
### Set up the web server
This is a web application, so the application needs a web server. This example uses Apache by way of a Unix socket listener.
#### 1. Set up Apache
To configure Apache as a reverse proxy and connect your application to it, you need to enable FastCGI proxy support, which generally means using the `proxy` and `proxy_fcgi` modules.
For Fedora systems (or others, like Arch) enable the `proxy` and `proxy_fcgi` modules by adding (or uncommenting) the appropriate **LoadModule** directives in the `/etc/httpd/conf/httpd.conf` Apache configuration file.
For Debian, Ubuntu, and similar systems, enable the `proxy` and `proxy_fcgi` modules:
```
$ sudo a2enmod proxy
$ sudo a2enmod proxy_fcgi
```
For OpenSUSE, add these lines to the end of `/etc/apache2/httpd.conf`:
```
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
```
#### 2. Configure Apache
Now you must add the proxy information to the Apache configuration file:
```
ProxyPass "/multitenant_SaaS" unix:///var/lib/vv/multitenant_SaaS/sock/sock|fcgi://localhost/multitenant_SaaS
```
The location of your configuration may vary, depending on your Linux distribution:
- Fedora, CentOS, Mageia, and Arch: `/etc/httpd/conf/httpd.conf`
- Debian, Ubuntu, Mint: `/etc/apache2/apache2.conf`
- OpenSUSE: `/etc/apache2/httpd.conf`
#### 3. Restart
Finally, restart Apache. On Fedora and similar systems, as well as Arch Linux:
```
$ sudo systemctl restart httpd
```
On Debian and Debian-based systems, as well as OpenSUSE:
```
$ sudo systemctl restart apache2
```
### Set up local mail
This example uses email as a part of its function. If your server can already send email, you can skip this. Otherwise, you can use local mail (`myuser@localhost`) just to test it out. To do that, install Sendmail.
On Fedora and similar:
```
$ sudo dnf installsendmail
$ sudo systemctl start sendmail
```
On Debian systems (like Ubuntu):
```
$ sudo apt installsendmail
$ sudo systemctl start sendmail
```
When the application sends an email to a local user, such as `OS_user@localhost`, then you can verify that the email was sent by looking at `/var/mail/` (the "mail spool").
### Access the application server from the browser
Assuming you're running the application locally, use `http://127.0.0.1/multitenant_SaaS?req=notes&action=begin` to access your application server from your web browser. If you're running this on a live server on the Internet, you may need to adjust your firewall settings to allow HTTP traffic.
### Source code
This example application contains seven source files. You can review the code yourself (remember, it's just 310 lines across these files), but here's an overview of each one.
#### SQL setup (setup.sql)
The two tables created are:
- **users**: Information about each user. Each user in the **users** table has its own unique ID (**userId** column) along with other information such as email address and whether it's verified. There's also a hashed password. An actual password is never stored in plain text (or otherwise); a one-way hash is used to check the password.
- **notes**: Notes entered by the user. The **notes** table contains the notes, each along with **userId** column that states which user owns them. The **userId** column's value matches the namesake column from **users** table. This way, every note clearly belongs to a single user.
The file contents:
```
CREATETABLEIFNOTEXISTS notes (dateOf datetime, noteId BIGINTAUTO_INCREMENTPRIMARYKEY, userId BIGINT, note VARCHAR(1000));
CREATETABLEIFNOTEXISTS users (userId BIGINTAUTO_INCREMENTPRIMARYKEY, email VARCHAR(100), hashed_pwd VARCHAR(100), verified SMALLINT, verify_token VARCHAR(30),SESSIONVARCHAR(100));
CREATEUNIQUEINDEXIFNOTEXISTS users1 ON users (email);
```
#### Run-time data (login.h)
To properly display the Login, Sign Up, and Logout links, you need some flags that are available anywhere in the application. Also, the application uses cookies to maintain a session, so this needs to be available anywhere, for example, to verify that the session is valid. Every request sent to the application is confirmed that way. Only requests that come with verifiable cookies are permitted.
So to that effect, you have a **global_request_data** type `reqdata` (request data) and in it there's `sess_userId` (ID of user) and `sess_id` (user's current session ID). You also have rather self-explanatory flags that help render pages:
```
#ifndef _VV_LOGIN
#define _VV_LOGIN
typedef struct s_reqdata {
    bool displayed_logout; // true if Logout link displayed
    bool is_logged_in; // true if session verified logged-in
    char *sess_userId; // user ID of current session
    char *sess_id; // session ID
} reqdata;
void login_or_signup ();
#endif
```
#### Session checking and session data (_before.vely)
Vely has a notion of a **before_request_handler**. The code you write executes before any other code that handles a request. To do this, all you need is to write this code in a file named `_before.vely`, and the rest is automatically handled.
Anything that a SaaS application does, such as handling requests sent to an application, must be validated for security. This way, the application knows whether the caller has the permissions needed to perform an action.
Checking for permission is done here in a before-request handler. That way, whatever other code you have handling a request, you already have the session information.
To keep session data (like session ID and user ID) available anywhere in your code, you use **global_request_data**. It's just a generic pointer (**void***) to memory that any code that handles requests can access. This is perfect for handling sessions, as shown below:
```
#include "vely.h"
#include "login.h"
// _before() is a before-request-handler. It always executes before
// any other code that handles a request. It's a good place for any
// kind of request-wide setting or data initialization
void _before() {
    // Output HTTP header
    out-header default
    reqdata *rd; // this is global request data, see login.h
    // allocate memory for global request data, will be automatically deallocated
    // at the end of request
    new-mem rd size sizeof(reqdata)
    // initialize flags
    rd->displayed_logout = false;
    rd->is_logged_in = false;
    // set the data we created to be global request data, accessible
    // from any code that handles a request
    set-req data rd
    // check if session exists (based on cookies from the client)
    // this executes before any other request-handling code, making it
    // easier to just have session information ready
    _check_session ();
}
```
#### Checking if the session is valid (_check_session.vely)
One of the most important tasks in a multitenant SaaS application is to check (as soon as possible) if the session is valid by checking whether a user is logged in. It's done by getting the session ID and user ID cookies from the client (such as a web browser) and checking these against the database where sessions are stored:
```
#include "vely.h"
#include "login.h"
// Check if session is valid
void _check_session () {
    // Get global request data
    reqdata *rd;
    get-req data to rd
    // Get cookies from user browser
    get-cookie rd->sess_userId="sess_userId"
    get-cookie rd->sess_id="sess_id"
    if (rd->sess_id[0] != 0) {
        // Check if session ID is correct for given user ID
        char *email;
        run-query @db_multitenant_SaaS = "select email from users where userId='%s' and session='%s'" output email : rd->sess_userId, rd->sess_id row-count define rcount
            query-result email to email
        end-query
        if (rcount == 1) {
            // if correct, set logged-in flag
            rd->is_logged_in = true;
            // if Logout link not display, then display it
            if (rd->displayed_logout == false) {
                @Hi <<p-out email>>! <a href="https://opensource.com/?req=login&action=logout">Logout</a><br/>
                rd->displayed_logout = true;
            }
        } else rd->is_logged_in = false;
    }
}
```
#### Signing up, Logging in, Logging out (login.vely)
The basis of any multitenant system is the ability for a user to sign up, log in, and log out. Typically, signing up involves verifying the email address; more often than not, the same email address is used as a username. That's the case here.
There are several subrequests implemented here that are necessary to perform the functionality:
- When Signing Up a new user, display the HTML form to collect the information. The URL request signature for this is `req=login&action=newuser`.
- As a response to the Sign Up form, create a new user. The URL request signature is `req=login&action=createuser`. The **input-param** signal obtains an **email** and **pwd** POST form fields. The password value is a one-way hash, and an email verification token is created as a random five-digit number. These are inserted into the **users** table, creating a new user. A verification email is sent, and the user is prompted to read the email and enter the code.
- Verify the email by entering the verification code sent to that email. The URL request signature is `req=login&action=verify`.
- Display a Login form for the user to log in. The URL request signature is `req=login` (for instance, `action` is empty.)
- Log in by verifying the email address (username) and password. The URL request signature is `req=login&action=login`.
- Logout at the user's request. The URL request signature is `req=login&action=logout`.
- Landing page for the application. The URL request signature is `req=login&action=begin`.
- If the user is currently logged in, go to the application's landing page.
See examples of these below:
```
#include "vely.h"
#include "login.h"
// Handle session maintenance, login, logout, session verification
// for any multitenant Cloud application
void login () {
    // Get URL input parameter "action"
    input-param action
    // Get global request data, we record session information in it, so it's handy
    reqdata *rd;
    get-req data to rd
    // If session is already established, the only reason why we won't proceed to
    // application home is if we're logging out
    if (rd->is_logged_in) {
        if (strcmp(action, "logout")) {
            _show_home();
            exit-request
        }
    }
    // Application screen to get started. Show links to login or signup and show
    // home screen appropriate for this
    if (!strcmp (action, "begin")) {
        _show_home();
        exit-request
    // Start creating new user. Ask for email and password, then proceed to create user
    // when this form is submitted.
    } else if (!strcmp (action, "newuser")) {
        @Create New User<hr/>
        @<form action="https://opensource.com/?req=login" method="POST">
        @<input name="action" type="hidden" value="createuser">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<input type="submit" value="Sign Up">
        @</form>
    // Verify code sent to email by user. The code must match, thus verifying email address    
    } else if (!strcmp (action, "verify")) {
        input-param code
        input-param email
        // Get verify token based on email
        run-query @db_multitenant_SaaS = "select verify_token from users where email='%s'" output db_verify : email
            query-result db_verify to define db_verify
            // Compare token recorded in database with what user provided
            if (!strcmp (code, db_verify)) {
                @Your email has been verifed. Please <a href="https://opensource.com/?req=login">Login</a>.
                // If matches, update user info to indicate it's verified
                run-query @db_multitenant_SaaS no-loop = "update users set verified=1 where email='%s'" : email
                exit-request
            }
        end-query
        @Could not verify the code. Please try <a href="https://opensource.com/?req=login">again</a>.
        exit-request
    // Create user - this runs when user submits form with email and password to create a user    
    } else if (!strcmp (action, "createuser")) {
        input-param email
        input-param pwd
        // create hashed (one-way) password
        hash-string pwd to define hashed_pwd
        // generate random 5 digit string for verify code
        random-string to define verify length 5 number
        // create user: insert email, hashed password, verification token. Current verify status is 0, or not verified
        begin-transaction @db_multitenant_SaaS
        run-query @db_multitenant_SaaS no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" : email, hashed_pwd, verify affected-rows define arows error define err on-error-continue
        if (strcmp (err, "0") || arows != 1) {
            // if cannot add user, it probably doesn't exist. Either way, we can't proceed.
            login_or_signup();
            @User with this email already exists.
            rollback-transaction @db_multitenant_SaaS
        } else {
            // Create email with verification code and email it to user
            write-string define msg
                @From: vely@vely.dev
                @To: <<p-out email>>
                @Subject: verify your account
                @
                @Your verification code is: <<p-out verify>>
            end-write-string
            exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status define st
            if (st != 0) {
                @Could not send email to <<p-out email>>, code is <<p-out verify>>
                rollback-transaction @db_multitenant_SaaS
                exit-request
            }
            commit-transaction @db_multitenant_SaaS
            // Inform the user to go check email and enter verification code
            @Please check your email and enter verification code here:
            @<form action="https://opensource.com/?req=login" method="POST">
            @<input name="action" type="hidden" value="verify" size="50" maxlength="50">
            @<input name="email" type="hidden" value="<<p-out email>>">
            @<input name="code" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Verification code">
            @<button type="submit">Verify</button>
            @</form>
        }
    // This runs when logged-in user logs out.    
    } else if (!strcmp (action, "logout")) {
        // Update user table to wipe out session, meaning no such user is logged in
        if (rd->is_logged_in) {
            run-query @db_multitenant_SaaS = "update users set session='' where userId='%s'" : rd->sess_userId no-loop affected-rows define arows
            if (arows == 1) {
                rd->is_logged_in = false; // indicate user not logged in
                @You have been logged out.<hr/>
            }
        }
        _show_home();
    // Login: this runs when user enters user name and password
    } else if (!strcmp (action, "login")) {
        input-param pwd
        input-param email
        // create one-way hash with the intention of comparing with user table - password is NEVER recorded
        hash-string pwd to define hashed_pwd
        // create random 30-long string for session ID
        random-string to rd->sess_id length 30
        // Check if user name and hashed password match
        run-query @db_multitenant_SaaS = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_userId : email, hashed_pwd
            query-result sess_userId to rd->sess_userId
            // If match, update user table with session ID
            run-query @db_multitenant_SaaS no-loop = "update users set session='%s' where userId='%s'" : rd->sess_id, rd->sess_userId affected-rows define arows
            if (arows != 1) {
                @Could not create a session. Please try again. <<.login_or_signup();>> <hr/>
                exit-request
            }
            // Set user ID and session ID as cookies. User's browser will return those to us with every request
            set-cookie "sess_userId" = rd->sess_userId
            set-cookie "sess_id" = rd->sess_id
            // Display home, make sure session is correct first and set flags
            _check_session();
            _show_home();
            exit-request
        end-query
        @Email or password are not correct. <<.login_or_signup();>><hr/>
    // Login screen, asks user to enter user name and password    
    } else if (!strcmp (action, "")) {
        login_or_signup();
        @Please Login:<hr/>
        @<form action="https://opensource.com/?req=login" method="POST">
        @<input name="action" type="hidden" value="login" size="50" maxlength="50">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<button type="submit">Go</button>
        @</form>
    }
}
// Display Login or Sign Up links
void login_or_signup() {
        @<a href="https://opensource.com/?req=login">Login</a> & & <a href="https://opensource.com/?req=login&action=newuser">Sign Up</a><hr/>
}
```
#### General-purpose application (_show_home.vely)
With this tutorial, you can create any multitenant SaaS application you want. The multitenant-processing module above (`login.vely`) calls the **_show_home()** function, which can house any code of yours. This example code shows the Notes application, but it could be anything. The **_show_home()** function calls any code you wish and is a general-purpose multitenant application plug-in:
```
#include "vely.h"
void _show_home() {
    notes();
    exit-request
}
```
#### Notes application (notes.vely)
The application is able to add, list, and delete any given note:
```
#include "vely.h"
#include "login.h"
// Notes application in a multitenant Cloud
void notes () {
    // get global request data
    reqdata *rd;
    get-req data to rd
    // If session invalid, display Login or Signup
    if (!rd->is_logged_in) {
        login_or_signup();
    }
    // Greet the user
    @<h1>Welcome to Notes!</h1><hr/>
    // If not logged in, exit - this ensures security verification of user's identity
    if (!rd->is_logged_in) {
        exit-request
    }
    // Get URL parameter that tells Notes what to do
    input-param subreq
    // Display actions that Notes can do (add or list notes)
    @<a href="https://opensource.com/?req=notes&subreq=add">Add Note</a> <a href="https://opensource.com/?req=notes&subreq=list">List Notes</a><hr/>
    // List all notes for this user
    if (!strcmp (subreq, "list")) {
        // select notes for this user ONLY
        run-query @db_multitenant_SaaS = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" : rd->sess_userId output dateOf, note, noteId
            query-result dateOf to define dateOf
            query-result note to define note
            query-result noteId to define noteId
            // change new lines to <br/> with fast cached Regex
            match-regex "\n" in note replace-with "<br/>\n" result define with_breaks status define st cache
            if (st == 0) with_breaks = note; // nothing was found/replaced, just use original
            // Display a note
            @Date: <<p-out dateOf>> (<a href="https://opensource.com/?req=notes&subreq=delete_note_ask&note_id=%3C%3Cp-out%20noteId%3E%3E">delete note</a>)<br/>
            @Note: <<p-out with_breaks>><br/>
            @<hr/>
        end-query
    }
    // Ask to delete a note
    else if (!strcmp (subreq, "delete_note_ask")) {
        input-param note_id
        @Are you sure you want to delete a note? Use Back button to go back, or <a href="https://opensource.com/?req=notes&subreq=delete_note&note_id=%3C%3Cp-out%20note_id%3E%3E">delete note now</a>.
    }
    // Delete a note
    else if (!strcmp (subreq, "delete_note")) {
        input-param note_id
        // Delete note
        run-query @db_multitenant_SaaS = "delete from notes where noteId='%s' and userId='%s'" : note_id, rd->sess_userId affected-rows define arows no-loop error define errnote
        // Inform user of status
        if (arows == 1) {
            @Note deleted
        } else {
            @Could not delete note (<<p-out errnote>>)
        }
    }
    // Add a note
    else if (!strcmp (subreq, "add_note")) {
        // Get URL POST data from note form
        input-param note
        // Insert note under this user's ID
        run-query @db_multitenant_SaaS = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : rd->sess_userId, note affected-rows define arows no-loop error define errnote
        // Inform user of status
        if (arows == 1) {
            @Note added
        } else {
            @Could not add note (<<p-out errnote>>)
        }
    }
    // Display an HTML form to collect a note, and send it back here (with subreq="add_note" URL param)
    else if (!strcmp (subreq, "add")) {
        @Add New Note
        @<form action="https://opensource.com/?req=notes" method="POST">
        @<input name="subreq" type="hidden" value="add_note">
        @<textarea name="note" rows="5" cols="50" required autofocus placeholder="Enter Note"></textarea>
        @<button type="submit">Create</button>
        @</form>
    }
}
```
### SaaS with C performance
Vely makes it possible to leverage the power of C in your web applications. A multitenant SaaS application is a prime example of a use case that benefits from that. Take a look at the code examples, write some code, and give Vely a try.
--------------------------------------------------------------------------------
via: https://opensource.com/article/22/11/build-your-own-saas-vely
作者:[Sergio Mijatovic][a]
选题:[lkxed][b]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://opensource.com/users/vely
[b]: https://github.com/lkxed
[1]: https://opensource.com/article/22/5/write-c-appplications-vely-linux
[2]: https://opensource.com/sites/default/files/2022-10/1createuser.png
[3]: https://opensource.com/sites/default/files/2022-10/2verifyemail.png
[4]: https://opensource.com/sites/default/files/2022-10/3login.png
[5]: https://opensource.com/sites/default/files/2022-10/4addnote.png
[6]: https://opensource.com/sites/default/files/2022-10/5listnotes.png
[7]: https://opensource.com/sites/default/files/2022-10/6confirmdelete.png
[8]: https://opensource.com/sites/default/files/2022-10/7notedeleted.png
[9]: https://vely.dev/

View File

@ -0,0 +1,652 @@
[#]: subject: "Build your own SaaS on Linux with Vely"
[#]: via: "https://opensource.com/article/22/11/build-your-own-saas-vely"
[#]: author: "Sergio Mijatovic https://opensource.com/users/vely"
[#]: collector: "lkxed"
[#]: translator: "Drwhooooo"
[#]: reviewer: " "
[#]: publisher: " "
[#]: url: " "
利用 Vely 在 Linux 构建你自己的 SaaS
======
Vely 语言使得 C 语言在你的 Web 应用程序中得到充分利用这件事成为可能。
[Vely][1]将 C 语言的高性能和低内存占用与 PHP 等语言的易用性和安全性相结合。作为免费的开源软件,它受 GPLv3 和 LGPL 3 的许可,所以你甚至可以用它来构建商业软件。
### 利用 Vely 构建 SaaS
您可以使用 Vely 创建一个多租户 web 应用程序,它可以作为软件即服务模式(Software-as-a-Service, SaaS)在互联网上运行。每个用户都有一个完全独立的数据空间。
在这个web应用程序示例中用户可以注册一个笔记本服务来创建笔记然后查看和删除它们。它在横跨7个源文件的310行代码中演示了几种技术集成。这些技术包括:
- MariaDB
- Web 浏览器
- Apache
- Unix 套接字
#### 运作原理
以下是从用户的角度来看应用程序是如何工作的。下图是代码演示。
该应用允许用户通过指定电子邮件地址和密码创建新的登录名。你可以用任何你喜欢的方式设置它们,例如运用 CSS
![创建一个用户账户][2]
验证用户的电子邮件:
![验证用户的电子邮件地址][3]
每个用户使用自己独有的用户名和密码登录:
![用户登录][4]
一旦登录,用户就可以添加笔记:
![用户可以添加笔记][5]
用户可以获取笔记列表:
![用户列举笔记][6]
删除笔记之前,应用会申请确认信息:
![删除笔记之前,应用会申请确认信息][7]
用户确认后,笔记被删除:
![用户确认后,笔记被删除][8]
#### 设置先决条件
遵照[Vely.dev][9]上的安装指示。这是使用标准工具包的快速流程,例如 DNFAPTPacman 或者 Zypper。
这缘于他们都是这个范例的一部分,你必须安装 Apache 作为 web 服务器,安装 MariaDB 作为数据库。
安装 Vely 后,使用 Vim 时,打开里面的“语法高亮显示”如果你在使用它的话:
```
vv -m
```
#### 获取源代码
这个演示 SaaS 应用程序的源代码是 Vely 安装的一部分。为每个应用程序创建一个单独的源代码目录不失为一个好主意(而且你可以按自己喜好命名)。在这种情况下,解包源代码会帮你完成这些工作:
```
$ tar xvf $(vv -o)/examples/multitenant_SaaS.tar.gz
$ cd multitenant_SaaS
```
默认情况下,该应用程序以`multitenant_SaaS`命名,但你可以将其命名为任何内容(如果这么做,其他每个地方你都需要改一遍)。
### 创建应用程序
第一步是创建一个应用程序。使用Vely的`vf`工具很简单:
```
$ sudo vf -i-u $(whoami) multitenant_SaaS
```
这个命令创建了一个新的应用程序主页(`/var/lib/vv/multitenant_SaaS`),并帮你执行应用程序设置。通常,这意味着在主文件夹中创建各种子目录并分配权限。在这种情况下,只有当前用户(`whoami`的结果)拥有目录,具有 0700 权限,这确保了其他人没有访问文件的权限。
### 创建数据库
在你键入任何代码之前,你需要一个能够存储该应用程序所用信息的空间。首先,创建一个名为`db_multitenant_SaaS`的 MariaDB 数据库,由用户名`vely`密码`your_password`的用户所有。你可以修改刚才提到的任何数据,但得记住,在这个示例里,你需要将包含这些内容的每个地方都得修改一遍。
在 MySQL 中以 root 身份登录:
```
CREATEDATABASEIFNOTEXISTS db_multitenant_SaaS;
CREATEUSERIFNOTEXISTS vely IDENTIFIEDBY'your_password';
GRANTCREATE,ALTER,DROP,SELECT,INSERT,DELETE,UPDATEON db_multitenant_SaaS.*TO vely;
```
然后在数据库内创建数据库对象(表,记录等等):
```
USE db_multitenant_SaaS;
SOURCE setup.sql;
exit
```
### 将 Vely 连接至数据库
为了让 Vely 知晓你数据库的位置以及如何登录进去,创建一个名为`db_multitenant_SaaS`的数据库配置文件。(该名称用于在源代码中的数据库声明,所以如果你改了它,确保在它存在的每个地方都改一遍。)
Vely 使用原生的 MariaDB 数据库连接,因此你可以指定给定的数据库所能允许的任何选项:
```
$ echo'[client]
user=vely
password=your_password
database=db_multitenant_SaaS
protocol=TCP
host=127.0.0.1
port=3306'> db_multitenant_SaaS
```
### 构建应用程序
使用`vv`工具构建应用程序,利用`--db`选项指定 MariaDB 数据库和数据库配置文件:
```
$ vv -q--db=mariadb:db_multitenant_SaaS
```
### 启用应用程序服务器
启用你 web 应用程序的服务器,需要使用`vf`的 FastCGI 过程管理器。应用程序服务器使用 Unix 套接字与 web 服务器(创建反向代理)通信:
```
$ vf -w3 multitenant_SaaS
```
这么做会启用三个守护进程来服务传入的请求。你也可以启用自适应服务器,它会增加进程的数量从而服务更多的请求,并在不需要他们时减少进程的数量:
```
$ vf multitenant_SaaS
```
查看`vf`中的更多帮助你获得最好性能的选项。
当你需要停用你的应用程序服务器,使用`-m quit`选项:
```
$ vf -m quit multitenant_SaaS
```
### 创建 web 服务器
这是一个 web 应用程序,那么应用程序就得需要一个 web 服务器。该示例通过一个 Unix 套接字监听器使用 Apache。
#### 1. 设置 Apache
将 Apache 配置为一个反向代理并将你的应用程序与之连接,你需要启用 FastCGI 代理支持,这通常表示使用`proxy` 和 `proxy_fcgi` 模块。
对于 Fedora 系统(或者其它的,比如 Arch来说通过添加或取消注释在 Apache 配置文件`/etc/httpd/conf/httpd.conf`中适当的 **加载模块** 指令,就可启用`proxy`和`proxy_fcgi`模块。
以下指令适用于 DebianUbuntu 以及类似的系统,启用`proxy`和`proxy_fcgi`模块:
```
$ sudo a2enmod proxy
$ sudo a2enmod proxy_fcgi
```
以下指令适用于 OpenSUSE将这几行添加在`/etc/apache2/httpd.conf`结尾处:
```
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
```
#### 2. 配置 Apache
现在你必须将代理信息添加在 Apache 的配置文件中:
```
ProxyPass "/multitenant_SaaS" unix:///var/lib/vv/multitenant_SaaS/sock/sock|fcgi://localhost/multitenant_SaaS
```
你们配置文件的位置可能会有所不同,这取决于你们不同的 Linux 发行版:
- FedoraCentOSMageia 和 Arch `/etc/httpd/conf/httpd.conf`
- DebianUbuntuMint `/etc/apache2/apache2.conf`
- OpenSUSE`/etc/apache2/httpd.conf`
#### 3. 重新启动
最后,重启 Apache。在 Fedora 和类似系统,还有 Arch Linux 是如下指令:
```
$ sudo systemctl restart httpd
```
在 Debian 和基于 Debian 的系统,还有 OpenSUSE 是如下指令:
```
$ sudo systemctl restart apache2
```
### 设置本地邮箱
这个示例将电子邮件作为其功能的一部分。如果你的服务器已经可以发送电子邮件了,你可以跳过这一条。此外,你可以使用本地邮箱(`myuser@localhost`)来测试它。要做到这一点,需安装 Sendmail。
在 Fedora 和类似系统中是如下指令:
```
$ sudo dnf installsendmail
$ sudo systemctl start sendmail
```
而在 Debian 系统(像 Ubuntu
```
$ sudo apt installsendmail
$ sudo systemctl start sendmail
```
当应用程序向本地用户发送电子邮件,比如说`OS_user@localhost`,你就可以通过查看`/var/mail/`处(所谓“邮件池”)来确认电子邮件是否被发送。
### 自浏览器访问应用程序服务器
假设你在本地运行该应用程序,可以自你的 web 服务器通过使用`http://127.0.0.1/multitenant_SaaS?req=notes&action=begin`域名访问你的应用程序服务器。如果你再互联网的在线服务器运行该程序,你可能就需要调整防火墙设置以允许 HTTP 通信。
### 源代码
该应用程序示例包含 7 个源文件。你可以自行回顾代码(记住,这些文件只有 310 行代码),不过这里有关于每个文件的概述。
#### SQL 设置 (setup.sql)
创建的两个表:
- **用户**:每个用户的信息。在 **users** 表中,每个用户都有自己唯一的 ID **userId** 列),以及其他信息,如电子邮件地址和该地址是否通过验证。还有一个哈希密码。实际的密码永远不会存储在纯文本(或其他形式)中;单向哈希用于检查密码。
- **笔记**:用户输入的笔记。**notes** 表包含了所有的笔记,每个笔记都有一个 **userId** 列,表示哪个用户拥有它们。**userId** 列的值与 **users** 表中的同名列匹配。这样,每个笔记显然都属于单个用户。
该文件内容如下:
```
CREATETABLEIFNOTEXISTS notes (dateOf datetime, noteId BIGINTAUTO_INCREMENTPRIMARYKEY, userId BIGINT, note VARCHAR(1000));
CREATETABLEIFNOTEXISTS users (userId BIGINTAUTO_INCREMENTPRIMARYKEY, email VARCHAR(100), hashed_pwd VARCHAR(100), verified SMALLINT, verify_token VARCHAR(30),SESSIONVARCHAR(100));
CREATEUNIQUEINDEXIFNOTEXISTS users1 ON users (email);
```
#### 运行时数据 (login.h)
为了正确地显示登录、注册和注销链接,你需要一些在应用程序中任何地方都可以使用的标志。此外,应用程序使用 cookie 来维护会话,因此它需要在任何地方都可用,例如,验证会话是否有效。发送到应用程序的每个请求都以这种方式进行确认。只有带有可验证 cookie 的请求是允许的。
所以要做到这种效果,你需要有一个 **global_request_data** 类型的`reqdata`(请求数据)并且其中包含`sess_userId`(用户的 ID以及`sess_id`(用户目前的会话 ID。你还最好有具有自说明性的标志帮着呈现页面
```
#ifndef _VV_LOGIN
#define _VV_LOGIN
typedef struct s_reqdata {
    bool displayed_logout; // true 如果显示登出连接
    bool is_logged_in; // true 如果会话验证登录
    char *sess_userId; // 目前会话的用户 ID
    char *sess_id; // 会话 ID
} reqdata;
void login_or_signup ();
#endif
```
#### 会话检查和会话数据 (_before.vely)
Vely 里有一个 **before_request_handler** 的概念。你写的代码是在任何处理请求的代码之前执行的。要达到这个目的,你只需要将这样的代码写在名为`_before.vely`的文件中,然后剩余的部分将会自动处理。
任何 SaaS 应用程序所能作的,例如处理发送至应用程序的请求,必须验证其安全性。这样,应用程序就能知晓调用方是否有执行操作所需要的权限。
在这里,通过 before-request 处理程序进行权限检查。这样,无论你使用处理请求的任何其他代码,你都已经拥有了会话信息。
为保持会话数据(比如会话 ID 和用户 ID在你代码中的任何地方都可用你可以使用 **global_request_data**。它只是一个指向内存的通用指针(**void***),任何处理请求的代码都可以访问它。这非常适合处理会话,如下所示:
```
#include "vely.h"
#include "login.h"
// _before() 是 before-request-handler。它总是在任何其它处理请求的
// 任何其它处理请求的代码之前执行。它是任何类型的
// 请求范围设置或数据初始化的好地方
void _before() {
    // 输出 HTTP 请求头
    out-header default
    reqdata *rd; // 这是全局请求数据,见 login.h
    // 为全局请求数据分配内存,
    // 将在请求结束时自动释放
    new-mem rd size sizeof(reqdata)
    // 初始化标志
    rd->displayed_logout = false;
    rd->is_logged_in = false;
    // 将我们创建的数据设置为全局请求数据,
    // 可以从任何处理请求的代码中访问
    set-req data rd
    // 检查会话是否存在(基于来自客户端的 cookie)
    // 这在任何其他请求处理代码之前执行,
    // 使其更容易准备好会话信息
    _check_session ();
}
```
#### 检查会话是否有效 (_check_session.vely)
多租户 SaaS 应用程序中最重要的任务之一就是通过检查用户是否登录来检查(尽快)会话是否有效。这是通过从客户端(例如 web 浏览器)获取会话 ID 和用户 ID 的 cookie并将它们与存储会话的数据库进行比较来实现的
```
#include "vely.h"
#include "login.h"
// 检查会话是否有效
void _check_session () {
    // 获取全局请求数据
    reqdata *rd;
    get-req data to rd
    // 自用户浏览器获取 cookies
    get-cookie rd->sess_userId="sess_userId"
    get-cookie rd->sess_id="sess_id"
    if (rd->sess_id[0] != 0) {
        // 检查对于给定用户 ID 下的会话 ID 是否正确
        char *email;
        run-query @db_multitenant_SaaS = "select email from users where userId='%s' and session='%s'" output email : rd->sess_userId, rd->sess_id row-count define rcount
            query-result email to email
        end-query
        if (rcount == 1) {
            // 如果正确,设置登录标志
            rd->is_logged_in = true;
            // 如果登出链接不显示,则显示它
            if (rd->displayed_logout == false) {
                @Hi <<p-out email>>! <a href="https://opensource.com/?req=login&action=logout">Logout</a><br/>
                rd->displayed_logout = true;
            }
        } else rd->is_logged_in = false;
    }
}
```
#### 注册登录登出login.vely
T任何多租户系统的基础便是具有用户注册登录和登出的功能。通常情况下注册包括验证电子邮件地址不止于此同一电子邮件地址会作为一个用户名。这里有个例子。
这里实现了几个执行该功能所必须的子请求:
- 注册新用户时,显示 HTML 表单以收集信息。它的 URL 请求签名是`req=login&action=newuser`。
- 作为对注册表单的响应创建一个新用户。URL 请求的签名是`req=login&action=createuser`。**input-param** 信号获取 **email****pwd** 的 POST 表单字段。密码值是单向散列,电子邮件验证令牌是一个随机的 5 位数字。这些被插入到 **users** 表中,创建一个新用户。系统会发送一封验证邮件,并提示用户阅读邮件并输入代码。
- 通过输入发送到该电子邮件的验证码来验证电子邮件。URL 请求的签名是`req=login&action=verify`。
- 显示一个登录表单让用户登录。URL 请求的签名是`req=login`(例如,`action`为空)。
- 通过验证电子邮件地址用户名和密码登录。URL请求的签名是`req=login&action=login`。
- 应用户要求登出。URL请求的签名是`req=login&action=logout`。
- 应用程序的登录页。URL请求的签名是`req=login&action=begin`。
- 如果用户当前已登录,转到应用程序的登录页面。
可以看看下面这些例子:
```
#include "vely.h"
#include "login.h"
// 处理任何云端多租户应用程序的会话维护,登录,注销,会话验证
// 会话维护,登录,注销,会话验证
void login () {
    // 获取 URL 的输入参数"action"
    input-param action
    // 获取全局请求数据,我们在其中记录会话信息,所以它很方便
    reqdata *rd;
    get-req data to rd
    // 如果会话已经建立,我们不会
    // 继续到应用程序主页的唯一原因是我们正在登出
    if (rd->is_logged_in) {
        if (strcmp(action, "logout")) {
            _show_home();
            exit-request
        }
    }
    // 应用程序页面启动。 显示登录或注册的链接,
    // 并显示适当的主屏幕
    if (!strcmp (action, "begin")) {
        _show_home();
        exit-request
    // 开始创建新用户。 询问电子邮件和密码,
    // 然后在提交此表单时继续创建用户。
    } else if (!strcmp (action, "newuser")) {
        @Create New User<hr/>
        @<form action="https://opensource.com/?req=login" method="POST">
        @<input name="action" type="hidden" value="createuser">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<input type="submit" value="Sign Up">
        @</form>
    // 验证用户发送到电子邮件的代码。代码必须匹配,从而验证电子邮件地址   
    } else if (!strcmp (action, "verify")) {
        input-param code
        input-param email
        // 获取基于电子邮件的验证令牌
        run-query @db_multitenant_SaaS = "select verify_token from users where email='%s'" output db_verify : email
            query-result db_verify to define db_verify
            // 将数据库中记录的令牌与用户提供的令牌进行比较
            if (!strcmp (code, db_verify)) {
                @Your email has been verifed. Please <a href="https://opensource.com/?req=login">Login</a>.
                // 如果匹配,更新用户信息以表明已验证。
                run-query @db_multitenant_SaaS no-loop = "update users set verified=1 where email='%s'" : email
                exit-request
            }
        end-query
        @Could not verify the code. Please try <a href="https://opensource.com/?req=login">again</a>.
        exit-request
    // 创建用户 —— 当用户使用电子邮件和密码提交表单以创建用户时运行   
    } else if (!strcmp (action, "createuser")) {
        input-param email
        input-param pwd
        // 创建散列(单向)密码
        hash-string pwd to define hashed_pwd
        // 生成随机的 5 位数字字符串验证代码
        random-string to define verify length 5 number
        // 创建用户:插入电子邮件,哈希密码,验证令牌。当前验证状态为 0或未验证
        begin-transaction @db_multitenant_SaaS
        run-query @db_multitenant_SaaS no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" : email, hashed_pwd, verify affected-rows define arows error define err on-error-continue
        if (strcmp (err, "0") || arows != 1) {
            // 如果不能添加用户,则可能该用户不存在。不管怎样,我们都无法继续。
            login_or_signup();
            @User with this email already exists.
            rollback-transaction @db_multitenant_SaaS
        } else {
            // 创建带有验证码的电子邮件并将其发送给用户
            write-string define msg
                @From: vely@vely.dev
                @To: <<p-out email>>
                @Subject: verify your account
                @
                @Your verification code is: <<p-out verify>>
            end-write-string
            exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status define st
            if (st != 0) {
                @Could not send email to <<p-out email>>, code is <<p-out verify>>
                rollback-transaction @db_multitenant_SaaS
                exit-request
            }
            commit-transaction @db_multitenant_SaaS
            // 通知用户查看邮件并输入验证码
            @Please check your email and enter verification code here:
            @<form action="https://opensource.com/?req=login" method="POST">
            @<input name="action" type="hidden" value="verify" size="50" maxlength="50">
            @<input name="email" type="hidden" value="<<p-out email>>">
            @<input name="code" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Verification code">
            @<button type="submit">Verify</button>
            @</form>
        }
    // 这里在登录用户登出时运行    
    } else if (!strcmp (action, "logout")) {
        // 更新用户表以清除会话,即没有该用户登录
        if (rd->is_logged_in) {
            run-query @db_multitenant_SaaS = "update users set session='' where userId='%s'" : rd->sess_userId no-loop affected-rows define arows
            if (arows == 1) {
                rd->is_logged_in = false; // 提示用户未登录
                @You have been logged out.<hr/>
            }
        }
        _show_home();
    // 登录:当用户输入用户名和密码时运行
    } else if (!strcmp (action, "login")) {
        input-param pwd
        input-param email
        // 创建单向散列,目的是与用户表进行比较 —— 密码 永远不会 被记录
        hash-string pwd to define hashed_pwd
        // 为会话 ID 创建一个随机的 30 位长的字符串
        random-string to rd->sess_id length 30
        // 检查用户名和哈希密码是否匹配
        run-query @db_multitenant_SaaS = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_userId : email, hashed_pwd
            query-result sess_userId to rd->sess_userId
            // 如果匹配,使用会话 ID 更新用户表
            run-query @db_multitenant_SaaS no-loop = "update users set session='%s' where userId='%s'" : rd->sess_id, rd->sess_userId affected-rows define arows
            if (arows != 1) {
                @Could not create a session. Please try again. <<.login_or_signup();>> <hr/>
                exit-request
            }
            // 设置“用户 ID”和“会话 ID”为 cookie。用户的浏览器将在每个请求中返回这些信息
            set-cookie "sess_userId" = rd->sess_userId
            set-cookie "sess_id" = rd->sess_id
            // 显示主页,确保会话是正确的,并设置标志
            _check_session();
            _show_home();
            exit-request
        end-query
        @Email or password are not correct. <<.login_or_signup();>><hr/>
    // 登录界面,要求用户输入用户名和密码  
    } else if (!strcmp (action, "")) {
        login_or_signup();
        @Please Login:<hr/>
        @<form action="https://opensource.com/?req=login" method="POST">
        @<input name="action" type="hidden" value="login" size="50" maxlength="50">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<button type="submit">Go</button>
        @</form>
    }
}
// 显示登录或注册链接
void login_or_signup() {
        @<a href="https://opensource.com/?req=login">Login</a> & & <a href="https://opensource.com/?req=login&action=newuser">Sign Up</a><hr/>
}
```
#### 通用应用程序 (_show_home.vely)
借助本教程,您可以创建您想要的任何多租户 SaaS 应用程序。上面的多租户处理模块(`login.vely`)调用 **_show_home()** 函数,它可以容纳你的任何代码。这个示例代码展示了笔记应用程序,但它可以是任何内容。**_show_home()** 函数可以调用你想要的任何代码,它是一个通用的多租户应用程序插件:
```
#include "vely.h"
void _show_home() {
    notes();
    exit-request
}
```
#### 笔记应用程序notes.vely
该应用程序能够添加,列举以及删除任何给定的笔记:
```
#include "vely.h"
#include "login.h"
// 多租户云中的笔记应用程序
void notes () {
    // 获取全局请求数据
    reqdata *rd;
    get-req data to rd
    // 如果会话有效,显示登录或注册
    if (!rd->is_logged_in) {
        login_or_signup();
    }
    // 问候用户
    @<h1>Welcome to Notes!</h1><hr/>
    // 如果没有登出,退出 —— 这里确保对用户身份的安全验证
    if (!rd->is_logged_in) {
        exit-request
    }
    // 获取 URL 参数,告诉笔记要做什么
    input-param subreq
    // 显示笔记能够做什么操作(添加或列举笔记)
    @<a href="https://opensource.com/?req=notes&subreq=add">Add Note</a> <a href="https://opensource.com/?req=notes&subreq=list">List Notes</a><hr/>
    // 列举该用户的所有笔记
    if (!strcmp (subreq, "list")) {
        // 只 选取该用户的笔记
        run-query @db_multitenant_SaaS = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" : rd->sess_userId output dateOf, note, noteId
            query-result dateOf to define dateOf
            query-result note to define note
            query-result noteId to define noteId
            // 使用快速缓存正则表达式将新行更改为<br/>
            match-regex "\n" in note replace-with "<br/>\n" result define with_breaks status define st cache
            if (st == 0) with_breaks = note; // 什么都没有发现/替换,只用原来的
            // 显示笔记
            @Date: <<p-out dateOf>> (<a href="https://opensource.com/?req=notes&subreq=delete_note_ask&note_id=%3C%3Cp-out%20noteId%3E%3E">delete note</a>)<br/>
            @Note: <<p-out with_breaks>><br/>
            @<hr/>
        end-query
    }
    // 要求删除笔记
    else if (!strcmp (subreq, "delete_note_ask")) {
        input-param note_id
        @Are you sure you want to delete a note? Use Back button to go back, or <a href="https://opensource.com/?req=notes&subreq=delete_note&note_id=%3C%3Cp-out%20note_id%3E%3E">delete note now</a>.
    }
    // 删除笔记
    else if (!strcmp (subreq, "delete_note")) {
        input-param note_id
        // 删除笔记
        run-query @db_multitenant_SaaS = "delete from notes where noteId='%s' and userId='%s'" : note_id, rd->sess_userId affected-rows define arows no-loop error define errnote
        // Inform user of status
        if (arows == 1) {
            @Note deleted
        } else {
            @Could not delete note (<<p-out errnote>>)
        }
    }
    // 添加笔记
    else if (!strcmp (subreq, "add_note")) {
        // 从note表单中获取 URL POST 数据
        input-param note
        // 在该用户的 ID 下插入笔记
        run-query @db_multitenant_SaaS = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : rd->sess_userId, note affected-rows define arows no-loop error define errnote
        // 告知用户状态
        if (arows == 1) {
            @Note added
        } else {
            @Could not add note (<<p-out errnote>>)
        }
    }
    // 显示一个 HTML 表单来收集笔记,并将其发送回这里(使用 subreq="add_note" URL 参数)
    else if (!strcmp (subreq, "add")) {
        @Add New Note
        @<form action="https://opensource.com/?req=notes" method="POST">
        @<input name="subreq" type="hidden" value="add_note">
        @<textarea name="note" rows="5" cols="50" required autofocus placeholder="Enter Note"></textarea>
        @<button type="submit">Create</button>
        @</form>
    }
}
```
### 具有 C 性能的 SaaS
Vely 语言使得 C 语言在你的 Web 应用程序中得到充分利用这件事成为可能。多租户 SaaS 应用程序便是从中受益的一个典型用例。参考参考代码示例,写一写代码,然后给 Vely 一个机会。
--------------------------------------------------------------------------------
via: https://opensource.com/article/22/11/build-your-own-saas-vely
作者:[Sergio Mijatovic][a]
选题:[lkxed][b]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://opensource.com/users/vely
[b]: https://github.com/lkxed
[1]: https://opensource.com/article/22/5/write-c-appplications-vely-linux
[2]: https://opensource.com/sites/default/files/2022-10/1createuser.png
[3]: https://opensource.com/sites/default/files/2022-10/2verifyemail.png
[4]: https://opensource.com/sites/default/files/2022-10/3login.png
[5]: https://opensource.com/sites/default/files/2022-10/4addnote.png
[6]: https://opensource.com/sites/default/files/2022-10/5listnotes.png
[7]: https://opensource.com/sites/default/files/2022-10/6confirmdelete.png
[8]: https://opensource.com/sites/default/files/2022-10/7notedeleted.png
[9]: https://vely.dev/