Form & Authentification
We cannot simply grant anyone the ability to populate our catalog. Let’s add a login form to the home page.
1. Login Fragment
Prepare the login fragment with a form
./home/login.templ
package home
import "github.com/doors-dev/doors"
func login() templ.Component {
return doors.F(&loginFragment{})
}
type loginFragment struct {}
templ (f *loginFragment) Render() {
<h1>Log In</h1>
<form>
<fieldset>
<label>
Login
<input
name="login"
required="true"
/>
</label>
<label>
Password
<input
type="password"
name="password"
required="true"
/>
</label>
</fieldset>
<button role="submit">Log In</button>
</form>
}
./home/page.teml
/* .. */
templ (h *homePage) Body() {
@login()
}
/* .. */
Okay, now we can see the form on the home page, but it doesn’t do anything.
2. Submit Event Handler
Prepare form data and handler
./home/login.templ
type loginData struct {
Login string `form:"login"`
Password string `form:"password"`
}
func (f *loginFragment) submit(ctx context.Context, r doors.RForm[loginData]) bool {
// debug print
fmt.Printf("%+v\n", r.Data())
// imitation of something happening
<-time.After(time.Second)
// not done, keep active
return false
}
It uses https://github.com/go-playground/form under the hood.
Attach handler to form
./home/login.templ
templ (f *loginFragment) Render() {
<h1>Log In</h1>
// onsubmit
@doors.ASubmit[loginData]{
On: f.submit,
}
<form>
/* .. */
</form>
}
We added a one-second delay in the form handler function to examine the doors’ concurrency control.
First, you can notice that regardless of how frequently you submit the form, events are queued and processed sequentially — that’s intentional concurrency protection at a single hook level.
**Inside the hook function, you can be sure that it can be invoked next time only after the current execution completes. **
That does not affect other hooks and operations, so you need to use synchronization techniques when accessing shared data.
But we don’t want the user to submit a new form when the previous one is still processing.
That’s what scopes are here for.
Scope
templ (f *loginFragment) Render() {
/* ... */
@doors.ASubmit[loginData]{
Scope: doors.ScopeOnlyBlocking(),
On: f.submit,
}
<form>
/* ... */
</form>
}
There are multiple types of scopes; additionally, they can be shared between hooks and combined in a pipeline.
doors.Scope{Type}
is a helper for creating a scope pipeline of one scope of a specific type. Please refer to Scopes for details.
Indication
Let’s tell the user that something is happening during our form processing. PicoCSS provides a special attribute we can use on a button to display a loading state.
Specify Id for the submit button
<button id="login-submit" role="submit">Log In</button>
Set up hook pending indication
templ (f *loginFragment) Render() {
/* ... */
@doors.ASubmit[loginData]{
// query element with id login-submit and set attr area-busy to true during hook execution
Indicator: doors.IndicatorOnlyAttrQuery("#login-submit", "aria-busy", "true"),
// blocks new submission (on front-end), until the previous one is processed
Scope: doors.ScopeOnlyBlocking(),
On: f.submit,
}
<form>
/* ... */
<button id="login-submit" role="submit">Log In</button>
</form>
}
doors.IndicatorOnly{Type}{Selector}
is a helper function to define a single indicator of a specific type and selector. There are multiple indication types (attribute, class, content) and selectors (target, query, parent query). You can also specify multiple indicators. Please refer to Indication for details.
3. Form Error Message
Just add a door for the error message.
./home/login.teml
/* ... */
type loginFragment struct {
// door to display message
message doors.Door
}
// error message template
templ (l *loginFragment) errorMessage() {
<p><mark>wrong password or login</mark></p>
}
func (f *loginFragment) submit(ctx context.Context, r doors.RForm[loginData]) bool {
if r.Data().Login != userLogin || r.Data().Password != userPassword {
//display errror
f.message.Update(ctx, f.errorMessage())
return false
}
// display ok, just for testing
f.message.Update(ctx, doors.Text("ok"))
return false
}
templ (f *loginFragment) Render() {
/* ... */
<form>
<fieldset>
/* ... */
// render th message door
@f.message
</fieldset>
/* ... */
</form>
}
Now, the form should tell if the login and password are correct or not.
4. Session Cookies
Set Cookie
In the form handler function
./home/login.templ
const sessionDuration = time.Hour * 24
func (f *loginFragment) submit(ctx context.Context, r doors.RForm[loginData]) bool {
if r.Data().Login != userLogin || r.Data().Password != userPassword {
f.message.Update(ctx, f.errorMessage())
return false
}
// add session to the storage
session := driver.Sessions.Add(r.Data().Login, sessionDuration)
// set cookie in the form submit response
r.SetCookie(&http.Cookie{
Name: "session",
Value: session.Token,
Expires: time.Now().Add(sessionDuration),
Path: "/",
HttpOnly: true,
})
// tell frontent to reload the page after the form submit request
r.After(doors.ActionOnlyLocationReload())
// limit doors internal session to cookie session duration
// to ensure that pages won't outlive authenitifacation
doors.SessionExpire(ctx, sessionDuration)
return true
}
r.After([]doors.Action)
allows you to specify an action to execute on the front-end after the hook request is finished.doors.ActionOnlyLocationReload()
is useful for situations when you need to reinitialize the page after hook execution.
doors.SessionExpire
is a safe precaution to ensure that opened pages with access to authorized functionality will not outlive the authorization session.
5. Check Authorization
./home/page.templ
type homePage struct {
// add session property
session *driver.Session
}
/* ... */
templ (h *homePage) Body() {
// display login form if there is no session
if h.session == nil {
@login()
} else {
<h1>Welcome <strong>{ h.session.Login }</strong>!</h1>
}
}
/* ... */
func Handler(p doors.PageRouter[Path], r doors.RPage[Path]) doors.PageRoute {
// read cookie for the page request
c, err := r.GetCookie("session")
if err != nil {
return p.Page(&homePage{})
}
// get session entry by cookie value
s, found := driver.Sessions.Get(c.Value)
if !found {
return p.Page(&homePage{})
}
// provide session to page
return p.Page(&homePage{
session: &s,
})
}
The handler function is the page’s entry point. It’s the only place where you need to worry about checking cookie authorization.
6. Logout
Add a logout button and handler to the home page for simplicity.
./home/page.templ
templ (h *homePage) Body() {
if h.session == nil {
@login()
} else {
<h1>Welcome <strong>{ h.session.Login }</strong>!</h1>
// button to log out
@doors.AClick{
On: func(ctx context.Context, r doors.REvent[doors.PointerEvent]) bool {
// clean cookies
r.SetCookie(&http.Cookie{
Name: "session",
Path: "/",
MaxAge: -1,
})
// remove session entry
driver.Sessions.Remove(h.session.Token)
// end doors session to ensure no active pages left
doors.SessionEnd(ctx)
return true
},
}
<button class="secondary">Log Out</button>
}
}
It’s very important to call SessionEnd(ctx) on logout to ensure that no pages are left running under the authorized user.
7. Refactor
Let’s refactor the session extraction to an external function, so we can reuse it on the catalog page.
./common/utils.go
package common
import (
"github.com/derstruct/doors-tutorial/driver"
"github.com/doors-dev/doors"
)
// doors.R - base request interface to deal with cookies
func GetSession(r doors.R) *driver.Session {
c, err := r.GetCookie("session")
if err != nil {
return nil
}
s, found := driver.Sessions.Get(c.Value)
if !found {
return nil
}
return &s
}
./home/page.templ
/* ... */
func Handler(p doors.PageRouter[Path], r doors.RPage[Path]) doors.PageRoute {
return p.Page(&homePage{
session: common.GetSession(r),
})
}
8. Add authorization to the catalog page
Page Handler
./catalog/page.templ
/* ... */
type catalogPage struct {
// session property
session *driver.Session
path doors.SourceBeam[Path]
}
/* ... */
func Handler(p doors.PageRouter[Path], r doors.RPage[Path]) doors.PageRoute {
return p.Page(&catalogPage{
session: common.GetSession(r),
})
}
Instance Storage
Now, instead of propagating the session object as a property, let’s utilize instance context storage.
Prepare utils:
./common/utils.go
/* ... */
type sessionKey struct{}
func StoreSession(ctx context.Context, session *driver.Session) {
// save to the pages global "thread safe" storage
doors.InstanceSave(ctx, sessionKey{}, session)
}
func LoadSession(ctx context.Context) *driver.Session {
session, ok := doors.InstanceLoad(ctx, sessionKey{}).(*driver.Session)
if !ok {
return nil
}
return session
}
// helper just to check
func IsAuthorized(ctx context.Context) bool {
return LoadSession(ctx) != nil
}
/* ... */
Auth check in category fragment
templ (f *categoryFragment) content(catId string) {
{{ cat, ok := driver.Cats.Get(catId) }}
if ok {
<hgroup>
<h1>{ cat.Name }</h1>
<p>{ cat.Desc } </p>
</hgroup>
// check directly on the context in scope
if common.IsAuthorized(ctx) {
<p>
<button class="contrast">Add Item</button>
</p>
}
} else {
@doors.Status(http.StatusNotFound)
<div>
<mark>Not Found</mark>
</div>
}
}
Check any category page, you should see button “Add Item” button if authorized
Next: Create Item