diff --git a/Cargo.lock b/Cargo.lock index 1669d2d2..758a1f09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2039,7 +2039,7 @@ dependencies = [ [[package]] name = "sqlpage" -version = "0.6.12" +version = "0.7.0" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 804716e2..7286838f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlpage" -version = "0.6.12" +version = "0.7.0" edition = "2021" description = "A SQL-only web application framework. Takes .sql files and formats the query result using pre-made configurable professional-looking components." keywords = ["web", "sql", "framework"] diff --git a/examples/official-site/sqlpage/migrations/05_cookie.sql b/examples/official-site/sqlpage/migrations/05_cookie.sql new file mode 100644 index 00000000..a02f4108 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/05_cookie.sql @@ -0,0 +1,99 @@ +-- Insert the http_header component into the component table +INSERT INTO component (name, description, icon) +VALUES ( + 'cookie', + 'Sets a cookie in the client browser, used for session management and storing user-related information. + + This component creates a single cookie. Since cookies need to be set before the response body is sent to the client, + this component should be placed at the top of the page, before any other components that generate output. + + After being set, a cookie can be accessed anywhere in your SQL code using the `sqlpage.cookie(''cookie_name'')` pseudo-function.', + 'cookie' + ); +-- Insert the parameters for the http_header component into the parameter table +INSERT INTO parameter ( + component, + name, + description, + type, + top_level, + optional + ) +VALUES ( + 'cookie', + 'name', + 'The name of the cookie to set.', + 'TEXT', + TRUE, + FALSE + ), + ( + 'cookie', + 'value', + 'The value of the cookie to set.', + 'TEXT', + TRUE, + TRUE + ), + ( + 'cookie', + 'path', + 'The path for which the cookie will be sent. If not specified, the cookie will be sent for all paths.', + 'TEXT', + TRUE, + TRUE + ), + ( + 'cookie', + 'domain', + 'The domain for which the cookie will be sent. If not specified, the cookie will be sent for all domains.', + 'TEXT', + TRUE, + TRUE + ), + ( + 'cookie', + 'secure', + 'Whether the cookie should only be sent over a secure (HTTPS) connection. If not specified, the cookie will be sent over both secure and non-secure connections.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'cookie', + 'http_only', + 'Whether the cookie should only be accessible via HTTP and not via client-side scripts. If not specified, the cookie will be accessible via both HTTP and client-side scripts.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'cookie', + 'remove', + 'Set to TRUE to remove the cookie from the client browser. When specified, other parameters are ignored.', + 'BOOLEAN', + TRUE, + TRUE + ) + ; +-- Insert an example usage of the http_header component into the example table +INSERT INTO example (component, description, properties) +VALUES ( + 'cookie', + 'Create a cookie named `username` with the value `John Doe`... + +```sql +SELECT ''cookie'' as component, + ''username'' as name, + ''John Doe'' as value; +``` + +and then display the value of the cookie: + +```sql +SELECT ''text'' as component, + ''Your name is '' || COALESCE(sqlpage.cookie(''username''), ''not known to us''); +``` + ', + JSON('[]') + ); \ No newline at end of file diff --git a/examples/read-and-set-http-cookies/README.md b/examples/read-and-set-http-cookies/README.md new file mode 100644 index 00000000..dc5f2a66 --- /dev/null +++ b/examples/read-and-set-http-cookies/README.md @@ -0,0 +1,5 @@ +# SQLPage application with custom login in Postgres + +This is a very simple example of a website that uses the SQLPage web application framework. It uses a Postgres database for storing the data. + +It lets an user log in and out, and it shows a list of the users that have logged in. \ No newline at end of file diff --git a/examples/read-and-set-http-cookies/index.sql b/examples/read-and-set-http-cookies/index.sql new file mode 100644 index 00000000..9442a735 --- /dev/null +++ b/examples/read-and-set-http-cookies/index.sql @@ -0,0 +1,14 @@ +-- Sets the username cookie to the value of the username parameter +SELECT 'cookie' as component, + 'username' as name, + $username as value +WHERE $username IS NOT NULL; + +SELECT 'form' as component; +SELECT 'username' as name, + 'User Name' as label, + COALESCE($username, sqlpage.cookie('username')) as value, + 'try leaving this page and coming back, the value should be saved in a cookie' as description; + +select 'text' as component; +select 'log out' as contents, 'logout.sql' as link; \ No newline at end of file diff --git a/examples/read-and-set-http-cookies/logout.sql b/examples/read-and-set-http-cookies/logout.sql new file mode 100644 index 00000000..83bef32d --- /dev/null +++ b/examples/read-and-set-http-cookies/logout.sql @@ -0,0 +1,6 @@ +-- Sets the username cookie to the value of the username parameter +SELECT 'cookie' as component, + 'username' as name, + TRUE as remove; + +SELECT 'http_header' as component, 'index.sql' as Location; \ No newline at end of file diff --git a/examples/read-and-set-http-cookies/sqlpage/sqlpage.json b/examples/read-and-set-http-cookies/sqlpage/sqlpage.json new file mode 100644 index 00000000..78995eb2 --- /dev/null +++ b/examples/read-and-set-http-cookies/sqlpage/sqlpage.json @@ -0,0 +1,3 @@ +{ + "database_url": "sqlite://:memory:" +} \ No newline at end of file diff --git a/src/render.rs b/src/render.rs index 0d707076..4adc44fc 100644 --- a/src/render.rs +++ b/src/render.rs @@ -42,6 +42,7 @@ impl HeaderContext { match get_object_str(&data, "component") { Some("status_code") => self.status_code(&data).map(PageContext::Header), Some("http_header") => self.add_http_header(&data).map(PageContext::Header), + Some("cookie") => self.add_cookie(&data).map(PageContext::Header), _ => self.start_body(data).await, } } @@ -78,6 +79,46 @@ impl HeaderContext { Ok(self) } + fn add_cookie(mut self, data: &JsonValue) -> anyhow::Result { + let obj = data.as_object().with_context(|| "expected object")?; + let name = obj + .get("name") + .and_then(JsonValue::as_str) + .with_context(|| "cookie name must be a string")?; + let mut cookie = actix_web::cookie::Cookie::named(name); + + let remove = obj.get("remove"); + if remove == Some(&json!(true)) || remove == Some(&json!(1)) { + self.response.cookie(cookie); + return Ok(self); + } + + let value = obj + .get("value") + .and_then(JsonValue::as_str) + .with_context(|| "cookie value must be a string")?; + cookie.set_value(value); + let http_only = obj.get("http_only"); + cookie.set_http_only(http_only != Some(&json!(false)) && http_only != Some(&json!(0))); + let secure = obj.get("secure"); + cookie.set_secure(secure != Some(&json!(false)) && secure != Some(&json!(0))); + let path = obj.get("path").and_then(JsonValue::as_str); + if let Some(path) = path { + cookie.set_path(path); + } + let domain = obj.get("domain").and_then(JsonValue::as_str); + if let Some(domain) = domain { + cookie.set_domain(domain); + } + let expires = obj.get("expires").and_then(JsonValue::as_i64); + if let Some(expires) = expires { + cookie.set_expires(actix_web::cookie::Expiration::DateTime( + actix_web::cookie::time::OffsetDateTime::from_unix_timestamp(expires)?, + )); + } + self.response.cookie(cookie); + Ok(self) + } async fn start_body(self, data: JsonValue) -> anyhow::Result> { let renderer = RenderContext::new(self.app_state, self.writer, data).await?; let http_response = self.response;