+
+
+ """
diff --git a/docs/_includes/htmx_load_snippet.sc b/docs/_includes/htmx_load_snippet.sc
new file mode 100644
index 0000000..15bf1ba
--- /dev/null
+++ b/docs/_includes/htmx_load_snippet.sc
@@ -0,0 +1,34 @@
+//> using scala "3.7.0"
+//> using dep ba.sake::sharaf-undertow:0.13.0
+
+import ba.sake.sharaf.{*, given}
+import ba.sake.sharaf.undertow.UndertowSharafServer
+
+val routes = Routes:
+ case GET -> Path() =>
+ Response.withBody(IndexView)
+ case POST -> Path("html-snippet") =>
+ Response.withBody:
+ html"""
+
+ WOW, it works! 😲
+
Look ma, no JS! 😎
+
+ """
+
+UndertowSharafServer("localhost", 8181, routes).start()
+
+println(s"Server started at http://localhost:8181")
+
+def IndexView =
+ html"""
+
+
+
+
+
+
+
+
+
+ """
diff --git a/docs/_includes/path_params.sc b/docs/_includes/path_params.sc
index 8635334..038acbf 100644
--- a/docs/_includes/path_params.sc
+++ b/docs/_includes/path_params.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
import ba.sake.sharaf.*
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/docs/_includes/query_params.sc b/docs/_includes/query_params.sc
index 65f226c..eb1620b 100644
--- a/docs/_includes/query_params.sc
+++ b/docs/_includes/query_params.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
import ba.sake.querson.QueryStringRW
import ba.sake.sharaf.*
diff --git a/docs/_includes/static_files.sc b/docs/_includes/static_files.sc
index 5194d6e..0bd3df8 100644
--- a/docs/_includes/static_files.sc
+++ b/docs/_includes/static_files.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
import ba.sake.sharaf.*
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/docs/_includes/validation.sc b/docs/_includes/validation.sc
index 4a16a1d..b5b521d 100644
--- a/docs/_includes/validation.sc
+++ b/docs/_includes/validation.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
import ba.sake.querson.QueryStringRW
import ba.sake.tupson.JsonRW
diff --git a/docs/_layouts/search-results.html b/docs/_layouts/search-results.html
new file mode 100644
index 0000000..254511b
--- /dev/null
+++ b/docs/_layouts/search-results.html
@@ -0,0 +1,63 @@
+{% extends "base.html" %}
+
+{% block title %}Search Results{% endblock %}
+
+{% block content %}
+
+{% endblock %}
+
+
+{% block scripts %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/docs/content/howtos/upload-file.md b/docs/content/howtos/upload-file.md
index 5d66858..693c5cd 100644
--- a/docs/content/howtos/upload-file.md
+++ b/docs/content/howtos/upload-file.md
@@ -7,15 +7,13 @@ description: Sharaf How To Upload File
Uploading a file is usually done via `multipart/form-data` form submission.
-{%
-set form_snippet = ''
-%}
-
```scala
// 1. somewhere in a view, use enctype="multipart/form-data"
-{{ form_snippet | e }}
+html"""
+
+"""
// 2. define form data class with a NIO Path file
import java.nio.file.Path
diff --git a/docs/content/tutorials/forms.md b/docs/content/tutorials/forms.md
index 80f7b1b..c46dafe 100644
--- a/docs/content/tutorials/forms.md
+++ b/docs/content/tutorials/forms.md
@@ -5,54 +5,13 @@ description: Sharaf Tutorial Forms
# {{ page.title }}
-
-
Form data can be extracted with `Request.current.bodyForm[MyData]`.
The `MyData` needs to have a `FormDataRW` given instance.
Create a file `form_handling.sc` and paste this code into it:
-{# need to HTML encode these snippets, so that Markdown doesnt process them! #}
-{% set contact_us_view = 'html"""
-
-
-
-
-
-
- """'
-%}
-
```scala
-//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
-
-import ba.sake.formson.FormDataRW
-import ba.sake.sharaf.{*, given}
-import ba.sake.sharaf.undertow.UndertowSharafServer
-
-val routes = Routes:
- case GET -> Path() =>
- Response.withBody(ContactUsView)
- case POST -> Path("handle-form") =>
- case class ContactUsForm(fullName: String, email: String) derives FormDataRW
- val formData = Request.current.bodyForm[ContactUsForm]
- Response.withBody(s"Got form data: ${formData}")
-
-UndertowSharafServer("localhost", 8181, routes).start()
-
-println("Server started at http://localhost:8181")
-
-def ContactUsView =
- {{ contact_us_view | e }}
+{% include 'form_handling.sc' %}
```
Then run it like this:
diff --git a/docs/content/tutorials/html.md b/docs/content/tutorials/html.md
index 7e9dc01..37d65de 100644
--- a/docs/content/tutorials/html.md
+++ b/docs/content/tutorials/html.md
@@ -11,53 +11,8 @@ Then you return it directly in the `Response.withBody()`.
Let's make a simple HTML page that greets the user.
Create a file `html.sc` and paste this code into it:
-{# need to HTML encode these snippets, so that Markdown doesnt process them! #}
-{% set index_view = 'html"""
-
-
-
-
-
-
- """'
-%}
-
```scala
-//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
-
-import ba.sake.sharaf.{*, given}
-import ba.sake.sharaf.undertow.UndertowSharafServer
-
-val routes = Routes:
- case GET -> Path() =>
- Response.withBody(IndexView)
- case GET -> Path("hello", name) =>
- Response.withBody(HelloView(name))
-
-UndertowSharafServer("localhost", 8181, routes).start()
-
-println(s"Server started at http://localhost:8181")
-
-def IndexView =
- {{ index_view | e }}
-
-def HelloView(name: String) =
- {{ hello_view | e }}
+{% include 'html.sc' %}
```
and run it like this:
diff --git a/docs/content/tutorials/htmx.md b/docs/content/tutorials/htmx.md
index 5154fe5..02d74b6 100644
--- a/docs/content/tutorials/htmx.md
+++ b/docs/content/tutorials/htmx.md
@@ -16,54 +16,13 @@ You can lots of examples in [examples/htmx]({{site.data.project.gh.sourcesUrl}}/
Let's make a simple page that triggers a POST request to fetch a HTML snippet.
Create a file `htmx_load_snippet.sc` and paste this code into it:
-{# need to HTML encode these snippets, so that Markdown doesnt process them! #}
-{% set div_snippet = 'html"""
-
- WOW, it works! 😲
-
Look ma, no JS! 😎
-
- """'
-%}
-{% set index_view = 'html"""
-
-
-
-
-
-
-
-
-
- """'
-%}
-
-
```scala
-//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
-
-import ba.sake.sharaf.{*, given}
-import ba.sake.sharaf.undertow.UndertowSharafServer
-
-val routes = Routes:
- case GET -> Path() =>
- Response.withBody(IndexView)
- case POST -> Path("html-snippet") =>
- Response.withBody:
- {{ div_snippet | e }}
-
-UndertowSharafServer("localhost", 8181, routes).start()
-
-println(s"Server started at http://localhost:8181")
-
-def IndexView =
- {{ index_view | e }}
-
+{% include 'htmx_load_snippet.sc' %}
```
and run it like this:
```sh
-scala html.sc
+scala htmx_load_snippet.sc
```
Go to [http://localhost:8181](http://localhost:8181)
diff --git a/docs/content/tutorials/index.md b/docs/content/tutorials/index.md
index de29e70..66a3160 100644
--- a/docs/content/tutorials/index.md
+++ b/docs/content/tutorials/index.md
@@ -26,8 +26,7 @@ set tutorials = [
%}
-{% for tut in tutorials %}
-- [{{ tut.label }}]({{ tut.url}})
+{% for tut in tutorials %}- [{{ tut.label }}]({{ tut.url}})
{% endfor %}
diff --git a/docs/content/tutorials/json.md b/docs/content/tutorials/json.md
index 82dd49c..87cfb94 100644
--- a/docs/content/tutorials/json.md
+++ b/docs/content/tutorials/json.md
@@ -12,7 +12,7 @@ Let's make a simple JSON API in scala-cli.
Create a file `json_api.sc` and paste this code into it:
```scala
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep {{site.data.project.artifact.org}}::{{site.data.project.artifact.name}}:{{site.data.project.artifact.version}}
import ba.sake.tupson.JsonRW
import ba.sake.sharaf.*
diff --git a/docs/content/tutorials/quickstart.md b/docs/content/tutorials/quickstart.md
index c8bad9f..97627b9 100644
--- a/docs/content/tutorials/quickstart.md
+++ b/docs/content/tutorials/quickstart.md
@@ -40,11 +40,15 @@ scala my_script.sc --scala-option -Yretain-trees
## Examples
-- [scala examples]({{site.data.project.gh.sourcesUrl}}/examples/scala-cli), standalone examples using scala-cli
-- [scala HTMX examples]({{site.data.project.gh.sourcesUrl}}/examples/htmx), standalone examples featuring HTMX
+- [Scala CLI examples]({{site.data.project.gh.sourcesUrl}}/examples/scala-cli), standalone examples using Scala CLI
+- [Scala CLI HTMX examples]({{site.data.project.gh.sourcesUrl}}/examples/htmx), standalone examples featuring HTMX
- [API example]({{site.data.project.gh.sourcesUrl}}/examples/api) featuring JSON and validation
- [full-stack example]({{site.data.project.gh.sourcesUrl}}/examples/fullstack) featuring HTML, static files and forms
- [sharaf-todo-backend](https://github.com/sake92/sharaf-todo-backend), implementation of the [todobackend.com](http://todobackend.com/) spec, featuring CORS handling
-- [OAuth2 login]({{site.data.project.gh.sourcesUrl}}/examples/oauth2) with [Pac4J library](https://www.pac4j.org/)
+- [Username+Password form login]({{site.data.project.gh.sourcesUrl}}/examples/user-pass-form) with [Pac4J](https://www.pac4j.org/)
+- [JWT auth]({{site.data.project.gh.sourcesUrl}}/examples/jwt) with [Pac4J](https://www.pac4j.org/)
+- [OAuth2 login]({{site.data.project.gh.sourcesUrl}}/examples/oauth2) with [Pac4J](https://www.pac4j.org/)
+- [Snunit]({{site.data.project.gh.sourcesUrl}}/examples/snunit) demo app
+- [Http4s]({{site.data.project.gh.sourcesUrl}}/examples/http4s) demo app
- [PetClinic](https://github.com/sake92/sharaf-petclinic) implementation, featuring full-stack app with Postgres db, config, integration tests etc.
- [Giter8 template for fullstack app](https://github.com/sake92/sharaf-fullstack.g8)
diff --git a/docs/content/tutorials/sql.md b/docs/content/tutorials/sql.md
index db9f44f..02c494b 100644
--- a/docs/content/tutorials/sql.md
+++ b/docs/content/tutorials/sql.md
@@ -33,7 +33,7 @@ Create a file `sql_db.sc` and paste this code into it:
//> using scala "3.7.0"
//> using dep org.postgresql:postgresql:42.7.5
//> using dep com.zaxxer:HikariCP:6.3.0
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep {{site.data.project.artifact.org}}::{{site.data.project.artifact.name}}:{{site.data.project.artifact.version}}
//> using dep ba.sake::squery:0.7.0
import ba.sake.tupson.JsonRW
diff --git a/docs/copy-examples.ps1 b/docs/copy-examples.ps1
index 6ef5a8e..82d821a 100644
--- a/docs/copy-examples.ps1
+++ b/docs/copy-examples.ps1
@@ -6,7 +6,10 @@ $examplesList = @(
"examples/scala-cli/query_params.sc",
"examples/scala-cli/static_files.sc",
"examples/scala-cli/json_api.test.scala",
- "examples/scala-cli/validation.sc"
+ "examples/scala-cli/validation.sc",
+ "examples/scala-cli/html.sc",
+ "examples/scala-cli/form_handling.sc",
+ "examples/htmx/htmx_load_snippet.sc"
)
$targetFolder = "docs/_includes"
diff --git a/examples/fullstack/src/views/ShowFormPage.scala b/examples/fullstack/src/views/ShowFormPage.scala
index 7a347bb..0e6290a 100644
--- a/examples/fullstack/src/views/ShowFormPage.scala
+++ b/examples/fullstack/src/views/ShowFormPage.scala
@@ -3,7 +3,6 @@ package fullstack.views
import ba.sake.validson.ValidationError
import ba.sake.sharaf.*
import fullstack.CreateCustomerForm
-import play.twirl.api.Html
def ShowFormPage(formData: CreateCustomerForm, errors: Seq[ValidationError] = Seq.empty) = {
// errors are returned as JSON Path, hence the $. prefix below!
@@ -30,7 +29,7 @@ def ShowFormPage(formData: CreateCustomerForm, errors: Seq[ValidationError] = Se
"""
}
- val hobbiesInputs = formData.hobbies.zipWithIndex.map { case (hobby, idx) =>
+ val hobbiesInputs = formData.hobbies.zipWithIndex.map { case (_, idx) =>
withInputErrors(s"hobbies[${idx}]", _.hobbies.applyOrElse(idx, _ => "")) {
case (fieldName, fieldValue, fieldErrors) =>
html"""
diff --git a/examples/htmx/htmx_active_search.sc b/examples/htmx/htmx_active_search.sc
index ec2984a..d5f84c4 100644
--- a/examples/htmx/htmx_active_search.sc
+++ b/examples/htmx/htmx_active_search.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/active-search/
diff --git a/examples/htmx/htmx_animations.sc b/examples/htmx/htmx_animations.sc
index 3f9951a..ef198a7 100644
--- a/examples/htmx/htmx_animations.sc
+++ b/examples/htmx/htmx_animations.sc
@@ -1,9 +1,8 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/animations/
-import play.twirl.api.Html
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/examples/htmx/htmx_bulk_update.sc b/examples/htmx/htmx_bulk_update.sc
index 910e3a6..935dcc6 100644
--- a/examples/htmx/htmx_bulk_update.sc
+++ b/examples/htmx/htmx_bulk_update.sc
@@ -1,9 +1,9 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/bulk-update/
-import play.twirl.api.Html
+
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
import ba.sake.formson.FormDataRW
diff --git a/examples/htmx/htmx_cascading_selects.sc b/examples/htmx/htmx_cascading_selects.sc
index 3a9a48e..089641f 100644
--- a/examples/htmx/htmx_cascading_selects.sc
+++ b/examples/htmx/htmx_cascading_selects.sc
@@ -1,9 +1,9 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/value-select/
-import play.twirl.api.Html
+
import ba.sake.querson.QueryStringRW
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/examples/htmx/htmx_click_edit.sc b/examples/htmx/htmx_click_edit.sc
index 4e40a0a..b20f2c7 100644
--- a/examples/htmx/htmx_click_edit.sc
+++ b/examples/htmx/htmx_click_edit.sc
@@ -1,9 +1,9 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/click-to-edit/
-import play.twirl.api.Html
+
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
import ba.sake.formson.FormDataRW
diff --git a/examples/htmx/htmx_click_to_load.sc b/examples/htmx/htmx_click_to_load.sc
index 546c9dc..0d6f393 100644
--- a/examples/htmx/htmx_click_to_load.sc
+++ b/examples/htmx/htmx_click_to_load.sc
@@ -1,10 +1,10 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/click-to-load/
import java.util.UUID
-import play.twirl.api.Html
+
import ba.sake.querson.QueryStringRW
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/examples/htmx/htmx_delete_row.sc b/examples/htmx/htmx_delete_row.sc
index 889c80a..32571a9 100644
--- a/examples/htmx/htmx_delete_row.sc
+++ b/examples/htmx/htmx_delete_row.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/delete-row/
diff --git a/examples/htmx/htmx_dialogs_bootstrap_form.sc b/examples/htmx/htmx_dialogs_bootstrap_form.sc
index 3d1ee13..4815c01 100644
--- a/examples/htmx/htmx_dialogs_bootstrap_form.sc
+++ b/examples/htmx/htmx_dialogs_bootstrap_form.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// example of BS5 modal with a form
// https://htmx.org/examples/modal-bootstrap/
diff --git a/examples/htmx/htmx_dialogs_browser.sc b/examples/htmx/htmx_dialogs_browser.sc
index eb5e59e..4ac80af 100644
--- a/examples/htmx/htmx_dialogs_browser.sc
+++ b/examples/htmx/htmx_dialogs_browser.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/dialogs/
diff --git a/examples/htmx/htmx_edit_row.sc b/examples/htmx/htmx_edit_row.sc
index f8c8fd1..442536a 100644
--- a/examples/htmx/htmx_edit_row.sc
+++ b/examples/htmx/htmx_edit_row.sc
@@ -1,9 +1,9 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/edit-row/
-import play.twirl.api.Html
+
import ba.sake.formson.FormDataRW
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/examples/htmx/htmx_file_upload_js.sc b/examples/htmx/htmx_file_upload_js.sc
index 5215098..1d0f5df 100644
--- a/examples/htmx/htmx_file_upload_js.sc
+++ b/examples/htmx/htmx_file_upload_js.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
import play.twirl.api.{Html, HtmlFormat}
import ba.sake.formson.FormDataRW
diff --git a/examples/htmx/htmx_infinite_scroll.sc b/examples/htmx/htmx_infinite_scroll.sc
index fcf2435..d77f842 100644
--- a/examples/htmx/htmx_infinite_scroll.sc
+++ b/examples/htmx/htmx_infinite_scroll.sc
@@ -1,10 +1,10 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/click-to-load/
import java.util.UUID
-import play.twirl.api.Html
+
import ba.sake.querson.QueryStringRW
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/examples/htmx/htmx_inline_validation.sc b/examples/htmx/htmx_inline_validation.sc
index f8acf66..ba54f1c 100644
--- a/examples/htmx/htmx_inline_validation.sc
+++ b/examples/htmx/htmx_inline_validation.sc
@@ -1,9 +1,8 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/inline-validation/
-import play.twirl.api.Html
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
import ba.sake.formson.FormDataRW
diff --git a/examples/htmx/htmx_lazy_load.sc b/examples/htmx/htmx_lazy_load.sc
index 0b37ff0..e148858 100644
--- a/examples/htmx/htmx_lazy_load.sc
+++ b/examples/htmx/htmx_lazy_load.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
// https://htmx.org/examples/lazy-load/
diff --git a/examples/htmx/htmx_load_snippet.sc b/examples/htmx/htmx_load_snippet.sc
index c7065fc..15bf1ba 100644
--- a/examples/htmx/htmx_load_snippet.sc
+++ b/examples/htmx/htmx_load_snippet.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/examples/htmx/htmx_progress_bar.sc b/examples/htmx/htmx_progress_bar.sc
index 627470c..66f9c62 100644
--- a/examples/htmx/htmx_progress_bar.sc
+++ b/examples/htmx/htmx_progress_bar.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
import java.util.concurrent.TimeUnit
// https://htmx.org/examples/progress-bar/
diff --git a/examples/htmx/htmx_sse.sc b/examples/htmx/htmx_sse.sc
new file mode 100644
index 0000000..0c0b40f
--- /dev/null
+++ b/examples/htmx/htmx_sse.sc
@@ -0,0 +1,51 @@
+//> using scala 3.7.3
+//> using dep ba.sake::sharaf-undertow:0.13.3
+
+import ba.sake.sharaf.{*, given}
+import ba.sake.sharaf.undertow.UndertowSharafServer
+
+val routes = Routes {
+ case GET -> Path() =>
+ Response.withBody(
+ html"""
+
+
+
+
+
+
+
+
+
+
Hello HTMX + SSE!
+
+
+
+
+ """
+ )
+ case GET -> Path("sse-events") =>
+ val sseSender = SseSender()
+ new Thread(() => {
+ for i <- 1 to 5 do
+ sseSender.send(
+ ServerSentEvent.Message(
+ data = html"""
event${i}
""".toString
+ )
+ )
+ Thread.sleep(1_000)
+ sseSender.send(ServerSentEvent.Done())
+ }).start()
+ Response.withBody(sseSender)
+}
+
+UndertowSharafServer("localhost", 8181, routes).start()
+println(s"Server started at http://localhost:8181")
diff --git a/examples/htmx/htmx_tabs_hateoas.sc b/examples/htmx/htmx_tabs_hateoas.sc
index a2fe5f6..c5beaa2 100644
--- a/examples/htmx/htmx_tabs_hateoas.sc
+++ b/examples/htmx/htmx_tabs_hateoas.sc
@@ -1,5 +1,5 @@
//> using scala "3.7.0"
-//> using dep ba.sake::sharaf-undertow:0.12.1
+//> using dep ba.sake::sharaf-undertow:0.13.0
import ba.sake.sharaf.{*, given}
import ba.sake.sharaf.undertow.UndertowSharafServer
diff --git a/examples/http4s/README.md b/examples/http4s/README.md
new file mode 100644
index 0000000..fb98cf7
--- /dev/null
+++ b/examples/http4s/README.md
@@ -0,0 +1,17 @@
+The following steps are done from root of this git repo.
+
+----
+Run from repo root:
+
+```scala
+
+./mill examples.http4s.jvm.run
+
+```
+
+Then in another shell:
+
+```shell
+curl localhost:8080
+# should return "Hello Http4s!"
+```
diff --git a/examples/http4s/src/Main.scala b/examples/http4s/src/Main.scala
new file mode 100644
index 0000000..1740450
--- /dev/null
+++ b/examples/http4s/src/Main.scala
@@ -0,0 +1,23 @@
+import ba.sake.sharaf.*
+import ba.sake.sharaf.http4s.*
+
+import cats.effect.*
+import com.comcast.ip4s.*
+import org.http4s.ember.server.*
+
+val routes = Routes {
+ case GET -> Path("hello", name) =>
+ Response.withBody(s"Hello ${name}!")
+ case _ =>
+ Response.withBody("Hello Http4s!")
+}
+
+object Main extends IOApp.Simple:
+ def run =
+ EmberServerBuilder
+ .default[cats.effect.IO]
+ .withHost(ipv4"0.0.0.0")
+ .withPort(port"8080")
+ .withHttpApp(SharafHttpApp(SharafHandler.routes(routes)))
+ .build
+ .useForever
diff --git a/examples/jwt/README.md b/examples/jwt/README.md
new file mode 100644
index 0000000..e14472b
--- /dev/null
+++ b/examples/jwt/README.md
@@ -0,0 +1,17 @@
+
+
+```shell
+# public route
+curl localhost:8181
+
+# should return 401 Unauthorized when JWT is not provided or is invalid
+curl localhost:8181/protected
+
+# should return 200 OK
+curl localhost:8181/protected -H "Authorization: eyJhbGc....."
+```
+
+
+
+
+
diff --git a/examples/jwt/src/Main.scala b/examples/jwt/src/Main.scala
new file mode 100644
index 0000000..48a5c0e
--- /dev/null
+++ b/examples/jwt/src/Main.scala
@@ -0,0 +1,92 @@
+package jwt
+
+import java.util.Optional
+import scala.jdk.CollectionConverters.*
+import io.undertow.Undertow
+import org.pac4j.core.client.Clients
+import org.pac4j.core.config.Config
+import org.pac4j.http.client.direct.HeaderClient
+import org.pac4j.undertow.handler.SecurityHandler
+import org.pac4j.core.context.WebContext
+import org.pac4j.core.context.session.SessionStore
+import org.pac4j.core.matching.matcher.{DefaultMatchers, PathMatcher}
+import org.pac4j.core.profile.BasicUserProfile
+import org.pac4j.jwt.config.signature.SecretSignatureConfiguration
+import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator
+import org.pac4j.jwt.profile.JwtGenerator
+import ba.sake.sharaf.*
+import ba.sake.sharaf.undertow.*
+
+// TODO add a test
+
+@main def main(): Unit =
+ val module = JwtModule(8181)
+ module.server.start()
+ println(s"Started HTTP server at ${module.baseUrl}")
+
+class JwtModule(port: Int) {
+
+ val baseUrl = s"http://localhost:${port}"
+
+ private val signatureConfiguration = new SecretSignatureConfiguration("your_jwt_secret_key_that_is_at_least_32_chars")
+ private val jwtAuthenticator = JwtAuthenticator(signatureConfiguration)
+ private val headerClient = new HeaderClient("Authorization", jwtAuthenticator)
+
+ // generate a JWT claims set for testing purposes
+ locally {
+ val up = BasicUserProfile()
+ up.setId("12345")
+ val jwt = JwtGenerator(signatureConfiguration).generate(up)
+ println(s"Use this JWT: ${jwt}")
+ }
+
+ private val clients = Clients("/callback", headerClient)
+ private val pac4jConfig = Config(clients)
+ // use noop session store, JWTs are stateless
+ pac4jConfig.setSessionStoreFactory(_ => NoopSessionStore())
+ private val clientNames = clients.getClients.asScala.map(_.getName()).toSeq
+ private val publicRoutesMatcher = PathMatcher()
+ private val publicRoutesMatcherName = "publicRoutesMatcher"
+ publicRoutesMatcher.excludePaths("/")
+ pac4jConfig.addMatcher(publicRoutesMatcherName, publicRoutesMatcher)
+ private val securityHandler =
+ SecurityHandler.build(
+ UndertowExceptionHandler(
+ ExceptionMapper.default,
+ SharafUndertowHandler(SharafHandler.routes(Routes {
+ case GET -> Path() =>
+ Response.withBody("Hello there! This is a public endpoint. Try accessing localhost:8181/protected.")
+ case GET -> Path("protected") =>
+ Response.withBody("This is a protected resource. You are authenticated.")
+ }))
+ ),
+ pac4jConfig,
+ clientNames.mkString(","),
+ null,
+ s"${DefaultMatchers.SECURITYHEADERS},${publicRoutesMatcherName}"
+ )
+
+ val server = Undertow
+ .builder()
+ .addHttpListener(port, "localhost")
+ .setHandler(securityHandler)
+ .build()
+}
+
+// A no-op session store implementation that does not store any session data
+class NoopSessionStore extends SessionStore {
+ override def getTrackableSession(context: WebContext): Optional[AnyRef] = Optional.empty()
+
+ override def buildFromTrackableSession(context: WebContext, trackableSession: Any): Optional[SessionStore] =
+ Optional.empty()
+
+ override def getSessionId(context: WebContext, createSession: Boolean): Optional[String] = Optional.empty()
+
+ override def get(context: WebContext, key: String): Optional[AnyRef] = Optional.empty()
+
+ override def set(context: WebContext, key: String, value: Any): Unit = ()
+
+ override def destroySession(context: WebContext): Boolean = false
+
+ override def renewSession(context: WebContext): Boolean = false
+}
diff --git a/examples/oauth2/src/AppModule.scala b/examples/oauth2/src/AppModule.scala
index 2adca43..2dd3b02 100644
--- a/examples/oauth2/src/AppModule.scala
+++ b/examples/oauth2/src/AppModule.scala
@@ -6,6 +6,7 @@ import io.undertow.server.HttpHandler
import io.undertow.server.session.InMemorySessionManager
import io.undertow.server.session.SessionAttachmentHandler
import io.undertow.server.session.SessionCookieConfig
+import io.undertow.server.handlers.BlockingHandler
import org.pac4j.core.client.Clients
import org.pac4j.undertow.handler.CallbackHandler
import org.pac4j.undertow.handler.LogoutHandler
@@ -24,25 +25,30 @@ class AppModule(port: Int, clients: Clients) {
private val httpHandler: HttpHandler = locally {
val securityHandler =
SecurityHandler.build(
- SharafUndertowHandler(SharafHandler.routes(appRoutes.routes)),
+ SharafUndertowHandler(
+ SharafHandler.exceptions(
+ SharafHandler.routes(appRoutes.routes)
+ )
+ ),
securityConfig.pac4jConfig,
securityConfig.clientNames.mkString(","),
null,
- securityConfig.matchers,
- CustomSecurityLogic()
+ securityConfig.matchers
)
val pathHandler = Handlers
.path()
- .addExactPath("/callback", CallbackHandler.build(securityConfig.pac4jConfig, null, CustomCallbackLogic()))
+ .addExactPath("/callback", CallbackHandler.build(securityConfig.pac4jConfig))
.addExactPath("/logout", LogoutHandler(securityConfig.pac4jConfig, "/"))
.addPrefixPath("/", securityHandler)
- SessionAttachmentHandler(pathHandler, InMemorySessionManager("SessionManager"), SessionCookieConfig())
+ BlockingHandler(
+ SessionAttachmentHandler(pathHandler, InMemorySessionManager("SessionManager"), SessionCookieConfig())
+ )
}
val server = Undertow
.builder()
- .addHttpListener(port, "0.0.0.0", httpHandler)
+ .addHttpListener(port, "localhost", httpHandler)
.build()
}
diff --git a/examples/oauth2/src/AppRoutes.scala b/examples/oauth2/src/AppRoutes.scala
index 724a78b..6c1aea3 100644
--- a/examples/oauth2/src/AppRoutes.scala
+++ b/examples/oauth2/src/AppRoutes.scala
@@ -25,7 +25,7 @@ def IndexPage(userOpt: Option[CustomUserProfile]) =