new example: image gallery with user uploads
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
```
|
||||
');
|
||||
|
||||
Reference in New Issue
Block a user