From d3eb7a962ea793a5dbff362f7211085aa8f461d1 Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Tue, 18 Jul 2023 11:37:47 +0200 Subject: [PATCH] Nice doc :) --- docs/modules/ROOT/nav.adoc | 24 +- docs/modules/ROOT/pages/advanced.adoc | 755 ++++++++++ docs/modules/ROOT/pages/concepts.adoc | 475 +++++++ docs/modules/ROOT/pages/config-reference.adoc | 7 + docs/modules/ROOT/pages/index.adoc | 1222 +---------------- docs/modules/ROOT/pages/security.adoc | 5 +- 6 files changed, 1266 insertions(+), 1222 deletions(-) create mode 100644 docs/modules/ROOT/pages/advanced.adoc create mode 100644 docs/modules/ROOT/pages/concepts.adoc create mode 100644 docs/modules/ROOT/pages/config-reference.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 1fa51146..7fa3023d 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1 +1,23 @@ -* xref:index.adoc[Quarkus Renarde] +include::./includes/attributes.adoc[] +:config-file: application.properties + +* xref:index.adoc[Getting started] +* xref:concepts.adoc[Main Concepts] +** xref:concepts.adoc#models[Models] +** xref:concepts.adoc#controllers[Controllers] +** xref:concepts.adoc#views[Views] +* xref:advanced.adoc[Advanced Guides] +** xref:advanced.adoc#forms[Forms] +** xref:advanced.adoc#routing[Routing] +** xref:advanced.adoc#emails[Emails] +** xref:advanced.adoc#localisation[Localisation / Internationalisation] +** xref:advanced.adoc#flash_scope[Flash Scope] +** xref:advanced.adoc#htmx[htmx] +** xref:advanced.adoc#generating_barcodes[Generating barcodes] +** xref:advanced.adoc#generating_pdf_documents[Generating PDF] +* xref:security.adoc[Security] +** xref:security.adoc#_csrf[CSRF] +** xref:security.adoc#_custom_authentication_with_jwt[Custom Auth (JWT)] +** xref:security.adoc#oidc[OIDC] +* xref:config-reference.adoc[Config Reference] + diff --git a/docs/modules/ROOT/pages/advanced.adoc b/docs/modules/ROOT/pages/advanced.adoc new file mode 100644 index 00000000..122927df --- /dev/null +++ b/docs/modules/ROOT/pages/advanced.adoc @@ -0,0 +1,755 @@ += Renarde image:renarde-head.svg[width=25em] Web Framework - Advanced +:favicon: _images/renarde-head.svg + +include::./includes/attributes.adoc[] +:config-file: application.properties + + +[#forms] +== Forms + +A lot of the time, you need to send data from the browser to your endpoints, which is often done with forms. + +=== The HTML form + +Creating forms in Renarde is easy: let's see an example of how to do it in Qute: + +[source,html] +---- +{#form uri:Login.complete(newUser.confirmationCode)} + +
+ Complete registration for {newUser.email} + {#formElement name="userName" label="User Name"} + {#input name="userName"/} + {/formElement} + {#formElement name="password" label="Password"} + {#input name="password" type="password"/} + {/formElement} + {#formElement name="password2" label="Password Confirmation"} + {#input name="password2" type="password"/} + {/formElement} + {#formElement name="firstName" label="First Name"} + {#input name="firstName"/} + {/formElement} + {#formElement name="lastName" label="Last Name"} + {#input name="lastName"/} + {/formElement} + +
+ +{/form} +---- + +Here we're defining a form whose action will go to `Register.complete(newUser.confirmationCode)` and +which contains several form elements, which are just tags to make composition easier. For example `formElement` is +a custom Qute tag for Bootstrap which defines layout for the form element and displays any associated error: + +[source,html] +---- +
+ + {nested-content} + {#ifError name} + ​{#error name/}​ + {/ifError} +
+---- + +The `input` user tag is also designed for Bootstrap as an abstraction: + +[source,html] +---- + +---- + +As you can see, we have default values for certain attributes, a special error class if there is a validation +error, and we default the value to the one preserved in the flash scope, which is filled whenever validation +fails, so that the user can see the validation error without losing their form values. + +As for the `form` Renarde tag, it is also fairly simple, and only includes an authenticity token for CSRF protection. + +[source,html] +---- +
+ {#authenticityToken/} + {nested-content} +
+---- + +=== The endpoint + +Most forms will be a `@POST` endpoint, with each form element having a corresponding parameter annotated with `@RestForm`. + +[source,java] +---- +@POST +public void complete(@RestQuery String confirmationCode, + @RestForm String userName, + @RestForm String password, + @RestForm String password2, + @RestForm String firstName, + @RestForm String lastName) { + // do something with the form parameters +} +---- + +You can also group parameters in a POJO, but for now you have to add a special +`@Consumes(MediaType.MULTIPART_FORM_DATA)` annotation: + +[source,java] +---- +@Consumes(MediaType.MULTIPART_FORM_DATA) +@POST +public void complete(@RestQuery String confirmationCode, + FormData form) { + // do something with the form parameters +} + +public static class FormData { + @RestForm String userName; + @RestForm String password; + @RestForm String password2; + @RestForm String firstName; + @RestForm String lastName; +} +---- + +Check out the {quarkus-guides-url}/resteasy-reactive#handling-multipart-form-data[RESTEasy Reactive documentation] +for more information about form parameters and multi-part. + +=== Validation + +You can place your usual {quarkus-guides-url}/validation[Hibernate Validation] annotations on the controller methods that receive user data, but +keep in mind that you have to check for validation errors in your method before you do any action that modifies your state. +This allows you to check more things than you can do with just annotations, with richer logic: + +[source,java] +---- +@POST +public Response complete(@RestQuery String confirmationCode, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String userName, + @RestForm @NotBlank @Length(min = 8, max = Util.VARCHAR_SIZE) String password, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String password2, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String firstName, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String lastName) { + // Find the user for this confirmation code + User user = User.findForContirmation(confirmationCode); + if(user == null){ + validation.addError("confirmationCode", "Invalid confirmation code"); + } + + // Make sure the passwords match + validation.equals("password", password, password2); + + // Make sure the username is free + if(User.findByUserName(userName) != null){ + validation.addError("userName", "User name already taken"); + } + + // If validation failed, redirect to the confirm page + if(validationFailed()){ + confirm(confirmationCode); + } + + // Now proceed to complete user registration + ... +} +---- + +You can use the `validation` object to trigger additional validation logic and collect errors. + +Those errors are then placed in the _flash_ scope by a call to `validationFailed()` if there +are any errors, and thus preserved when you redirect from your action method to the `@GET` method +that holds the submitted form, which you can then access in your views using the `{#ifError field}{/ifError}` +conditional tag, or the `{#error field/}` tag which accesses the error message for the given field. + +[#routing] +== Routing, URI mapping, redirects + +We have seen how to declare endpoints and how URIs map to them, but very often we need to map from endpoints to +URIs, which Renarde makes easy. + +=== Redirects after POST + +When handling a `@POST`, `@PUT` or `@DELETE` endpoint, it's good form to redirect to a `@GET` endpoint after +the action has been done, in order to allow the user to reload the page without triggering the action a second +time, and such redirects are simply done by calling the corresponding `@GET` endpoint. In reality, the endpoint +will not be called and will be replaced by a redirect that points to the endpoint in question. + +[source,java] +---- +package rest; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkiverse.renarde.Controller; + +public class Application extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(); + } + + @Path("/") + public TemplateInstance index() { + return Templates.index(); + } + + @POST + public void someAction() { + // do something + ... + // redirect to the index page + index(); + } +} +---- + +If there are any parameters that form the URI, you must pass them along: + +[source,java] +---- +package rest; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkiverse.renarde.Controller; + +public class Application extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(); + } + + @Path("/") + public TemplateInstance index() { + return Templates.index(); + } + + public TemplateInstance somePage(@RestPath String id, @RestQuery String q) { + // do something with the id and q + return Templates.index(); + } + + @POST + public void someAction() { + // do something + ... + // redirect to the somePage page + somePage("foo", "bar"); + } +} +---- + +If you want to redirect to another controller, you can use the `redirect(Class)` method: + +[source,java] +---- +package rest; + +import jakarta.ws.rs.POST; + +import io.quarkiverse.renarde.Controller; + +public class Application extends Controller { + + @POST + public void someAction() { + // do something + ... + // redirect to the Todos.index() endpoint + redirect(Todos.class).index(); + } +} +---- + +=== Obtaining a URI in endpoints + +If you don't want a redirect but need a URI to a given endpoint, you can use the `Router.getURI` +or `Router.getAbsoluteURI` methods, by +passing them a method reference to the endpoint you want and the required parameters: + +[source,java] +---- +package rest; + +import java.net.URI; + +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkiverse.renarde.Controller; +import io.quarkiverse.renarde.router.Router; + +public class Application extends Controller { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance somePage(); + public static native TemplateInstance otherPage(URI uri); + } + + public TemplateInstance somePage(@RestPath String foo, @RestQuery Long bar) { + return Templates.somePage(); + } + + public TemplateInstance otherPage() { + // Obtain a URI to somePage + URI uri = Router.getURI(Login::somePage, "something", 23l); + // pass it on to our view + return Templates.otherPage(uri); + } +} +---- + +NOTE: If you plan on using `Response.seeOther` or `Controller.seeOther`, make sure to use the `Router.getAbsoluteURI` +variant, especially if you use the `quarkus.http.root-path` configuration, otherwise your URIs contain that prefix twice. + +=== Obtaining a URI in Qute views + +If you want a URI to an endpoint in a Qute view, you can use the `uri` and `uriabs` namespace with a +call to the endpoint you want to point to: + +[source,html] +---- +Todo +---- + +Naturally, you can also pass any required parameters. + +[#emails] +== Emails + +[source,xml] +---- + + io.quarkus + quarkus-mailer + +---- + +Often you will need your actions to send email notifications.You can use Qute for this too, by declaring your +emails in an `Emails` class: + +[source,java] +---- +package email; + +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.qute.CheckedTemplate; +import model.User; + +public class Emails { + + private static final String FROM = "Todos "; + private static final String SUBJECT_PREFIX = "[Todos] "; + + @CheckedTemplate + static class Templates { + public static native MailTemplateInstance confirm(User user); + } + + public static void confirm(User user) { + Templates.confirm(user) + .subject(SUBJECT_PREFIX + "Please confirm your email address") + .to(user.email) + .from(FROM) + .send().await().indefinitely(); + } +} +---- + +You can then send the email from your endpoint by calling `Emails.confirm(user)`. + +You can use composition for emails too, by having a pair of base templates for HTML in +`src/main/resources/templates/email.html`: + +[source,html] +---- + + + + + + + + {#insert /} +

+ This is an automated email, you should not reply to it: your mail will be ignored. +

+ + +---- + +And for text in `src/main/resources/templates/email.txt`: + +[source,txt] +---- +{#insert /} + +This is an automated email, you should not reply to it: your mail will be ignored. +---- + +You can then use those templates in your emails in `src/main/resources/templates/Emails/confirm.html`: + +[source,html] +---- +{#include email.html } + +

+ Welcome to Todos. +

+ +

+ You received this email because someone (hopefully you) wants to register on Todos. +

+ +

+ If you don't want to register, you can safely ignore this email. +

+ +

+ If you want to register, complete your registration. +

+{/include} +---- + +And for text in `src/main/resources/templates/Emails/confirm.txt`: + +[source,txt] +---- +{#include email.txt} + +Welcome to Todos. + +You received this email because someone (hopefully you) wants to register on Todos. + +If you don't want to register, you can safely ignore this email. + +If you want to register, complete your registration by going to the following address: + +{uriabs:Login.confirm(user.confirmationCode)} +{/include} +---- + +Note that in emails you will want to use the `uriabs` namespace for absolute URIs and not relative ones, +otherwise the links won't work for your email recipients. + +You can find more information in the {quarkus-guides-url}/mailer-reference[Quarkus mailer documentation]. + +[#localisation] +== Localisation / Internationalisation + +You can declare your default language and supported languages in `src/main/resources/application.properties`: + +[source,properties] +---- +# This is the default locale for your application +quarkus.default-locale=en +# These are the supported locales (should include the default locale, but order is not important) +quarkus.locales=en,fr +---- + +Next, you can declare your default language messages in the `src/main/resources/message.properties` file: + +[source,properties] +---- +# A simple message +hello=Hello World +# A parameterised message for your view +views_Application_index_greet=Hello %s +---- + +Declare your other language translations in the `src/main/resources/message_fr.properties` file: + +[source,properties] +---- +hello=Bonjour Monde +views_Application_index_greet=Salut %s +---- + +Now you can use these translated messages in your controller: + +[source,java] +---- +public static class Application extends Controller { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance index(String name); + } + + public TemplateInstance index() { + return Templates.index("Stef"); + } + + public String hello() { + return i18n.formatMessage("hello"); + } +} +---- + +Or in your template: + +[source,txt] +---- +With no parameter: +{m:hello} +With parameters: +{m:views_Application_index_greet(name)} +---- + +=== Selecting the language + +The current language for a request will depend on the following (in order): + +. The `_renarde_language` cookie, if set +. The https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language[`Accept-Language`] HTTP header, if set, +which defines an ordered list of languages by user preference. We will select a best matching language from the set +of `quarkus.locales`. +. If nothing else, we default to the default language as set by `quarkus.default-locale`, which defaults to `en_US`. + +You can override the user's language with a cookie by calling `i18n.set(language)`: + +[source,java] +---- +public static class Application extends Controller { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance index(); + } + + public void index() { + return Templates.index(); + } + + @POST + public void french() { + i18n.set("fr"); + index(); + } + + @POST + public void english() { + i18n.set("en"); + index(); + } +} +---- + +[#flash_scope] +== Flash scope + +If you need to pass values from one endpoint to another request after a redirect, you can use the Flash scope. +Usually this is done in a `@POST` endpoint, by filling the Flash scope with either errors or messages, +before trigerring a redirect to the right `@GET` endpoint. + +You can push values in the Flash scope in an endpoint using the `flash(name, value)` method, or using the +`Flash` injectable component. + +You can read values from the Flash scope in your Qute views using the `{flash:name}` namespace. + +The Flash scope only lasts from one request to the next and is cleared at each request. + +[#htmx] +== htmx + +If you want to use https://htmx.org/[htmx] with Renarde, we added an extra layer to make things more convenient: +[source,java] +---- +public class Application extends HxController { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance foo(int a, int b); + public static native TemplateInstance foo$sectionA(int a); // <1> + public static native TemplateInstance foo$sectionB(int b); // <1> + + public static native TemplateInstance bar(); + } + + public TemplateInstance foo() { + if (isHxRequest()) { // <2> + return concatTemplates( // <3> + Templates.foo$sectionA(1), + Templates.foo$sectionB(2) + ); + } + return Templates.foo(1, 2); + } + + public TemplateInstance bar() { + onlyHxRequest(); // <4> + this.hx(HxResponseHeader.TRIGGER, "refresh"); // <5> + return Templates.bar(); + } +} +---- +<1> {quarkus-guides-url}/qute-reference#fragments[Qute fragments] declarations +<2> Check if this is a htmx request by looking for the `HX-Request` header or using flash data for redirects +<3> https://htmx.org/attributes/hx-swap-oob/[Out of band swap] with different templates or {quarkus-guides-url}/qute-reference#fragments[fragments] +<4> Only Hx requests are allowed, else it will fail with a BAD_REQUEST error +<5> Flag the response with an https://htmx.org/reference/#response_headers[htmx response header] + +For CSRF Security, you need a form parameter with the CSRF Token. By adding this 👇 when doing a hx-post/put/delete, The Hx requests will be sent with the CSRF parameter: +[source,html] +---- +
+---- + +NOTE: There is a ongoing issue to allow using a header instead of a form parameter (https://github.com/quarkusio/quarkus/issues/34513), this way it will be possible to have a `hx-headers` on the to make all hx requests secured with CSRF. + +Some example projects with Quarkus Renarde and htmx: +- https://github.com/ia3andy/renotes[a demo note-taking web app] +- https://github.com/ia3andy/quarkus-blast[a board game] + +[#generating_barcodes] +== Generating barcodes + +If you import the optional `quarkus-renarde-barcode` module, you can generate barcodes in your views or controllers. + +[source,xml,subs=attributes+] +---- + + io.quarkiverse.renarde + quarkus-renarde-barcode + {quarkus-renarde-version} + +---- + +We support the following barcode types, with custom Qute tags: + +[cols="1,1"] +|=== +|Tag|Description + +|`{#ean13 value/}` +|Generate an https://en.wikipedia.org/wiki/International_Article_Number[EAN 13] barcode. + +|`{#ean8 value/}` +|Generate an https://en.wikipedia.org/wiki/EAN-8[EAN 8] barcode. + +|`{#upca value/}` +|Generate an https://en.wikipedia.org/wiki/Universal_Product_Code[UPC A] barcode. + +|`{#upce value/}` +|Generate an https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E[UPC E] barcode. + +|`{#code39 value/}` +|Generate a https://en.wikipedia.org/wiki/Code_39[Code 39] barcode. + +|`{#code93 value/}` +|Generate a https://en.wikipedia.org/wiki/Code_93[Code 93] barcode. + +|`{#code128 value/}` +|Generate a https://en.wikipedia.org/wiki/Code_128[Code 128] barcode. + +|`{#qrcode value/}` +|Generate a https://en.wikipedia.org/wiki/QR_code[QR Code] barcode. + +|`{#datamatrix value/}` +|Generate a https://en.wikipedia.org/wiki/Data_Matrix[Data Matrix] barcode. + +|=== + +All these tags accept an optional `size` parameter that sets both the width and height of the barcode +in pixels, or `width` and `height` parameters to specify different values.These values all default +to `200` pixels. + +The generated barcodes will be inline HTML images using the https://en.wikipedia.org/wiki/Data_URI_scheme[Data URI] +scheme as an embedded https://en.wikipedia.org/wiki/PNG[PNG] image. + +If you wish to generate barcode images as PNG bytes or other formats, you can use the `io.quarkiverse.renarde.barcode.Barcode` +helper class which allows you to generate bytecodes from your controllers. + +[#generating_pdf_documents] +== Generating PDF documents + +If you import the optional `quarkus-renarde-pdf` module, you can generate PDF documents in your views. + +[source,xml,subs=attributes+] +---- + + io.quarkiverse.renarde + quarkus-renarde-pdf + {quarkus-renarde-version} + +---- + +There are lots of complicated ways to generate PDFs, but the simplest is to use the versatility of HTML for rendering +and its printing support. So, very much like you can print HTML files using your browser, and there are special rendering +rules in CSS, you do the same in Renarde PDF: you render your page in HTML, and simply add `@Produces(Pdf.APPLICATION_PDF)` +to your controller to produce a PDF file: + +[source,java] +---- +public class Application extends Controller { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance page(); + } + + // This will return a PDF file to the caller + @Produces(Pdf.APPLICATION_PDF) + public TemplateInstance pdf() { + return Templates.page(); + } + + // This will return HTML to the caller + public TemplateInstance html() { + return Templates.page(); + } +} +---- + +This way, the http://localhost:8080/Application/pdf and http://localhost:8080/Application/html +pages will render the same document in either PDF or HTML, which allows you to use your browser and its +developer tools to fine-tweak your document, until you're ready to turn it into a PDF, which is +much harder to fine-tweak. + +As for the `templates/Application/page.html` template, it's regular HTML, but you can also +take advantage of the https://developer.mozilla.org/en-US/docs/Web/CSS/Paged_Media[CSS print support] +to set things like document page size, odd/even page margins, etc…: + +[source,html] +---- + + + + This is the PDF document title + + + + Some PDF text. + + +---- + +Note that the default page size is https://en.wikipedia.org/wiki/ISO_216#A_series[A4]. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/concepts.adoc b/docs/modules/ROOT/pages/concepts.adoc new file mode 100644 index 00000000..a957e07e --- /dev/null +++ b/docs/modules/ROOT/pages/concepts.adoc @@ -0,0 +1,475 @@ += Renarde image:renarde-head.svg[width=25em] Web Framework - Main Concepts +:favicon: _images/renarde-head.svg + + +[#models] +== Models + +By convention, you can place your model classes in the `model` package, but anywhere else works just as well.We +recommend using {quarkus-guides-url}/hibernate-orm-panache[Hibernate ORM with Panache].Here's an example entity for our sample Todo application: + +[source,java] +---- +package model; + +import java.util.Date; +import java.util.List; + +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; + +@Entity +public class Todo extends PanacheEntity { + + @ManyToOne + public User owner; + + public String task; + + public boolean done; + + public Date doneDate; + + public static List findByOwner(User user) { + return find("owner = ?1 ORDER BY id", user).list(); + } +} +---- + +[#controllers] +== Controllers + +By convention, you can place your controllers in the `rest` package, but anywhere else works just as well.You +have to extend the `Controller` class in order to benefit from extra easy endpoint declarations and reverse-routing, +but that superclass also gives you useful methods.We usually have one controller per model class, so we tend to use +the plural entity name for the corresponding controller: + +[source,java] +---- +package rest; + +import java.util.Date; +import java.util.List; + +import jakarta.validation.constraints.NotBlank; +import jakarta.ws.rs.POST; + +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.RestPath; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import model.Todo; + +public class Todos extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(List todos); + } + + public TemplateInstance index() { + // list every todo + List todos = Todo.listAll(); + // render the index template + return Templates.index(todos); + } + + @POST + public void delete(@RestPath Long id) { + // find the Todo + Todo todo = Todo.findById(id); + notFoundIfNull(todo); + // delete it + todo.delete(); + // send loving message + flash("message", "Task deleted"); + // redirect to index page + index(); + } + + @POST + public void done(@RestPath Long id) { + // find the Todo + Todo todo = Todo.findById(id); + notFoundIfNull(todo); + // switch its done state + todo.done = !todo.done; + if(todo.done) + todo.doneDate = new Date(); + // send loving message + flash("message", "Task updated"); + // redirect to index page + index(); + } + + @POST + public void add(@NotBlank @RestForm String task) { + // check if there are validation issues + if(validationFailed()) { + // go back to the index page + index(); + } + // create a new Todo + Todo todo = new Todo(); + todo.task = task; + todo.persist(); + // send loving message + flash("message", "Task added"); + // redirect to index page + index(); + } +} +---- + +=== Methods + +Every public method is a valid endpoint. If it has no HTTP method annotation (`@GET`, `@HEAD`, `@POST`, `@PUT`, `@DELETE`) then +it is assumed to be a `@GET` method. + +Most `@GET` methods will typically return a `TemplateInstance` for rendering an HTML server-side template, and should not +modify application state. + +Controller methods annotated with `@POST`, `@PUT` and `@DELETE` will typically return `void` and trigger a redirect to a `@GET` +method after they do their action. This is not mandatory, you can also return a `TemplateInstance` if you want, but it is good form +to use a redirect to avoid involuntary actions when browsers reload the page. Those methods also get an implicit `@Transactional` +annotation so you don't need to add it. + +If your controller is not annotated with `@Path` it will default to a path using the class name. If your controller method is not +annotated with `@Path` it will default to a path using the method name. The exception is if you have a `@Path` annotation on the +method with an absolute path, in which case the class path part will be ignored. Here's a list of example annotations and how they +result: + +[cols="1,1,1"] +|=== +|Class declaration|Method declaration|URI + +|`class Foo` +|`public TemplateInstance bar()` +|`Foo/bar` + +|`@Path("f") class Foo` +|`public TemplateInstance bar()` +|`f/bar` + +|`class Foo` +|`@Path("b") public TemplateInstance bar()` +|`Foo/b` + + +|`@Path("f") class Foo` +|`@Path("b") public TemplateInstance bar()` +|`f/b` + +|`class Foo` +|`@Path("/bar") public TemplateInstance bar()` +|`bar` + +|`@Path("f") class Foo` +|`@Path("/bar") public TemplateInstance bar()` +|`f/bar` + +|=== + +Furthermore, if you specify path parameters that are not present in your path annotations, they will be automatically +appended to your path: + +[source,java] +---- +public class Orders extends Controller { + + // The URI will be Orders/get/{owner}/{id} + public TemplateInstance get(@RestPath String owner, @RestPath Long id) { + } + + // The URI will be /orders/{owner}/{id} + @Path("/orders") + public TemplateInstance otherGet(@RestPath String owner, @RestPath Long id) { + } +} +---- + +[#views] +== Views + +You can place your {quarkus-guides-url}/qute-reference[Qute views] in the `src/main/resources/templates` folder, +using the `pass:[{className}/{methodName}].html` naming convention. + +Every controller that has views should declare them with a nested static class annotated with `@CheckedTemplate`: + +[source,java] +---- +public class Todos extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(List todos); + } + + public TemplateInstance index() { + // list every todo + List todos = Todo.listAll(); + // render the index template + return Templates.index(todos); + } +} +---- + +Here we're declaring the `Todos/index.html` template, specifying that it takes a `todos` parameter of type +`List` which allows us to validate the template at build-time. + +Templates are written in Qute, and you can also declare imported templates in order to validate them using a +toplevel class, such as the `main.html` template: + +[source,java] +---- +package rest; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; + +@CheckedTemplate +public class Templates { + public static native TemplateInstance main(); +} +---- + +=== Template composition + +Typical web applications will have a main template for their layout and use composition in every method. For example, we +can declare the following main template in `main.html`: + +[source,html] +---- + + + + {#insert title /} + + + {#insert moreStyles /} + + {#insert moreScripts /} + + + {#insert /} + + +---- + +And then use it in our `Todos/index.html` template to list the todo items: + +[source,html] +---- +{#include main.html } +{#title}Todos{/title} + + + + + + + + + + {#for todo in todos} + + + + + {/for} + +
#Task
{todo.id}{todo.task}
+ +{/include} +---- + +=== Standard tags + +[cols="1,1"] +|=== +|Tag|Description + +|{quarkus-guides-url}/qute-reference#loop_section[for/each] +|Iterate over collections + +|{quarkus-guides-url}/qute-reference#if_section[if/else] +|Conditional statement + +|{quarkus-guides-url}/qute-reference#when_section[switch/case] +|Switch statement + +|{quarkus-guides-url}/qute-reference#with_section[with] +|Adds value members to the local scope + +|{quarkus-guides-url}/qute-reference#letset_section[let] +|Declare local variables + +|{quarkus-guides-url}/qute-reference#include_helper[include/insert] +|Template composition + +|=== + +=== User tags + +If you want to declare additional tags in order to be able to repeat them in your templates, simply place them in the +`templates/tags` folder. For example, here is our `user.html` tag: + +[source,html] +---- + +{#if img??} +{#gravatar it.email size=size.or(20) default='mm' /} +{/if} +{it.userName} +---- + +Which allows us to use it in every template: + +[source,html] +---- +{#if inject:user} + {#if inject:user.isAdmin}{/if} + {#user inject:user img=true size=20/} +{/if} +---- + +You can pass parameters to your template with `name=value` pairs, and the first unnamed parameter value becomes available +as the `it` parameter. + +See the {quarkus-guides-url}/qute-reference#user_tags[Qute documentation] for more information. + +=== Renarde tags + +Renarde comes with a few extra tags to make your life easier: + +[cols="1,1"] +|=== +|Tag|Description + +|`{#authenticityToken/}` +|Generate a hidden HTML form element containing a xref:security.adoc#csrf[CSRF] token to be matched in the next request. + +|`{#error 'field'/}` +|Inserts the error message for the given field name + +|`{#form uri method='POST' class='css' id='id'}...{/form}` +|Generates an HTML form for the given `URI`, `method` (defaults to `POST`) and optional CSS classes and IDs. +Includes a xref:security.adoc#csrf[CSRF] token. + +|`{#gravatar email size='mm'/}` +|Inserts a gravatar image for the given `email`, with optional `size` (defaults to `mm`) + +|`{#ifError 'field'}...{/ifError}` +|Conditional statement executed if there is an error for the given field + +|=== + +=== Extension methods + +If you need additional methods to be registered to be used on your template expressions, you can declare static methods in +a class annotated with `@TemplateExtension`: + +[source,java] +---- +package util; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import io.quarkus.qute.TemplateExtension; + +@TemplateExtension +public class JavaExtensions { + + public static boolean isRecent(Date date){ + Date now = new Date(); + Calendar cal = new GregorianCalendar(); + cal.add(Calendar.MONTH, -6); + Date sixMonthsAgo = cal.getTime(); + return date.before(now) && date.after(sixMonthsAgo); + } + +} +---- + +This one declares an additional method on the `Date` type, allowing you to test whether a date is recent or not: + +[source,html] +---- +{#if todo.done && todo.doneDate.isRecent()} + This was done recently! +{/if} +---- + +=== Renarde extension methods + +[cols="1,1,1"] +|=== +|Target type|Method|Description + +|`Date` +|`format()` +|Formats the date to the `dd/MM/yyyy` format + +|`Date` +|`internetFormat()` +|Formats the date to the `yyyy-MM-dd` format + +|`Date` +|`future()` +|Returns `true` if the date is in the future + +|`Date` +|`since()` +|Formats the date in terms of `X seconds/minutes/hours/days/months/years ago` + +|`String` +|`md5()` +|Returns an MD5 hash of the given string + +|`Object` +|`instanceOf(className)` +|Returns true if the given object is exactly of the specified class name + +|=== + + +=== External CSS, JavaScript libraries + +You can use webjars to provide third-party JavaScript or CSS. For example, here is how you can import Bootstrap +and Bootstrap-icons in your `pom.xml`: + +[source,xml] +---- + + org.webjars + bootstrap + 5.1.3 + + + org.webjars.npm + bootstrap-icons + 1.7.0 + + + io.quarkus + quarkus-webjars-locator + +---- + +After that, you can include them in your Qute templates with: + +[source,html] +---- + + + + + +---- + +Look at https://mvnrepository.com/artifact/org.webjars for the list of available options. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/config-reference.adoc b/docs/modules/ROOT/pages/config-reference.adoc new file mode 100644 index 00000000..0866bdf1 --- /dev/null +++ b/docs/modules/ROOT/pages/config-reference.adoc @@ -0,0 +1,7 @@ += Renarde image:renarde-head.svg[width=25em] Web Framework Config +:favicon: _images/renarde-head.svg + +[[extension-configuration-reference]] +== Configuration Reference + +include::includes/quarkus-web-bundler.adoc[leveloffset=+1, opts=optional] diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index dcc9e1c5..e80d88d3 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -1,4 +1,4 @@ -= Renarde image:renarde-head.svg[width=25em] Web Framework += Renarde image:renarde-head.svg[width=25em] Web Framework - Getting Started :favicon: _images/renarde-head.svg include::./includes/attributes.adoc[] @@ -11,6 +11,7 @@ image::renarde.svg[alt=Renarde,width=100,float="right"] Renarde image:renarde-head.svg[width=15em] is a server-side Web Framework based on Quarkus, {quarkus-guides-url}/qute-reference[Qute], {quarkus-guides-url}/hibernate-orm-panache[Hibernate] and {quarkus-guides-url}/resteasy-reactive[RESTEasy Reactive]. + [source,xml,subs=attributes+] ---- @@ -69,1222 +70,3 @@ We can then define the main page in `src/main/resources/templates/Application/in ---- Now if you navigate to your application at http://localhost:8080 you will see `Hello, World!` rendered. - -== Models - -By convention, you can place your model classes in the `model` package, but anywhere else works just as well. We -recommend using {quarkus-guides-url}/hibernate-orm-panache[Hibernate ORM with Panache]. Here's an example entity for our sample Todo application: - -[source,java] ----- -package model; - -import java.util.Date; -import java.util.List; - -import jakarta.persistence.Entity; -import jakarta.persistence.ManyToOne; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; - -@Entity -public class Todo extends PanacheEntity { - - @ManyToOne - public User owner; - - public String task; - - public boolean done; - - public Date doneDate; - - public static List findByOwner(User user) { - return find("owner = ?1 ORDER BY id", user).list(); - } -} ----- - -== Controllers - -By convention, you can place your controllers in the `rest` package, but anywhere else works just as well. You -have to extend the `Controller` class in order to benefit from extra easy endpoint declarations and reverse-routing, -but that superclass also gives you useful methods. We usually have one controller per model class, so we tend to use -the plural entity name for the corresponding controller: - -[source,java] ----- -package rest; - -import java.util.Date; -import java.util.List; - -import jakarta.validation.constraints.NotBlank; -import jakarta.ws.rs.POST; - -import org.jboss.resteasy.reactive.RestForm; -import org.jboss.resteasy.reactive.RestPath; - -import io.quarkus.qute.CheckedTemplate; -import io.quarkus.qute.TemplateInstance; -import model.Todo; - -public class Todos extends Controller { - - @CheckedTemplate - static class Templates { - public static native TemplateInstance index(List todos); - } - - public TemplateInstance index() { - // list every todo - List todos = Todo.listAll(); - // render the index template - return Templates.index(todos); - } - - @POST - public void delete(@RestPath Long id) { - // find the Todo - Todo todo = Todo.findById(id); - notFoundIfNull(todo); - // delete it - todo.delete(); - // send loving message - flash("message", "Task deleted"); - // redirect to index page - index(); - } - - @POST - public void done(@RestPath Long id) { - // find the Todo - Todo todo = Todo.findById(id); - notFoundIfNull(todo); - // switch its done state - todo.done = !todo.done; - if(todo.done) - todo.doneDate = new Date(); - // send loving message - flash("message", "Task updated"); - // redirect to index page - index(); - } - - @POST - public void add(@NotBlank @RestForm String task) { - // check if there are validation issues - if(validationFailed()) { - // go back to the index page - index(); - } - // create a new Todo - Todo todo = new Todo(); - todo.task = task; - todo.persist(); - // send loving message - flash("message", "Task added"); - // redirect to index page - index(); - } -} ----- - -=== Methods - -Every public method is a valid endpoint. If it has no HTTP method annotation (`@GET`, `@HEAD`, `@POST`, `@PUT`, `@DELETE`) then -it is assumed to be a `@GET` method. - -Most `@GET` methods will typically return a `TemplateInstance` for rendering an HTML server-side template, and should not -modify application state. - -Controller methods annotated with `@POST`, `@PUT` and `@DELETE` will typically return `void` and trigger a redirect to a `@GET` -method after they do their action. This is not mandatory, you can also return a `TemplateInstance` if you want, but it is good form -to use a redirect to avoid involuntary actions when browsers reload the page. Those methods also get an implicit `@Transactional` -annotation so you don't need to add it. - -If your controller is not annotated with `@Path` it will default to a path using the class name. If your controller method is not -annotated with `@Path` it will default to a path using the method name. The exception is if you have a `@Path` annotation on the -method with an absolute path, in which case the class path part will be ignored. Here's a list of example annotations and how they -result: - -[cols="1,1,1"] -|=== -|Class declaration|Method declaration|URI - -|`class Foo` -|`public TemplateInstance bar()` -|`Foo/bar` - -|`@Path("f") class Foo` -|`public TemplateInstance bar()` -|`f/bar` - -|`class Foo` -|`@Path("b") public TemplateInstance bar()` -|`Foo/b` - - -|`@Path("f") class Foo` -|`@Path("b") public TemplateInstance bar()` -|`f/b` - -|`class Foo` -|`@Path("/bar") public TemplateInstance bar()` -|`bar` - -|`@Path("f") class Foo` -|`@Path("/bar") public TemplateInstance bar()` -|`f/bar` - -|=== - -Furthermore, if you specify path parameters that are not present in your path annotations, they will be automatically -appended to your path: - -[source,java] ----- -public class Orders extends Controller { - - // The URI will be Orders/get/{owner}/{id} - public TemplateInstance get(@RestPath String owner, @RestPath Long id) { - } - - // The URI will be /orders/{owner}/{id} - @Path("/orders") - public TemplateInstance otherGet(@RestPath String owner, @RestPath Long id) { - } -} ----- - -== Views - -You can place your {quarkus-guides-url}/qute-reference[Qute views] in the `src/main/resources/templates` folder, -using the `pass:[{className}/{methodName}].html` naming convention. - -Every controller that has views should declare them with a nested static class annotated with `@CheckedTemplate`: - -[source,java] ----- -public class Todos extends Controller { - - @CheckedTemplate - static class Templates { - public static native TemplateInstance index(List todos); - } - - public TemplateInstance index() { - // list every todo - List todos = Todo.listAll(); - // render the index template - return Templates.index(todos); - } -} ----- - -Here we're declaring the `Todos/index.html` template, specifying that it takes a `todos` parameter of type -`List` which allows us to validate the template at build-time. - -Templates are written in Qute, and you can also declare imported templates in order to validate them using a -toplevel class, such as the `main.html` template: - -[source,java] ----- -package rest; - -import io.quarkus.qute.CheckedTemplate; -import io.quarkus.qute.TemplateInstance; - -@CheckedTemplate -public class Templates { - public static native TemplateInstance main(); -} ----- - -=== Template composition - -Typical web applications will have a main template for their layout and use composition in every method. For example, we -can declare the following main template in `main.html`: - -[source,html] ----- - - - - {#insert title /} - - - {#insert moreStyles /} - - {#insert moreScripts /} - - - {#insert /} - - ----- - -And then use it in our `Todos/index.html` template to list the todo items: - -[source,html] ----- -{#include main.html } -{#title}Todos{/title} - - - - - - - - - - {#for todo in todos} - - - - - {/for} - -
#Task
{todo.id}{todo.task}
- -{/include} ----- - -=== Standard tags - -[cols="1,1"] -|=== -|Tag|Description - -|{quarkus-guides-url}/qute-reference#loop_section[for/each] -|Iterate over collections - -|{quarkus-guides-url}/qute-reference#if_section[if/else] -|Conditional statement - -|{quarkus-guides-url}/qute-reference#when_section[switch/case] -|Switch statement - -|{quarkus-guides-url}/qute-reference#with_section[with] -|Adds value members to the local scope - -|{quarkus-guides-url}/qute-reference#letset_section[let] -|Declare local variables - -|{quarkus-guides-url}/qute-reference#include_helper[include/insert] -|Template composition - -|=== - -=== User tags - -If you want to declare additional tags in order to be able to repeat them in your templates, simply place them in the -`templates/tags` folder. For example, here is our `user.html` tag: - -[source,html] ----- - -{#if img??} -{#gravatar it.email size=size.or(20) default='mm' /} -{/if} -{it.userName} ----- - -Which allows us to use it in every template: - -[source,html] ----- -{#if inject:user} - {#if inject:user.isAdmin}{/if} - {#user inject:user img=true size=20/} -{/if} ----- - -You can pass parameters to your template with `name=value` pairs, and the first unnamed parameter value becomes available -as the `it` parameter. - -See the {quarkus-guides-url}/qute-reference#user_tags[Qute documentation] for more information. - -=== Renarde tags - -Renarde comes with a few extra tags to make your life easier: - -[cols="1,1"] -|=== -|Tag|Description - -|`{#authenticityToken/}` -|Generate a hidden HTML form element containing a xref:security.adoc#csrf[CSRF] token to be matched in the next request. - -|`{#error 'field'/}` -|Inserts the error message for the given field name - -|`{#form uri method='POST' class='css' id='id'}...{/form}` -|Generates an HTML form for the given `URI`, `method` (defaults to `POST`) and optional CSS classes and IDs. -Includes a xref:security.adoc#csrf[CSRF] token. - -|`{#gravatar email size='mm'/}` -|Inserts a gravatar image for the given `email`, with optional `size` (defaults to `mm`) - -|`{#ifError 'field'}...{/ifError}` -|Conditional statement executed if there is an error for the given field - -|=== - -=== Extension methods - -If you need additional methods to be registered to be used on your template expressions, you can declare static methods in -a class annotated with `@TemplateExtension`: - -[source,java] ----- -package util; - -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; - -import io.quarkus.qute.TemplateExtension; - -@TemplateExtension -public class JavaExtensions { - - public static boolean isRecent(Date date){ - Date now = new Date(); - Calendar cal = new GregorianCalendar(); - cal.add(Calendar.MONTH, -6); - Date sixMonthsAgo = cal.getTime(); - return date.before(now) && date.after(sixMonthsAgo); - } - -} ----- - -This one declares an additional method on the `Date` type, allowing you to test whether a date is recent or not: - -[source,html] ----- -{#if todo.done && todo.doneDate.isRecent()} - This was done recently! -{/if} ----- - -=== Renarde extension methods - -[cols="1,1,1"] -|=== -|Target type|Method|Description - -|`Date` -|`format()` -|Formats the date to the `dd/MM/yyyy` format - -|`Date` -|`internetFormat()` -|Formats the date to the `yyyy-MM-dd` format - -|`Date` -|`future()` -|Returns `true` if the date is in the future - -|`Date` -|`since()` -|Formats the date in terms of `X seconds/minutes/hours/days/months/years ago` - -|`String` -|`md5()` -|Returns an MD5 hash of the given string - -|`Object` -|`instanceOf(className)` -|Returns true if the given object is exactly of the specified class name - -|=== - - -=== External CSS, JavaScript libraries - -You can use webjars to provide third-party JavaScript or CSS. For example, here is how you can import Bootstrap -and Bootstrap-icons in your `pom.xml`: - -[source,xml] ----- - - org.webjars - bootstrap - 5.1.3 - - - org.webjars.npm - bootstrap-icons - 1.7.0 - - - io.quarkus - quarkus-webjars-locator - ----- - -After that, you can include them in your Qute templates with: - -[source,html] ----- - - - - - ----- - -Look at https://mvnrepository.com/artifact/org.webjars for the list of available options. - -== Forms - -A lot of the time, you need to send data from the browser to your endpoints, which is often done with forms. - -=== The HTML form - -Creating forms in Renarde is easy: let's see an example of how to do it in Qute: - -[source,html] ----- -{#form uri:Login.complete(newUser.confirmationCode)} - -
- Complete registration for {newUser.email} - {#formElement name="userName" label="User Name"} - {#input name="userName"/} - {/formElement} - {#formElement name="password" label="Password"} - {#input name="password" type="password"/} - {/formElement} - {#formElement name="password2" label="Password Confirmation"} - {#input name="password2" type="password"/} - {/formElement} - {#formElement name="firstName" label="First Name"} - {#input name="firstName"/} - {/formElement} - {#formElement name="lastName" label="Last Name"} - {#input name="lastName"/} - {/formElement} - -
- -{/form} ----- - -Here we're defining a form whose action will go to `Register.complete(newUser.confirmationCode)` and -which contains several form elements, which are just tags to make composition easier. For example `formElement` is -a custom Qute tag for Bootstrap which defines layout for the form element and displays any associated error: - -[source,html] ----- -
- - {nested-content} - {#ifError name} - ​{#error name/}​ - {/ifError} -
----- - -The `input` user tag is also designed for Bootstrap as an abstraction: - -[source,html] ----- - ----- - -As you can see, we have default values for certain attributes, a special error class if there is a validation -error, and we default the value to the one preserved in the flash scope, which is filled whenever validation -fails, so that the user can see the validation error without losing their form values. - -As for the `form` Renarde tag, it is also fairly simple, and only includes an authenticity token for CSRF protection. - -[source,html] ----- -
- {#authenticityToken/} - {nested-content} -
----- - -=== The endpoint - -Most forms will be a `@POST` endpoint, with each form element having a corresponding parameter annotated with `@RestForm`. - -[source,java] ----- -@POST -public void complete(@RestQuery String confirmationCode, - @RestForm String userName, - @RestForm String password, - @RestForm String password2, - @RestForm String firstName, - @RestForm String lastName) { - // do something with the form parameters -} ----- - -You can also group parameters in a POJO, but for now you have to add a special -`@Consumes(MediaType.MULTIPART_FORM_DATA)` annotation: - -[source,java] ----- -@Consumes(MediaType.MULTIPART_FORM_DATA) -@POST -public void complete(@RestQuery String confirmationCode, - FormData form) { - // do something with the form parameters -} - -public static class FormData { - @RestForm String userName; - @RestForm String password; - @RestForm String password2; - @RestForm String firstName; - @RestForm String lastName; -} ----- - -Check out the {quarkus-guides-url}/resteasy-reactive#handling-multipart-form-data[RESTEasy Reactive documentation] -for more information about form parameters and multi-part. - -=== Validation - -You can place your usual {quarkus-guides-url}/validation[Hibernate Validation] annotations on the controller methods that receive user data, but -keep in mind that you have to check for validation errors in your method before you do any action that modifies your state. -This allows you to check more things than you can do with just annotations, with richer logic: - -[source,java] ----- -@POST -public Response complete(@RestQuery String confirmationCode, - @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String userName, - @RestForm @NotBlank @Length(min = 8, max = Util.VARCHAR_SIZE) String password, - @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String password2, - @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String firstName, - @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String lastName) { - // Find the user for this confirmation code - User user = User.findForContirmation(confirmationCode); - if(user == null){ - validation.addError("confirmationCode", "Invalid confirmation code"); - } - - // Make sure the passwords match - validation.equals("password", password, password2); - - // Make sure the username is free - if(User.findByUserName(userName) != null){ - validation.addError("userName", "User name already taken"); - } - - // If validation failed, redirect to the confirm page - if(validationFailed()){ - confirm(confirmationCode); - } - - // Now proceed to complete user registration - ... -} ----- - -You can use the `validation` object to trigger additional validation logic and collect errors. - -Those errors are then placed in the _flash_ scope by a call to `validationFailed()` if there -are any errors, and thus preserved when you redirect from your action method to the `@GET` method -that holds the submitted form, which you can then access in your views using the `{#ifError field}{/ifError}` -conditional tag, or the `{#error field/}` tag which accesses the error message for the given field. - -== Routing, URI mapping, redirects - -We have seen how to declare endpoints and how URIs map to them, but very often we need to map from endpoints to -URIs, which Renarde makes easy. - -=== Redirects after POST - -When handling a `@POST`, `@PUT` or `@DELETE` endpoint, it's good form to redirect to a `@GET` endpoint after -the action has been done, in order to allow the user to reload the page without triggering the action a second -time, and such redirects are simply done by calling the corresponding `@GET` endpoint. In reality, the endpoint -will not be called and will be replaced by a redirect that points to the endpoint in question. - -[source,java] ----- -package rest; - -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; - -import io.quarkus.qute.CheckedTemplate; -import io.quarkus.qute.TemplateInstance; -import io.quarkiverse.renarde.Controller; - -public class Application extends Controller { - - @CheckedTemplate - static class Templates { - public static native TemplateInstance index(); - } - - @Path("/") - public TemplateInstance index() { - return Templates.index(); - } - - @POST - public void someAction() { - // do something - ... - // redirect to the index page - index(); - } -} ----- - -If there are any parameters that form the URI, you must pass them along: - -[source,java] ----- -package rest; - -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; - -import org.jboss.resteasy.reactive.RestPath; -import org.jboss.resteasy.reactive.RestQuery; - -import io.quarkus.qute.CheckedTemplate; -import io.quarkus.qute.TemplateInstance; -import io.quarkiverse.renarde.Controller; - -public class Application extends Controller { - - @CheckedTemplate - static class Templates { - public static native TemplateInstance index(); - } - - @Path("/") - public TemplateInstance index() { - return Templates.index(); - } - - public TemplateInstance somePage(@RestPath String id, @RestQuery String q) { - // do something with the id and q - return Templates.index(); - } - - @POST - public void someAction() { - // do something - ... - // redirect to the somePage page - somePage("foo", "bar"); - } -} ----- - -If you want to redirect to another controller, you can use the `redirect(Class)` method: - -[source,java] ----- -package rest; - -import jakarta.ws.rs.POST; - -import io.quarkiverse.renarde.Controller; - -public class Application extends Controller { - - @POST - public void someAction() { - // do something - ... - // redirect to the Todos.index() endpoint - redirect(Todos.class).index(); - } -} ----- - -=== Obtaining a URI in endpoints - -If you don't want a redirect but need a URI to a given endpoint, you can use the `Router.getURI` -or `Router.getAbsoluteURI` methods, by -passing them a method reference to the endpoint you want and the required parameters: - -[source,java] ----- -package rest; - -import java.net.URI; - -import org.jboss.resteasy.reactive.RestPath; -import org.jboss.resteasy.reactive.RestQuery; - -import io.quarkus.qute.CheckedTemplate; -import io.quarkus.qute.TemplateInstance; -import io.quarkiverse.renarde.Controller; -import io.quarkiverse.renarde.router.Router; - -public class Application extends Controller { - - @CheckedTemplate - public static class Templates { - public static native TemplateInstance somePage(); - public static native TemplateInstance otherPage(URI uri); - } - - public TemplateInstance somePage(@RestPath String foo, @RestQuery Long bar) { - return Templates.somePage(); - } - - public TemplateInstance otherPage() { - // Obtain a URI to somePage - URI uri = Router.getURI(Login::somePage, "something", 23l); - // pass it on to our view - return Templates.otherPage(uri); - } -} ----- - -NOTE: If you plan on using `Response.seeOther` or `Controller.seeOther`, make sure to use the `Router.getAbsoluteURI` -variant, especially if you use the `quarkus.http.root-path` configuration, otherwise your URIs contain that prefix twice. - -=== Obtaining a URI in Qute views - -If you want a URI to an endpoint in a Qute view, you can use the `uri` and `uriabs` namespace with a -call to the endpoint you want to point to: - -[source,html] ----- -Todo ----- - -Naturally, you can also pass any required parameters. - -== Emails - -[source,xml] ----- - - io.quarkus - quarkus-mailer - ----- - -Often you will need your actions to send email notifications. You can use Qute for this too, by declaring your -emails in an `Emails` class: - -[source,java] ----- -package email; - -import io.quarkus.mailer.MailTemplate.MailTemplateInstance; -import io.quarkus.qute.CheckedTemplate; -import model.User; - -public class Emails { - - private static final String FROM = "Todos "; - private static final String SUBJECT_PREFIX = "[Todos] "; - - @CheckedTemplate - static class Templates { - public static native MailTemplateInstance confirm(User user); - } - - public static void confirm(User user) { - Templates.confirm(user) - .subject(SUBJECT_PREFIX + "Please confirm your email address") - .to(user.email) - .from(FROM) - .send().await().indefinitely(); - } -} ----- - -You can then send the email from your endpoint by calling `Emails.confirm(user)`. - -You can use composition for emails too, by having a pair of base templates for HTML in -`src/main/resources/templates/email.html`: - -[source,html] ----- - - - - - - - - {#insert /} -

- This is an automated email, you should not reply to it: your mail will be ignored. -

- - ----- - -And for text in `src/main/resources/templates/email.txt`: - -[source,txt] ----- -{#insert /} - -This is an automated email, you should not reply to it: your mail will be ignored. ----- - -You can then use those templates in your emails in `src/main/resources/templates/Emails/confirm.html`: - -[source,html] ----- -{#include email.html } - -

- Welcome to Todos. -

- -

- You received this email because someone (hopefully you) wants to register on Todos. -

- -

- If you don't want to register, you can safely ignore this email. -

- -

- If you want to register, complete your registration. -

-{/include} ----- - -And for text in `src/main/resources/templates/Emails/confirm.txt`: - -[source,txt] ----- -{#include email.txt} - -Welcome to Todos. - -You received this email because someone (hopefully you) wants to register on Todos. - -If you don't want to register, you can safely ignore this email. - -If you want to register, complete your registration by going to the following address: - -{uriabs:Login.confirm(user.confirmationCode)} -{/include} ----- - -Note that in emails you will want to use the `uriabs` namespace for absolute URIs and not relative ones, -otherwise the links won't work for your email recipients. - -You can find more information in the {quarkus-guides-url}/mailer-reference[Quarkus mailer documentation]. - -== Localisation / Internationalisation - -You can declare your default language and supported languages in `src/main/resources/application.properties`: - -[source,properties] ----- -# This is the default locale for your application -quarkus.default-locale=en -# These are the supported locales (should include the default locale, but order is not important) -quarkus.locales=en,fr ----- - -Next, you can declare your default language messages in the `src/main/resources/message.properties` file: - -[source,properties] ----- -# A simple message -hello=Hello World -# A parameterised message for your view -views_Application_index_greet=Hello %s ----- - -Declare your other language translations in the `src/main/resources/message_fr.properties` file: - -[source,properties] ----- -hello=Bonjour Monde -views_Application_index_greet=Salut %s ----- - -Now you can use these translated messages in your controller: - -[source,java] ----- -public static class Application extends Controller { - - @CheckedTemplate - public static class Templates { - public static native TemplateInstance index(String name); - } - - public TemplateInstance index() { - return Templates.index("Stef"); - } - - public String hello() { - return i18n.formatMessage("hello"); - } -} ----- - -Or in your template: - -[source,txt] ----- -With no parameter: -{m:hello} -With parameters: -{m:views_Application_index_greet(name)} ----- - -=== Selecting the language - -The current language for a request will depend on the following (in order): - -. The `_renarde_language` cookie, if set -. The https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language[`Accept-Language`] HTTP header, if set, - which defines an ordered list of languages by user preference. We will select a best matching language from the set - of `quarkus.locales`. -. If nothing else, we default to the default language as set by `quarkus.default-locale`, which defaults to `en_US`. - -You can override the user's language with a cookie by calling `i18n.set(language)`: - -[source,java] ----- -public static class Application extends Controller { - - @CheckedTemplate - public static class Templates { - public static native TemplateInstance index(); - } - - public void index() { - return Templates.index(); - } - - @POST - public void french() { - i18n.set("fr"); - index(); - } - - @POST - public void english() { - i18n.set("en"); - index(); - } -} ----- - -== Flash scope - -If you need to pass values from one endpoint to another request after a redirect, you can use the Flash scope. -Usually this is done in a `@POST` endpoint, by filling the Flash scope with either errors or messages, -before trigerring a redirect to the right `@GET` endpoint. - -You can push values in the Flash scope in an endpoint using the `flash(name, value)` method, or using the -`Flash` injectable component. - -You can read values from the Flash scope in your Qute views using the `{flash:name}` namespace. - -The Flash scope only lasts from one request to the next and is cleared at each request. - -== htmx - -If you want to use https://htmx.org/[htmx] with Renarde, we added an extra layer to make things more convenient: -[source,java] ----- -public class Application extends HxController { - - @CheckedTemplate - public static class Templates { - public static native TemplateInstance foo(int a, int b); - public static native TemplateInstance foo$sectionA(int a); // <1> - public static native TemplateInstance foo$sectionB(int b); // <1> - - public static native TemplateInstance bar(); - } - - public TemplateInstance foo() { - if (isHxRequest()) { // <2> - return concatTemplates( // <3> - Templates.foo$sectionA(1), - Templates.foo$sectionB(2) - ); - } - return Templates.foo(1, 2); - } - - public TemplateInstance bar() { - onlyHxRequest(); // <4> - this.hx(HxResponseHeader.TRIGGER, "refresh"); // <5> - return Templates.bar(); - } -} ----- -<1> {quarkus-guides-url}/qute-reference#fragments[Qute fragments] declarations -<2> Check if this is a htmx request by looking for the `HX-Request` header or using flash data for redirects -<3> https://htmx.org/attributes/hx-swap-oob/[Out of band swap] with different templates or {quarkus-guides-url}/qute-reference#fragments[fragments] -<4> Only Hx requests are allowed, else it will fail with a BAD_REQUEST error -<5> Flag the response with an https://htmx.org/reference/#response_headers[htmx response header] - -For CSRF Security, you need a form parameter with the CSRF Token. By adding this 👇 when doing a hx-post/put/delete, The Hx requests will be sent with the CSRF parameter: -[source,html] ----- -
----- - -NOTE: There is a ongoing issue to allow using a header instead of a form parameter (https://github.com/quarkusio/quarkus/issues/34513), this way it will be possible to have a `hx-headers` on the to make all hx requests secured with CSRF. - -Some example projects with Quarkus Renarde and htmx: -- https://github.com/ia3andy/renotes[a demo note-taking web app] -- https://github.com/ia3andy/quarkus-blast[a board game] - -== Generating barcodes - -If you import the optional `quarkus-renarde-barcode` module, you can generate barcodes in your views or controllers. - -[source,xml,subs=attributes+] ----- - - io.quarkiverse.renarde - quarkus-renarde-barcode - {quarkus-renarde-version} - ----- - -We support the following barcode types, with custom Qute tags: - -[cols="1,1"] -|=== -|Tag|Description - -|`{#ean13 value/}` -|Generate an https://en.wikipedia.org/wiki/International_Article_Number[EAN 13] barcode. - -|`{#ean8 value/}` -|Generate an https://en.wikipedia.org/wiki/EAN-8[EAN 8] barcode. - -|`{#upca value/}` -|Generate an https://en.wikipedia.org/wiki/Universal_Product_Code[UPC A] barcode. - -|`{#upce value/}` -|Generate an https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E[UPC E] barcode. - -|`{#code39 value/}` -|Generate a https://en.wikipedia.org/wiki/Code_39[Code 39] barcode. - -|`{#code93 value/}` -|Generate a https://en.wikipedia.org/wiki/Code_93[Code 93] barcode. - -|`{#code128 value/}` -|Generate a https://en.wikipedia.org/wiki/Code_128[Code 128] barcode. - -|`{#qrcode value/}` -|Generate a https://en.wikipedia.org/wiki/QR_code[QR Code] barcode. - -|`{#datamatrix value/}` -|Generate a https://en.wikipedia.org/wiki/Data_Matrix[Data Matrix] barcode. - -|=== - -All these tags accept an optional `size` parameter that sets both the width and height of the barcode -in pixels, or `width` and `height` parameters to specify different values. These values all default -to `200` pixels. - -The generated barcodes will be inline HTML images using the https://en.wikipedia.org/wiki/Data_URI_scheme[Data URI] -scheme as an embedded https://en.wikipedia.org/wiki/PNG[PNG] image. - -If you wish to generate barcode images as PNG bytes or other formats, you can use the `io.quarkiverse.renarde.barcode.Barcode` -helper class which allows you to generate bytecodes from your controllers. - -== Generating PDF documents - -If you import the optional `quarkus-renarde-pdf` module, you can generate PDF documents in your views. - -[source,xml,subs=attributes+] ----- - - io.quarkiverse.renarde - quarkus-renarde-pdf - {quarkus-renarde-version} - ----- - -There are lots of complicated ways to generate PDFs, but the simplest is to use the versatility of HTML for rendering -and its printing support. So, very much like you can print HTML files using your browser, and there are special rendering -rules in CSS, you do the same in Renarde PDF: you render your page in HTML, and simply add `@Produces(Pdf.APPLICATION_PDF)` -to your controller to produce a PDF file: - -[source,java] ----- -public class Application extends Controller { - - @CheckedTemplate - public static class Templates { - public static native TemplateInstance page(); - } - - // This will return a PDF file to the caller - @Produces(Pdf.APPLICATION_PDF) - public TemplateInstance pdf() { - return Templates.page(); - } - - // This will return HTML to the caller - public TemplateInstance html() { - return Templates.page(); - } -} ----- - -This way, the http://localhost:8080/Application/pdf and http://localhost:8080/Application/html -pages will render the same document in either PDF or HTML, which allows you to use your browser and its -developer tools to fine-tweak your document, until you're ready to turn it into a PDF, which is -much harder to fine-tweak. - -As for the `templates/Application/page.html` template, it's regular HTML, but you can also -take advantage of the https://developer.mozilla.org/en-US/docs/Web/CSS/Paged_Media[CSS print support] -to set things like document page size, odd/even page margins, etc…: - -[source,html] ----- - - - - This is the PDF document title - - - - Some PDF text. - - ----- - -Note that the default page size is https://en.wikipedia.org/wiki/ISO_216#A_series[A4]. - -== Security - -xref:security.adoc[Read the Renarde security guide] - -[[extension-configuration-reference]] -== Extension Configuration Reference - -include::includes/quarkus-renarde.adoc[opts=optional] diff --git a/docs/modules/ROOT/pages/security.adoc b/docs/modules/ROOT/pages/security.adoc index f404d012..6e4f14f5 100644 --- a/docs/modules/ROOT/pages/security.adoc +++ b/docs/modules/ROOT/pages/security.adoc @@ -6,6 +6,7 @@ include::./includes/attributes.adoc[] Renarde image:renarde-head.svg[width=15em] can help you deal with security and user registration, either manually, using OIDC, or a mix of both. +[#_csrf] == CSRF Renarde comes with built-in support for https://owasp.org/www-community/attacks/csrf[Cross-Site Request Forgery (CSRF)] protection, @@ -83,6 +84,7 @@ public void test() { } ---- +[#_custom_authentication_with_jwt] == Custom authentication with JWT In order to handle your own authentication by storing users in your database, you can use https://datatracker.ietf.org/doc/html/rfc7519[JWT tokens]. @@ -97,7 +99,7 @@ Start with importing the `renarde-security` module: ---- -And set those configuration values: +And set those configuration values: [source,properties] ---- @@ -495,6 +497,7 @@ quarkus.native.resources.includes=publicKey.pem quarkus.native.resources.includes=privateKey.pem ---- +[[oidc]] == Using OIDC for login OIDC (Open ID Connect) is a way to delegate user login to external services, such as: