Authentication
The most convenient way to handle authentication here is to wrap it in reactive state.
The model handler gives us access to the framework's session storage. That storage is shared across all app instances in the same browser session.
The relevant Doors pieces here are Storage and Auth and Session and Instance.
Session
Start by adding a field for the session state:
app.gox
type App struct {
/* ... */
session doors.Source[driver.Session]
}
main.go
func main() {
r := doors.NewRouter()
doors.UseModel(r, func(r doors.RequestModel, s doors.Source[Path]) doors.Response {
/* ... */
// initialize session-backed state
session := r.SessionStore().Init("session", func() any {
// initialize the state from the current cookie, if present
var s driver.Session
c, err := r.GetCookie("session")
if err == nil {
s = driver.Sessions.Get(c.Value)
}
return doors.NewSource(s)
}).(doors.Source[driver.Session])
return doors.ResponseComp(App{
city: city,
settings: settings,
session: session,
})
})
if err := http.ListenAndServe(":8080", r); err != nil {
panic(err)
}
}
Init(...)accepts a constructor. It runs only when there is no value under the provided key.
This is different from your own application session. The framework manages its runtime session automatically, and
SessionStoreis attached to that runtime session.
Login
Next, add the login component:
The form hooks in this section come from Events. The page title still uses the same Head and Status support introduced earlier.
login.gox
package main
import (
"context"
"net/http"
"time"
"github.com/doors-dev/doors"
"github.com/doors-dev/gox"
"github.com/doors-dev/tutorial/driver"
)
// login credentials for the tutorial
const userLogin = "admin"
const userPassword = "password123"
const sessionDuration = time.Hour * 24
// Login mutates shared session state.
func Login(session doors.Source[driver.Session]) gox.Comp {
return login{
session: session,
message: new(doors.Door),
}
}
type login struct {
session doors.Source[driver.Session]
message *doors.Door
}
// decode the form data into a Go value
type loginData struct {
Login string `form:"login"`
Password string `form:"password"`
}
// handle form submission
func (l login) submit(ctx context.Context, r doors.RequestForm[loginData]) bool {
// check credentials
if r.Data().Login != userLogin || r.Data().Password != userPassword {
// show the error inline
l.message.Update(ctx, <p><mark>wrong password or login</mark></p>)
return false
}
l.message.Clear(ctx)
// create an app session
session := driver.Sessions.Add(r.Data().Login, sessionDuration)
// set the browser cookie
r.SetCookie(&http.Cookie{
Name: "session",
Value: session.Token,
Expires: time.Now().Add(sessionDuration),
Path: "/",
HttpOnly: true,
})
// make the framework session expire with the app session
doors.SessionExpire(ctx, sessionDuration)
// finally update shared reactive state
l.session.Update(ctx, session)
return true
}
elem (l login) Main() {
<title>Log In</title>
<h1>Log In</h1>
<form
// attach the submit handler
(doors.ASubmit[loginData]{
// prevent repeated submits while the request is running
Scope: doors.ScopeOnlyBlocking(),
// reflect the pending state on the submit button
Indicator: doors.IndicatorOnlyAttrQuery("#login-submit", "aria-busy", "true"),
On: l.submit,
})>
<fieldset>
<label>
Login
<input
name="login"
required="true"/>
</label>
<label>
Password
<input
type="password"
name="password"
required="true"/>
</label>
~(l.message)
</fieldset>
<button id="login-submit" role="submit">Log In</button>
</form>
}
Logout
Now add a logout action to the app:
This reuses the same Events API plus the session-lifetime controls described in Session and Instance.
app.gox
elem (a App) logout(s driver.Session) {
<hr>
<section>
~// attach the click handler (~> is proxy syntax; the result is the same)
~>doors.AClick{
// prevent double clicks
Scope: doors.ScopeOnlyBlocking(),
// reflect the pending state on the link itself
Indicator: doors.IndicatorOnlyAttr("aria-busy", "true"),
PreventDefault: true,
On: func(ctx context.Context, r doors.RequestPointer) bool {
// remove the cookie
r.SetCookie(&http.Cookie{
Name: "session",
Path: "/",
MaxAge: -1,
})
// remove the stored session
driver.Sessions.Remove(s.Token)
// reset the framework session expiration
doors.SessionExpire(ctx, 0)
// update shared state
a.session.Update(ctx, driver.Session{})
return true
},
} <a class="contrast" href="#">Log Out</a>
</section>
}
~>is GoX proxy syntax. It proxies the following element through a value that implementsgox.Proxy, and alldoors.A...values do. In practice, proxy syntax and attribute-modifier syntax are mostly interchangeable here.
App
Finally, wire it into the app:
app.gox
elem (a App) Main() {
/* ... */
<main class="container">
~// subscribe to the session state
~(doors.Sub(a.session, elem(s driver.Session) {
~(if s.IsValid() {
~(a.content())
~(a.logout(s))
} else {
~(Login(a.session))
})
}))
</main>
</body>
/* ... */
}
elem (a App) content() {
~(doors.Sub(a.city, elem(city int) {
~(if city == -1 {
~(LocationSelector(func(ctx context.Context, city int) {
a.path.Mutate(ctx, func(p Path) Path {
p.Home = false
p.Dashboard = true
p.CityID = city
return p
})
}))
} else {
~(Dashboard(city, a.settings))
})
}))
}
Result:

And because we used session-scoped reactive state, logout (and login) affects all tabs simultaniously:

Code
main.go
package main
import (
"net/http"
"github.com/doors-dev/doors"
"github.com/doors-dev/tutorial/driver"
)
type Path struct {
Home bool `path:"/"`
Dashboard bool `path:"/:CityID"`
CityID int
Units *driver.Units `query:"units"`
Days *int `query:"days"`
}
func NewSettigns(p Path) Settings {
s := Settings{
Days: 7,
Units: driver.Metric,
}
if p.Days != nil {
s.Days = max(*p.Days, 1)
}
if p.Units != nil && *p.Units == driver.Imperial {
s.Units = driver.Imperial
}
return s
}
type Settings struct {
Units driver.Units
Days int
}
func (s Settings) DaysQuery() *int {
if s.Days == 7 {
return nil
}
return &s.Days
}
func (s Settings) UnitsQuery() *driver.Units {
if s.Units == driver.Metric {
return nil
}
return &s.Units
}
func main() {
r := doors.NewRouter()
doors.UseModel(r, func(r doors.RequestModel, s doors.Source[Path]) doors.Response {
city := doors.NewBeam(s, func(p Path) int {
if p.Home {
return -1
}
return p.CityID
})
settings := doors.NewBeam(s, NewSettigns)
session := r.SessionStore().Init("session", func() any {
var s driver.Session
c, err := r.GetCookie("session")
if err == nil {
s = driver.Sessions.Get(c.Value)
}
return doors.NewSource(s)
}).(doors.Source[driver.Session])
return doors.ResponseComp(App{
path: s,
city: city,
settings: settings,
session: session,
})
})
if err := http.ListenAndServe(":8080", r); err != nil {
panic(err)
}
}
app.gox
package main
import (
"context"
_"embed"
"net/http"
"github.com/doors-dev/doors"
"github.com/doors-dev/gox"
"github.com/doors-dev/tutorial/driver"
)
//go:embed style.css
var styles []byte
type App struct {
path doors.Source[Path]
city doors.Beam[int]
settings doors.Beam[Settings]
session doors.Source[driver.Session]
}
elem (a App) Main() {
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link
(doors.ResourceExternal("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"))
rel="stylesheet">
<link href=(styles) rel="stylesheet" cache>
</head>
<body>
<main class="container">
~(doors.Sub(a.session, elem(s driver.Session) {
~(if s.IsValid() {
~(a.content())
~(a.logout(s))
} else {
~(Login(a.session))
})
}))
</main>
</body>
</html>
}
elem (a App) content() {
~(doors.Sub(a.city, elem(city int) {
~(if city == -1 {
~(LocationSelector(func(ctx context.Context, city int) {
a.path.Mutate(ctx, func(p Path) Path {
p.Home = false
p.Dashboard = true
p.CityID = city
return p
})
}))
} else {
~(Dashboard(city, a.settings))
})
}))
}
elem (a App) logout(s driver.Session) {
<hr>
<section>
~>doors.AClick{
Scope: doors.ScopeOnlyBlocking(),
Indicator: doors.IndicatorOnlyAttr("aria-busy", "true"),
PreventDefault: true,
On: func(ctx context.Context, r doors.RequestPointer) bool {
r.SetCookie(&http.Cookie{
Name: "session",
Path: "/",
MaxAge: -1,
})
driver.Sessions.Remove(s.Token)
doors.SessionExpire(ctx, 0)
a.session.Update(ctx, driver.Session{})
return true
},
} <a class="contrast" href="#">Log Out</a>
</section>
}
login.gox
package main
import (
"context"
"net/http"
"time"
"github.com/doors-dev/doors"
"github.com/doors-dev/gox"
"github.com/doors-dev/tutorial/driver"
)
const userLogin = "admin"
const userPassword = "password123"
const sessionDuration = time.Hour * 24
func Login(session doors.Source[driver.Session]) gox.Comp {
return login{
session: session,
message: new(doors.Door),
}
}
type login struct {
session doors.Source[driver.Session]
message *doors.Door
}
type loginData struct {
Login string `form:"login"`
Password string `form:"password"`
}
func (l login) submit(ctx context.Context, r doors.RequestForm[loginData]) bool {
if r.Data().Login != userLogin || r.Data().Password != userPassword {
l.message.Update(ctx, <p><mark>wrong password or login</mark></p>)
return false
}
l.message.Clear(ctx)
session := driver.Sessions.Add(r.Data().Login, sessionDuration)
r.SetCookie(&http.Cookie{
Name: "session",
Value: session.Token,
Expires: time.Now().Add(sessionDuration),
Path: "/",
HttpOnly: true,
})
doors.SessionExpire(ctx, sessionDuration)
l.session.Update(ctx, session)
return true
}
elem (l login) Main() {
<title>Log In</title>
<h1>Log In</h1>
<form
(doors.ASubmit[loginData]{
Scope: doors.ScopeOnlyBlocking(),
Indicator: doors.IndicatorOnlyAttrQuery("#login-submit", "aria-busy", "true"),
On: l.submit,
})>
<fieldset>
<label>
Login
<input
name="login"
required="true"/>
</label>
<label>
Password
<input
type="password"
name="password"
required="true"/>
</label>
~(l.message)
</fieldset>
<button id="login-submit" role="submit">Log In</button>
</form>
}