diff --git a/sources/tech/20221107.2 ⭐️⭐️⭐️ Build your own SaaS on Linux with Vely.md b/sources/tech/20221107.2 ⭐️⭐️⭐️ Build your own SaaS on Linux with Vely.md deleted file mode 100644 index e0e5e6712a..0000000000 --- a/sources/tech/20221107.2 ⭐️⭐️⭐️ Build your own SaaS on Linux with Vely.md +++ /dev/null @@ -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 <>! Logout
-                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
-        @
-        @ -        @ -        @ -        @ -        @
- -    // 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 Login. -                // 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 again. -        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: <> -                @Subject: verify your account -                @ -                @Your verification code is: <> -            end-write-string -            exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status define st -            if (st != 0) { -                @Could not send email to <>, code is <> -                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: -            @
-            @ -            @ -            @ -            @ -            @
-        } - -    // 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.
-            } -        } -        _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();>>
-                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();>>
- -    // Login screen, asks user to enter user name and password     -    } else if (!strcmp (action, "")) { -        login_or_signup(); -        @Please Login:
-        @
-        @ -        @ -        @ -        @ -        @
-    } -} - -// Display Login or Sign Up links -void login_or_signup() { -        @Login & & Sign Up
-} -``` - -#### 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 -    @

Welcome to Notes!


-    // 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) -    @Add Note List Notes
- -    // 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
with fast cached Regex -            match-regex "\n" in note replace-with "
\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: <> (delete note)
-            @Note: <>
-            @
-        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 delete note now. -    } - -    // 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 (<>) -        } -    } - -    // 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 (<>) -        } -    } - -    // 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 -        @
-        @ -        @ -        @ -        @
-    } -} -``` - -### 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/ diff --git a/translated/tech/20221107.2 ⭐️⭐️⭐️ Build your own SaaS on Linux with Vely.md b/translated/tech/20221107.2 ⭐️⭐️⭐️ Build your own SaaS on Linux with Vely.md new file mode 100644 index 0000000000..3d076a9ae2 --- /dev/null +++ b/translated/tech/20221107.2 ⭐️⭐️⭐️ Build your own SaaS on Linux with Vely.md @@ -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]上的安装指示。这是使用标准工具包的快速流程,例如 DNF,APT,Pacman 或者 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`模块。 + +以下指令适用于 Debian,Ubuntu 以及类似的系统,启用`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 发行版: + +- Fedora,CentOS,Mageia 和 Arch: `/etc/httpd/conf/httpd.conf` +- Debian,Ubuntu,Mint: `/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 <>! Logout
+                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
+        @
+        @ +        @ +        @ +        @ +        @
+ +    // 验证用户发送到电子邮件的代码。代码必须匹配,从而验证电子邮件地址    +    } 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 Login. +                // 如果匹配,更新用户信息以表明已验证。 +                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 again. +        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: <> +                @Subject: verify your account +                @ +                @Your verification code is: <> +            end-write-string +            exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status define st +            if (st != 0) { +                @Could not send email to <>, code is <> +                rollback-transaction @db_multitenant_SaaS +                exit-request +            } +            commit-transaction @db_multitenant_SaaS +            // 通知用户查看邮件并输入验证码 +            @Please check your email and enter verification code here: +            @
+            @ +            @ +            @ +            @ +            @
+        } + +    // 这里在登录用户登出时运行     +    } 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.
+            } +        } +        _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();>>
+                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();>>
+ +    // 登录界面,要求用户输入用户名和密码   +    } else if (!strcmp (action, "")) { +        login_or_signup(); +        @Please Login:
+        @
+        @ +        @ +        @ +        @ +        @
+    } +} + +// 显示登录或注册链接 +void login_or_signup() { +        @Login & & Sign Up
+} +``` + +#### 通用应用程序 (_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(); +    } +    // 问候用户 +    @

Welcome to Notes!


+    // 如果没有登出,退出 —— 这里确保对用户身份的安全验证 +    if (!rd->is_logged_in) { +        exit-request +    } +    // 获取 URL 参数,告诉笔记要做什么 +    input-param subreq +    // 显示笔记能够做什么操作(添加或列举笔记) +    @Add Note List Notes
+ +    // 列举该用户的所有笔记 +    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 +            // 使用快速缓存正则表达式将新行更改为
+            match-regex "\n" in note replace-with "
\n" result define with_breaks status define st cache +            if (st == 0) with_breaks = note; // 什么都没有发现/替换,只用原来的 +            // 显示笔记 +            @Date: <> (delete note)
+            @Note: <>
+            @
+        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 delete note now. +    } + +    // 删除笔记 +    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 (<>) +        } +    } + +    // 添加笔记 +    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 (<>) +        } +    } + +    // 显示一个 HTML 表单来收集笔记,并将其发送回这里(使用 subreq="add_note" URL 参数) +    else if (!strcmp (subreq, "add")) { +        @Add New Note +        @
+        @ +        @ +        @ +        @
+    } +} +``` + +### 具有 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/