new example: image gallery with user uploads

This commit is contained in:
lovasoa
2023-11-25 19:34:24 +01:00
parent 31bf1b7c1d
commit 2acc3472e9
13 changed files with 155 additions and 1 deletions
+2
View File
@@ -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,
+1
View File
@@ -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.
@@ -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.
@@ -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;
@@ -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;
@@ -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;
@@ -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
@@ -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
);
@@ -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');
@@ -0,0 +1,3 @@
{
"max_uploaded_file_size": 500000
}
@@ -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
;
@@ -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;
@@ -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;
```
');
### 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;
```
');