chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:57 +08:00
commit d718c5a372
986 changed files with 74597 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
# User authentication demo
This example demonstrates how to handle user authentication with SQLpage.
It uses a PostgreSQL database to store user information and session ids,
but the same principles can be applied to other databases.
All the user and password management is done in SQLPage,
which uses [best practices](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#maximum-password-lengths) for password storage.
This demonstrates how to implement:
- [a signup form](./signup.sql)
- [a login form](./signin.sql)
- [a logout button](./logout.sql)
- [secured pages](./protected_page.sql) that can only be accessed by logged-in users
User authentication is a complex topic, and you can follow the work on implementing differenet authentication methods in [this issue](https://github.com/sqlpage/SQLPage/issues/12).
## How to run
Install [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
Then run the following command in this directory:
```bash
docker-compose up
```
Then open [http://localhost:8080](http://localhost:8080) in your browser.
## Caveats
In this example, we handle user creation and login in SQLpage.
If you are implementing user authentication in an public application with potentially sensitive data,
you should propably read the [Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) from OWASP.
## Screenshots
| Signup form | Login form | Protected page |
| --- | --- | --- |
| ![signup form](./screenshots/signup.png) | ![login form](./screenshots/signin.png) | ![protected page](./screenshots/secret.png) |
| ![home](./screenshots/homepage.png) | ![duplicate username](./screenshots/duplicate-user.png) | ![signup success](./screenshots/signup-success.png) |
## How it works
### User creation
The [signup form](./signup.sql) is a simple form that is handled by [`create_user.sql`](./create_user.sql).
You could restrict user creation to existing administrators and create an initial administrator in a database migration.
### User login
The [login form](./signin.sql) is a simple form that is handled by [`login.sql`](./login.sql).
`login.sql` checks that the username exists and that the password is correct using the [authentication component](https://sql-page.com/documentation.sql?component=authentication#component) extension with
```sql
SELECT 'authentication' AS component,
'signin.sql' AS link,
(SELECT password_hash FROM user_info WHERE username = :username) AS password_hash,
:password AS password;
```
If the login is successful, an entry is added to the [`login_session`](./sqlpage/migrations/0000_init.sql) table with a random session id.
If it is not, the authentication component will redirect the user to the login page and stop the execution of the page.
The session id is then stored in a cookie on the user's browser.
The user is then redirected to [`./protected_page.sql`](./protected_page.sql) which will check that the user is logged in.
### Protected pages
Protected pages are pages that can only be accessed by logged-in users.
There is an example in [`protected_page.sql`](./protected_page.sql) that uses
the [`redirect`](https://sql-page.com/documentation.sql?component=redirect#component)
component to redirect the user to the login page if they are not logged in.
Checking whether the user is logged in is as simple as checking that session id returned by [`sqlpage.cookie('session')`](https://sql-page.com/functions.sql?function=cookie#function) exists in the [`login_session`](./sqlpage/migrations/0000_init.sql) table.
### User logout
The cookie can be deleted in the browser by navigating to [`./logout.sql`](./logout.sql).
@@ -0,0 +1,10 @@
INSERT INTO user_info (username, password_hash)
VALUES (:username, sqlpage.hash_password(:password))
ON CONFLICT (username) DO NOTHING
RETURNING
'redirect' AS component,
'create_user_welcome_message.sql?username=' || :username AS link;
-- If we are still here, it means that the user was not created
-- because the username was already taken.
SELECT 'redirect' AS component, 'create_user_welcome_message.sql?error&username=' || :username AS link;
@@ -0,0 +1,15 @@
SELECT 'hero' AS component,
'Welcome' AS title,
'Welcome, ' || $username || '! Your user account was successfully created. You can now log in.' AS description,
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image,
'signin.sql' AS link,
'Log in' AS link_text
WHERE $error IS NULL;
SELECT 'hero' AS component,
'Sorry' AS title,
'Sorry, the user name "' || $username || '" is already taken.' AS description,
'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Sad_face_of_a_Wayuu_Woman.jpg/640px-Sad_face_of_a_Wayuu_Woman.jpg' AS image,
'signup.sql' AS link,
'Try again' AS link_text
WHERE $error IS NOT NULL;
@@ -0,0 +1,18 @@
services:
web:
image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version
ports:
- "8080:8080"
volumes:
- .:/var/www
- ./sqlpage:/etc/sqlpage
depends_on:
- db
environment:
DATABASE_URL: postgres://root:secret@db/sqlpage
db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases
image: postgres
environment:
POSTGRES_USER: root
POSTGRES_DB: sqlpage
POSTGRES_PASSWORD: secret
@@ -0,0 +1,5 @@
SELECT 'form' AS component;
SELECT 'password' AS name, 'Password to create a hash for' AS label, :password AS value;
SELECT 'code' AS component;
SELECT sqlpage.hash_password(:password) AS contents;
+15
View File
@@ -0,0 +1,15 @@
SELECT 'shell' AS component,
'User Management App' AS title,
'user' AS icon,
'/' AS link,
CASE COALESCE(sqlpage.cookie('session'), '')
WHEN '' THEN '["signin", "signup"]'::json
ELSE '["logout"]'::json
END AS menu_item;
SELECT 'hero' AS component,
'SQLPage Authentication Demo' AS title,
'This application requires signing up to view the protected page.' AS description_md,
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image,
'protected_page.sql' AS link,
'Access protected page' AS link_text;
+19
View File
@@ -0,0 +1,19 @@
-- The authentication component will stop the execution of the page and redirect the user to the login page if
-- the password is incorrect or if the user does not exist.
SELECT 'authentication' AS component,
'signin.sql?error' AS link,
(SELECT password_hash FROM user_info WHERE username = :username) AS password_hash,
:password AS password;
-- Generate a random 32 characters session ID, insert it into the database,
-- and save it in a cookie on the user's browser.
INSERT INTO login_session (id, username)
VALUES (sqlpage.random_string(32), :username)
RETURNING
'cookie' AS component,
'session' AS name,
id AS value,
FALSE AS secure; -- You can remove this if the site is served over HTTPS.
-- Redirect the user to the protected page.
SELECT 'redirect' AS component, 'protected_page.sql' AS link;
+4
View File
@@ -0,0 +1,4 @@
DELETE FROM login_session WHERE id = sqlpage.cookie('session');
SELECT 'cookie' AS component, 'session' AS name, TRUE AS remove;
SELECT 'redirect' AS component, '/' AS link;
@@ -0,0 +1,12 @@
SET username = (SELECT username FROM login_session WHERE id = sqlpage.cookie('session'));
SELECT 'redirect' AS component,
'signin.sql?error' AS link
WHERE $username IS NULL;
SELECT 'shell' AS component, 'Protected page' AS title, 'lock' AS icon, '/' AS link, 'logout' AS menu_item;
SELECT 'text' AS component,
'Welcome, ' || $username || ' !' AS title,
'This content is [top secret](https://youtu.be/dQw4w9WgXcQ).
You cannot view it if you are not connected.' AS contents_md;
Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+9
View File
@@ -0,0 +1,9 @@
SELECT 'login' AS component,
'login.sql' AS action,
'Sign in' AS title,
'Username' AS username,
'Password' AS password,
'user' AS username_icon,
'lock' AS password_icon,
case when $error is not null then 'We could not authenticate you. Please log in or [create an account](signup.sql).' end as error_message_md,
'Sign in' AS validate;
+8
View File
@@ -0,0 +1,8 @@
SELECT 'form' AS component,
'Create a new user account' AS title,
'Sign up' AS validate,
'create_user.sql' AS action;
SELECT 'username' AS name;
SELECT 'password' AS name, 'password' AS type, '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$' AS pattern, 'Password must be at least 8 characters long and contain at least one letter and one number.' AS description;
SELECT 'terms' AS name, 'I accept the terms and conditions' AS label, TRUE AS required, FALSE AS value, 'checkbox' AS type;
@@ -0,0 +1,10 @@
CREATE TABLE user_info (
username TEXT PRIMARY KEY,
password_hash TEXT NOT NULL
);
CREATE TABLE login_session (
id TEXT PRIMARY KEY,
username TEXT NOT NULL REFERENCES user_info(username),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -0,0 +1,4 @@
-- Creates an initial user called 'admin'
-- with a password hash that was generated using the 'generate_password_hash.sql' page.
INSERT INTO user_info (username, password_hash)
VALUES ('admin', '$argon2id$v=19$m=19456,t=2,p=1$IiReWDP0ocWvia+fTdozJw$53EozOKX7HkpvOdoWHjsh9yKvRN2TmQm/PjYBeaOqqc');
+59
View File
@@ -0,0 +1,59 @@
GET http://localhost:8080
HTTP 200
[Asserts]
body contains "SQLPage Authentication Demo"
body contains "Access protected page"
body not contains "An error occurred"
GET http://localhost:8080/signin.sql
HTTP 200
[Asserts]
body contains "Sign in"
body contains "Username"
body contains "Password"
GET http://localhost:8080/protected_page.sql
HTTP 302
[Asserts]
header "Location" contains "signin.sql?error"
GET http://localhost:8080/signin.sql?error
HTTP 200
[Asserts]
body contains "We could not authenticate you"
body htmlUnescape contains "href=\"signup.sql\""
POST http://localhost:8080/create_user.sql
[FormParams]
username: hurl_user
password: Password123
terms: on
HTTP 302
[Asserts]
header "Location" contains "create_user_welcome_message.sql?username=hurl_user"
POST http://localhost:8080/login.sql
[FormParams]
username: hurl_user
password: Password123
HTTP 302
[Asserts]
cookie "session" exists
header "Location" contains "protected_page.sql"
GET http://localhost:8080/protected_page.sql
HTTP 200
[Asserts]
body contains "Welcome, hurl_user"
body contains "top secret"
body not contains "An error occurred"
GET http://localhost:8080/logout.sql
HTTP 302
[Asserts]
header "Location" == "/"
GET http://localhost:8080/protected_page.sql
HTTP 302
[Asserts]
header "Location" contains "signin.sql?error"