From 2acc3472e95ddecc34c1b91dd0b698ddf4bd64fe Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sat, 25 Nov 2023 19:34:24 +0100 Subject: [PATCH] new example: image gallery with user uploads --- CHANGELOG.md | 2 ++ README.md | 1 + .../image gallery with user uploads/README.md | 6 ++++ .../create_session.sql | 19 +++++++++++ .../image gallery with user uploads/index.sql | 23 +++++++++++++ .../image gallery with user uploads/login.sql | 10 ++++++ .../logout.sql | 6 ++++ .../sqlpage/migrations/0001_images_table.sql | 8 +++++ .../sqlpage/migrations/0002_users.sql | 14 ++++++++ .../sqlpage/sqlpage.json | 3 ++ .../upload.sql | 25 +++++++++++++++ .../upload_form.sql | 7 ++++ .../sqlpage/migrations/07_authentication.sql | 32 ++++++++++++++++++- 13 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 examples/image gallery with user uploads/README.md create mode 100644 examples/image gallery with user uploads/create_session.sql create mode 100644 examples/image gallery with user uploads/index.sql create mode 100644 examples/image gallery with user uploads/login.sql create mode 100644 examples/image gallery with user uploads/logout.sql create mode 100644 examples/image gallery with user uploads/sqlpage/migrations/0001_images_table.sql create mode 100644 examples/image gallery with user uploads/sqlpage/migrations/0002_users.sql create mode 100644 examples/image gallery with user uploads/sqlpage/sqlpage.json create mode 100644 examples/image gallery with user uploads/upload.sql create mode 100644 examples/image gallery with user uploads/upload_form.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c15b36b..2ccca585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ returning 'text' as component, 'Uploaded new file with id: ' || id as contents; - [`sqlpage.uploaded_file_path`](https://sql.ophir.dev/functions.sql?function=uploaded_file_path#function) to get the temprary local path of a file uploaded by the user. This path will be valid until the end of the current request, and will be located in a temporary directory (customizable with `TMPDIR`). You can use [`sqlpage.exec`](https://sql.ophir.dev/functions.sql?function=exec#function) to operate on the file, for instance to move it to a permanent location. - [`sqlpage.uploaded_file_mime_type`](https://sql.ophir.dev/functions.sql?function=uploaded_file_name#function) to get the type of file uploaded by the user. This is the MIME type of the file, such as `image/png` or `text/csv`. You can use this to easily check that the file is of the expected type before storing it. + The new *Image gallery* example in the official repository shows how to use these functions to create a simple image gallery with user uploads. + ##### Read files These new functions are useful to read the content of a file uploaded by the user, diff --git a/README.md b/README.md index 2e0bcf39..ca1857c6 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ An alternative for Mac OS users is to use [SQLPage's homebrew package](https://f - [Corporate Conundrum](./examples/corporate-conundrum/): a board game implemented in SQL - [Master-Detail Forms](./examples/master-detail-forms/): shows how to implement a simple set of forms to insert data into database tables that have a one-to-many relationship. - [SQLPage's own official website and documentation](./examples/official-site/): The SQL source code for the project's official site, https://sql.ophir.dev +- [Image gallery](./examples/image%20gallery%20with%20user%20uploads/): An image gallery where users can log in and upload images. Illustrates the implementation of a user authentication system using session cookies, and the handling of file uploads. - [User Management](./examples/user-authentication/): An authentication demo with user registration, log in, log out, and confidential pages. Uses PostgreSQL. - [Making a JSON API and integrating React components in the frontend](./examples/using%20react%20and%20other%20custom%20scripts%20and%20styles/): Shows how to integrate a react component in a SQLPage website, and how to easily build a REST API with SQLPage. diff --git a/examples/image gallery with user uploads/README.md b/examples/image gallery with user uploads/README.md new file mode 100644 index 00000000..fdcbaa9c --- /dev/null +++ b/examples/image gallery with user uploads/README.md @@ -0,0 +1,6 @@ +# Image gallery + +This example shows how to create an image gallery with user uploads. + +Users can log in (default login is `admin`/`admin`) and upload images. +Uploaded images are stored in the `uploads` folder. \ No newline at end of file diff --git a/examples/image gallery with user uploads/create_session.sql b/examples/image gallery with user uploads/create_session.sql new file mode 100644 index 00000000..85f40491 --- /dev/null +++ b/examples/image gallery with user uploads/create_session.sql @@ -0,0 +1,19 @@ +-- redirect to the login page if the password is not correct +SELECT 'authentication' AS component, + 'login.sql?error' AS link, + (select password_hash from user where username = :Username) AS password_hash, + :Password AS password; + +-- code after this line will only be executed if the user is authenticated +-- (i.e. if the password that they sent matches the password hash that we have stored for them) + +insert into session (id, username) +values (sqlpage.random_string(32), :Username) +returning + 'cookie' AS component, + 'session_token' AS name, + id AS value; + +-- The user browser will now have a cookie named `session_token` that we can check later +-- to see if the user is logged in. +select 'redirect' as component, '/' as link; diff --git a/examples/image gallery with user uploads/index.sql b/examples/image gallery with user uploads/index.sql new file mode 100644 index 00000000..21ecc49c --- /dev/null +++ b/examples/image gallery with user uploads/index.sql @@ -0,0 +1,23 @@ +select 'shell' as component, + 'My image gallery' as title, + ( + case when sqlpage.cookie('session_token') is null then 'login' + else 'logout' end + ) as menu_item; + +select 'card' as component, + 'My image gallery' as title; + +select title, description, image_url as top_image +from image; + +select 'Your gallery is empty' as title, + 'You have not uploaded any images yet. Click the button below to upload a new image.' as description +where not exists (select 1 from image); + +select 'button' as component; +select + 'Upload a new image' as title, + 'upload_form.sql' as link, + 'plus' as icon, + 'primary' as color; diff --git a/examples/image gallery with user uploads/login.sql b/examples/image gallery with user uploads/login.sql new file mode 100644 index 00000000..0d5f70d1 --- /dev/null +++ b/examples/image gallery with user uploads/login.sql @@ -0,0 +1,10 @@ +select 'shell' as component, 'My image gallery' as title; + +select 'form' as component, 'Login' as title, 'create_session.sql' as action; +select 'text' as type, 'Username' as name, true as required; +select 'password' as type, 'Password' as name, true as required; + + +select 'alert' as component, 'You are not logged in' as title, + 'Sorry, we could not log you in. Please try again.' as description +where $error is not null; \ No newline at end of file diff --git a/examples/image gallery with user uploads/logout.sql b/examples/image gallery with user uploads/logout.sql new file mode 100644 index 00000000..2ebc7fa9 --- /dev/null +++ b/examples/image gallery with user uploads/logout.sql @@ -0,0 +1,6 @@ +select + 'cookie' AS component, + 'session_token' AS name, + true AS remove; + +select 'redirect' as component, '/login.sql' as link -- redirect to the login page after the user logs out diff --git a/examples/image gallery with user uploads/sqlpage/migrations/0001_images_table.sql b/examples/image gallery with user uploads/sqlpage/migrations/0001_images_table.sql new file mode 100644 index 00000000..0832a503 --- /dev/null +++ b/examples/image gallery with user uploads/sqlpage/migrations/0001_images_table.sql @@ -0,0 +1,8 @@ +-- a sqlite table that will hold the image URLs together with a title and a description +CREATE TABLE image ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT NOT NULL, + image_url TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/examples/image gallery with user uploads/sqlpage/migrations/0002_users.sql b/examples/image gallery with user uploads/sqlpage/migrations/0002_users.sql new file mode 100644 index 00000000..325dae68 --- /dev/null +++ b/examples/image gallery with user uploads/sqlpage/migrations/0002_users.sql @@ -0,0 +1,14 @@ +create table user ( + username text primary key, + password_hash text not null +); + +create table session ( + id text primary key, + username text not null references user(username), + created_at timestamp not null default current_timestamp +); + +-- Creates an initial user with the username `admin` and the password `admin` (hashed using sqlpage.hash_password('admin')) +insert into user (username, password_hash) +values ('admin', '$argon2id$v=19$m=19456,t=2,p=1$4lu3hSvaqXK0dMCPZLOIPg$PUFJSB6L3J5eZ33z9WX7y0nOH6KawV2FdW0abMuPE7o'); \ No newline at end of file diff --git a/examples/image gallery with user uploads/sqlpage/sqlpage.json b/examples/image gallery with user uploads/sqlpage/sqlpage.json new file mode 100644 index 00000000..1828e4db --- /dev/null +++ b/examples/image gallery with user uploads/sqlpage/sqlpage.json @@ -0,0 +1,3 @@ +{ + "max_uploaded_file_size": 500000 +} \ No newline at end of file diff --git a/examples/image gallery with user uploads/upload.sql b/examples/image gallery with user uploads/upload.sql new file mode 100644 index 00000000..d6f3c97c --- /dev/null +++ b/examples/image gallery with user uploads/upload.sql @@ -0,0 +1,25 @@ +-- important: we do not accept file uploads from unauthenticated users +select 'redirect' as component, '/login.sql' as link -- redirect to the login page if the user is not logged in +where not exists ( + select true from session + where + sqlpage.cookie('session_token') = id and + created_at > datetime('now', '-1 day') -- require the user to log in again after 1 day +); + +insert or ignore into image (title, description, image_url) +values ( + :Title, + :Description, + sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path('Image')) +) +returning 'redirect' as component, + format('/?created_id=%d', id) as link; + +-- If the insert failed, warn the user +select 'alert' as component, + 'red' as color, + 'alert-triangle' as icon, + 'Failed to upload image' as title, + 'Please try again with a smaller picture. Maximum allowed file size is 500Kb.' as description +; \ No newline at end of file diff --git a/examples/image gallery with user uploads/upload_form.sql b/examples/image gallery with user uploads/upload_form.sql new file mode 100644 index 00000000..0c23417e --- /dev/null +++ b/examples/image gallery with user uploads/upload_form.sql @@ -0,0 +1,7 @@ +select 'redirect' as component, '/login.sql' as link -- redirect to the login page if the user is not logged in +where not exists (select true from session where sqlpage.cookie('session_token') = id and created_at > datetime('now', '-1 day')); -- require the user to log in again after 1 day + +select 'form' as component, 'Upload a new image' as title, 'upload.sql' as action; +select 'text' as type, 'Title' as name, true as required; +select 'text' as type, 'Description' as name; +select 'file' as type, 'Image' as name, 'image/*' as accept; \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/07_authentication.sql b/examples/official-site/sqlpage/migrations/07_authentication.sql index 016b4395..8c28f75c 100644 --- a/examples/official-site/sqlpage/migrations/07_authentication.sql +++ b/examples/official-site/sqlpage/migrations/07_authentication.sql @@ -60,6 +60,8 @@ The username and password entered by the user will be accessible in your SQL cod [`sqlpage.basic_auth_username()`](functions.sql?function=basic_auth_username) and [`sqlpage.basic_auth_password()`](functions.sql?function=basic_auth_password) functions. +The [`sqlpage.hash_password`](functions.sql?function=hash_password) function can be used to generate a secure password hash that you need to store in your database. + ```sql SELECT ''authentication'' AS component, ''$argon2id$v=19$m=16,t=2,p=1$TERTd0lIcUpraWFTcmRQYw$+bjtag7Xjb6p1dsuYOkngw'' AS password_hash, -- generated using sqlpage.hash_password @@ -83,4 +85,32 @@ and in `login.sql` : SELECT ''form'' AS component, ''Login'' AS title, ''my_protected_page.sql'' AS action; SELECT ''password'' AS type, ''password'' AS name, ''Password'' AS label; ``` -'); \ No newline at end of file + +### Advanced: usage with a session token + +Calling the `authentication` component is expensive. +The password hashing algorithm is designed to be slow, so that it is difficult to brute-force the password, +even if an attacker gets access to the database. + +If you want to avoid calling the `authentication` component on every page, you can use a session token. +A session token is a random string that is generated when the user logs in, and stored in the database. +It has a limited lifetime, and is stored in a cookie in the user''s browser. +When the user visits a page, the session token is sent to the server, and the server checks if it is valid. + +```sql +SELECT ''authentication'' AS component, + ''login.sql'' AS link, + (SELECT password_hash FROM user WHERE username = :username) AS password_hash, + :password AS password; + +-- The code after this point is only executed if the user has sent the correct password + +-- Generate a random session token +INSERT INTO session (id, username) +VALUES (sqlpage.random_string(32), :username) +RETURNING + ''cookie'' AS component, + ''session_token'' AS name, + id AS value; +``` +');