aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile5
-rw-r--r--go.mod1
-rw-r--r--go.sum11
-rw-r--r--walrss/internal/core/users.go18
-rw-r--r--walrss/internal/http/auth.go100
-rw-r--r--walrss/internal/http/http.go24
-rw-r--r--walrss/internal/http/views/layoutComponents.qtpl.html9
-rw-r--r--walrss/internal/http/views/layoutComponents.qtpl.html.go43
-rw-r--r--walrss/internal/http/views/page.qtpl.html59
-rw-r--r--walrss/internal/http/views/page.qtpl.html.go237
-rw-r--r--walrss/internal/http/views/register.qtpl.html37
-rw-r--r--walrss/internal/http/views/register.qtpl.html.go96
-rw-r--r--walrss/internal/http/views/signin.qtpl.html33
-rw-r--r--walrss/internal/http/views/signin.qtpl.html.go92
-rw-r--r--walrss/internal/http/views/views.go19
15 files changed, 762 insertions, 22 deletions
diff --git a/Makefile b/Makefile
index 60bc03c..9f20f12 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
.PHONY: prebuild fmt
-build:
+build: templates
mkdir -p bin
go build -o bin/walrss github.com/codemicro/walrss/walrss
@@ -10,3 +10,6 @@ run: build
fmt:
go fmt github.com/codemicro/walrss/...
+
+templates:
+ qtc -skipLineComments -ext qtpl.html -dir walrss/internal/http/views \ No newline at end of file
diff --git a/go.mod b/go.mod
index b358424..bc60677 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.34.0 // indirect
+ github.com/valyala/quicktemplate v1.7.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
diff --git a/go.sum b/go.sum
index c9bfcf0..77abf86 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
+github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bwmarrin/go-alone v0.0.0-20190806015146-742bb55d1631 h1:Xb5rra6jJt5Z1JsZhIMby+IP5T8aU+Uc2RC9RzSxs9g=
@@ -6,10 +8,13 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.31.0 h1:M2rWPQbD5fDVAjcoOLjKRXTIlHesI5Eq7I5FEQPt4Ow=
github.com/gofiber/fiber/v2 v2.31.0/go.mod h1:1Ega6O199a3Y7yDGuM9FyXDPYQfv+7/y48wl6WCwUF4=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kkyr/fig v0.3.0 h1:5bd1amYKp/gsK2bGEUJYzcCrQPKOZp6HZD9K21v9Guo=
github.com/kkyr/fig v0.3.0/go.mod h1:fEnrLjwg/iwSr8ksJF4DxrDmCUir5CaVMLORGYMcz30=
+github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
+github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
@@ -26,8 +31,11 @@ github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a h1:oIi7H/bwFUY
github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a/go.mod h1:iSvujNDmpZ6eQX+bg/0X3lF7LEmZ8N77g2a/J/+Zt2U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
+github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
+github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
@@ -35,6 +43,7 @@ go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -42,6 +51,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -52,6 +62,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/walrss/internal/core/users.go b/walrss/internal/core/users.go
index dfc3635..2343de7 100644
--- a/walrss/internal/core/users.go
+++ b/walrss/internal/core/users.go
@@ -42,11 +42,8 @@ func RegisterUser(st *state.State, email, password string) (*db.User, error) {
}
func AreUserCredentialsCorrect(st *state.State, email, password string) (bool, error) {
- user := new(db.User)
- if err := st.Data.FindOne(user, bh.Where("Email").Eq(email)); err != nil {
- if errors.Is(err, bh.ErrNotFound) {
- return false, ErrNotFound
- }
+ user, err := GetUserByEmail(st, email)
+ if err != nil {
return false, err
}
@@ -59,3 +56,14 @@ func AreUserCredentialsCorrect(st *state.State, email, password string) (bool, e
return true, nil
}
+
+func GetUserByEmail(st *state.State, userID string) (*db.User, error) {
+ user := new(db.User)
+ if err := st.Data.FindOne(user, bh.Where("Email").Eq(userID)); err != nil {
+ if errors.Is(err, bh.ErrNotFound) {
+ return nil, ErrNotFound
+ }
+ return nil, err
+ }
+ return user, nil
+}
diff --git a/walrss/internal/http/auth.go b/walrss/internal/http/auth.go
index 7be4078..eff478d 100644
--- a/walrss/internal/http/auth.go
+++ b/walrss/internal/http/auth.go
@@ -1,29 +1,99 @@
package http
import (
+ "errors"
"github.com/codemicro/walrss/walrss/internal/core"
+ "github.com/codemicro/walrss/walrss/internal/http/views"
+ "github.com/codemicro/walrss/walrss/internal/urls"
"github.com/gofiber/fiber/v2"
"time"
)
func (s *Server) authRegister(ctx *fiber.Ctx) error {
- user, err := core.RegisterUser(s.state,
- ctx.FormValue("email"),
- ctx.FormValue("password"),
- )
- if err != nil {
- return err
+ page := new(views.RegisterPage)
+
+ if ctx.Method() == fiber.MethodPost {
+ password := ctx.FormValue("password")
+ passwordConfirmation := ctx.FormValue("passwordConfirmation")
+ if password != passwordConfirmation {
+ page.Problem = "Passwords do not match"
+ goto exit
+ }
+
+ user, err := core.RegisterUser(
+ s.state,
+ ctx.FormValue("email"),
+ password,
+ )
+ if err != nil {
+ if core.IsUserError(err) {
+ ctx.Status(core.GetUserErrorStatus(err))
+ page.Problem = "Could not register account: " + err.Error()
+ goto exit
+ }
+ return err
+ }
+
+ token := core.GenerateSessionToken(user.ID)
+
+ ctx.Cookie(&fiber.Cookie{
+ Name: sessionCookieKey,
+ Value: token,
+ Expires: time.Now().UTC().Add(sessionDuration),
+ Secure: !s.state.Config.Debug,
+ HTTPOnly: true,
+ })
+
+ return ctx.Redirect(urls.Index)
}
- token := core.GenerateSessionToken(user.ID)
+exit:
+ return views.SendPage(ctx, page)
+}
+
+func (s *Server) authSignIn(ctx *fiber.Ctx) error {
+ page := &views.SignInPage{}
+
+ if ctx.Method() == fiber.MethodPost {
+ email := ctx.FormValue("email")
+
+ ok, err := core.AreUserCredentialsCorrect(
+ s.state,
+ email,
+ ctx.FormValue("password"),
+ )
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ goto incorrectUsernameOrPassword
+ }
+ return err
+ }
+
+ if !ok {
+ goto incorrectUsernameOrPassword
+ }
+
+ user, err := core.GetUserByEmail(s.state, email)
+ if err != nil {
+ return err
+ }
+
+ token := core.GenerateSessionToken(user.ID)
+
+ ctx.Cookie(&fiber.Cookie{
+ Name: sessionCookieKey,
+ Value: token,
+ Expires: time.Now().UTC().Add(sessionDuration),
+ Secure: !s.state.Config.Debug,
+ HTTPOnly: true,
+ })
+
+ return ctx.Redirect(urls.Index)
+ }
- ctx.Cookie(&fiber.Cookie{
- Name: sessionCookieKey,
- Value: token,
- Expires: time.Now().UTC().Add(sessionDuration),
- Secure: !s.state.Config.Debug,
- HTTPOnly: true,
- })
+ return views.SendPage(ctx, page)
- return ctx.SendString("ok!")
+incorrectUsernameOrPassword:
+ ctx.Status(fiber.StatusUnauthorized)
+ return views.SendPage(ctx, &views.SignInPage{Problem: "Incorrect username or password"})
}
diff --git a/walrss/internal/http/http.go b/walrss/internal/http/http.go
index 5af260e..92829f7 100644
--- a/walrss/internal/http/http.go
+++ b/walrss/internal/http/http.go
@@ -5,6 +5,7 @@ import (
"github.com/codemicro/walrss/walrss/internal/state"
"github.com/codemicro/walrss/walrss/internal/urls"
"github.com/gofiber/fiber/v2"
+ "github.com/rs/zerolog/log"
"time"
)
@@ -22,8 +23,25 @@ func New(st *state.State) (*Server, error) {
app := fiber.New(fiber.Config{
DisableStartupMessage: !st.Config.Debug,
AppName: "Walrss",
+ ErrorHandler: func(ctx *fiber.Ctx, err error) error {
+ code := fiber.StatusInternalServerError
+ msg := "Internal Server Error"
+
+ switch e := err.(type) {
+ case *fiber.Error:
+ code = e.Code
+ msg = err.Error()
+ case *core.UserError:
+ code = e.Status
+ msg = err.Error()
+ default:
+ log.Error().Err(err).Str("location", "http").Str("url", ctx.OriginalURL()).Send()
+ }
+
+ ctx.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return ctx.Status(code).SendString(msg)
+ },
})
- // TODO: Add error handler with UserError support
s := &Server{
state: st,
@@ -36,7 +54,11 @@ func New(st *state.State) (*Server, error) {
}
func (s *Server) registerHandlers() {
+ s.app.Get(urls.AuthRegister, s.authRegister)
s.app.Post(urls.AuthRegister, s.authRegister)
+
+ s.app.Get(urls.AuthSignIn, s.authSignIn)
+ s.app.Post(urls.AuthSignIn, s.authSignIn)
}
func (s *Server) Run() error {
diff --git a/walrss/internal/http/views/layoutComponents.qtpl.html b/walrss/internal/http/views/layoutComponents.qtpl.html
new file mode 100644
index 0000000..bc3ef1c
--- /dev/null
+++ b/walrss/internal/http/views/layoutComponents.qtpl.html
@@ -0,0 +1,9 @@
+{% import "github.com/codemicro/walrss/walrss/internal/urls" %}
+
+{% func navbar() %}
+<nav class="navbar navbar-light bg-light mb-3">
+ <div class="container-fluid">
+ <a class="navbar-brand" href="{%s= urls.Index %}">Walrss</a>
+ </div>
+</nav>
+{% endfunc %} \ No newline at end of file
diff --git a/walrss/internal/http/views/layoutComponents.qtpl.html.go b/walrss/internal/http/views/layoutComponents.qtpl.html.go
new file mode 100644
index 0000000..28af874
--- /dev/null
+++ b/walrss/internal/http/views/layoutComponents.qtpl.html.go
@@ -0,0 +1,43 @@
+// Code generated by qtc from "layoutComponents.qtpl.html". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+package views
+
+import "github.com/codemicro/walrss/walrss/internal/urls"
+
+import (
+ qtio422016 "io"
+
+ qt422016 "github.com/valyala/quicktemplate"
+)
+
+var (
+ _ = qtio422016.Copy
+ _ = qt422016.AcquireByteBuffer
+)
+
+func streamnavbar(qw422016 *qt422016.Writer) {
+ qw422016.N().S(`
+<nav class="navbar navbar-light bg-light mb-3">
+ <div class="container-fluid">
+ <a class="navbar-brand" href="`)
+ qw422016.N().S(urls.Index)
+ qw422016.N().S(`">Walrss</a>
+ </div>
+</nav>
+`)
+}
+
+func writenavbar(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ streamnavbar(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func navbar() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ writenavbar(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
diff --git a/walrss/internal/http/views/page.qtpl.html b/walrss/internal/http/views/page.qtpl.html
new file mode 100644
index 0000000..968ea7d
--- /dev/null
+++ b/walrss/internal/http/views/page.qtpl.html
@@ -0,0 +1,59 @@
+{% interface
+Page {
+ Title()
+ Body()
+ HeadContent()
+}
+%}
+
+Page prints a page implementing Page interface.
+{% func RenderPage(p Page) %}
+<!DOCTYPE html>
+<html>
+<head>
+ <title>{%s= makePageTitle(p) %}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
+ {%= p.HeadContent() %}
+</head>
+<body>
+{%= p.Body() %}
+</body>
+</html>
+{% endfunc %}
+
+{% code type BasePage struct {} %}
+{% func (p *BasePage) Title() %}{% endfunc %}
+{% func (p *BasePage) HeadContent() %}{% endfunc %}
+
+{% func ProblemBox(p string) %}
+<div class="alert alert-danger" role="alert">
+ {%s= p %}
+</div>
+{% endfunc %}
+
+{% func SuccessBox(p string) %}
+<div class="alert alert-success" role="alert">
+ {%s= p %}
+</div>
+{% endfunc %}
+
+{% func WarningBox(p string) %}
+<div class="alert alert-warning" role="alert">
+ {%s= p %}
+</div>
+{% endfunc %}
+
+PolyPage is used to create a basic page dynamically using Daz.
+{% code
+type PolyPage struct {
+ BasePage
+ TitleString string
+ BodyContent string
+ ExtraHeadContent string
+}
+%}
+
+{% func (p *PolyPage) Title() %}{%s= p.TitleString %}{% endfunc %}
+{% func (p *PolyPage) Body() %}{%s= p.BodyContent %}{% endfunc %}
+{% func (p *PolyPage) HeadContent() %}{%s= p.ExtraHeadContent %}{% endfunc %} \ No newline at end of file
diff --git a/walrss/internal/http/views/page.qtpl.html.go b/walrss/internal/http/views/page.qtpl.html.go
new file mode 100644
index 0000000..125a3b0
--- /dev/null
+++ b/walrss/internal/http/views/page.qtpl.html.go
@@ -0,0 +1,237 @@
+// Code generated by qtc from "page.qtpl.html". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+package views
+
+import (
+ qtio422016 "io"
+
+ qt422016 "github.com/valyala/quicktemplate"
+)
+
+var (
+ _ = qtio422016.Copy
+ _ = qt422016.AcquireByteBuffer
+)
+
+type Page interface {
+ Title() string
+ StreamTitle(qw422016 *qt422016.Writer)
+ WriteTitle(qq422016 qtio422016.Writer)
+ Body() string
+ StreamBody(qw422016 *qt422016.Writer)
+ WriteBody(qq422016 qtio422016.Writer)
+ HeadContent() string
+ StreamHeadContent(qw422016 *qt422016.Writer)
+ WriteHeadContent(qq422016 qtio422016.Writer)
+}
+
+// Page prints a page implementing Page interface.
+
+func StreamRenderPage(qw422016 *qt422016.Writer, p Page) {
+ qw422016.N().S(`
+<!DOCTYPE html>
+<html>
+<head>
+ <title>`)
+ qw422016.N().S(makePageTitle(p))
+ qw422016.N().S(`</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
+ `)
+ p.StreamHeadContent(qw422016)
+ qw422016.N().S(`
+</head>
+<body>
+`)
+ p.StreamBody(qw422016)
+ qw422016.N().S(`
+</body>
+</html>
+`)
+}
+
+func WriteRenderPage(qq422016 qtio422016.Writer, p Page) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ StreamRenderPage(qw422016, p)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func RenderPage(p Page) string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ WriteRenderPage(qb422016, p)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+type BasePage struct{}
+
+func (p *BasePage) StreamTitle(qw422016 *qt422016.Writer) {
+}
+
+func (p *BasePage) WriteTitle(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamTitle(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *BasePage) Title() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteTitle(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func (p *BasePage) StreamHeadContent(qw422016 *qt422016.Writer) {
+}
+
+func (p *BasePage) WriteHeadContent(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamHeadContent(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *BasePage) HeadContent() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteHeadContent(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func StreamProblemBox(qw422016 *qt422016.Writer, p string) {
+ qw422016.N().S(`
+<div class="alert alert-danger" role="alert">
+ `)
+ qw422016.N().S(p)
+ qw422016.N().S(`
+</div>
+`)
+}
+
+func WriteProblemBox(qq422016 qtio422016.Writer, p string) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ StreamProblemBox(qw422016, p)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func ProblemBox(p string) string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ WriteProblemBox(qb422016, p)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func StreamSuccessBox(qw422016 *qt422016.Writer, p string) {
+ qw422016.N().S(`
+<div class="alert alert-success" role="alert">
+ `)
+ qw422016.N().S(p)
+ qw422016.N().S(`
+</div>
+`)
+}
+
+func WriteSuccessBox(qq422016 qtio422016.Writer, p string) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ StreamSuccessBox(qw422016, p)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func SuccessBox(p string) string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ WriteSuccessBox(qb422016, p)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func StreamWarningBox(qw422016 *qt422016.Writer, p string) {
+ qw422016.N().S(`
+<div class="alert alert-warning" role="alert">
+ `)
+ qw422016.N().S(p)
+ qw422016.N().S(`
+</div>
+`)
+}
+
+func WriteWarningBox(qq422016 qtio422016.Writer, p string) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ StreamWarningBox(qw422016, p)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func WarningBox(p string) string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ WriteWarningBox(qb422016, p)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+// PolyPage is used to create a basic page dynamically using Daz.
+
+type PolyPage struct {
+ BasePage
+ TitleString string
+ BodyContent string
+ ExtraHeadContent string
+}
+
+func (p *PolyPage) StreamTitle(qw422016 *qt422016.Writer) {
+ qw422016.N().S(p.TitleString)
+}
+
+func (p *PolyPage) WriteTitle(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamTitle(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *PolyPage) Title() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteTitle(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func (p *PolyPage) StreamBody(qw422016 *qt422016.Writer) {
+ qw422016.N().S(p.BodyContent)
+}
+
+func (p *PolyPage) WriteBody(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamBody(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *PolyPage) Body() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteBody(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func (p *PolyPage) StreamHeadContent(qw422016 *qt422016.Writer) {
+ qw422016.N().S(p.ExtraHeadContent)
+}
+
+func (p *PolyPage) WriteHeadContent(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamHeadContent(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *PolyPage) HeadContent() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteHeadContent(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
diff --git a/walrss/internal/http/views/register.qtpl.html b/walrss/internal/http/views/register.qtpl.html
new file mode 100644
index 0000000..812a671
--- /dev/null
+++ b/walrss/internal/http/views/register.qtpl.html
@@ -0,0 +1,37 @@
+{% import "github.com/codemicro/walrss/walrss/internal/urls" %}
+
+{% code type RegisterPage struct {
+ BasePage
+ Problem string
+} %}
+
+{% func (p *RegisterPage) Title() %}Register{% endfunc %}
+{% func (p *RegisterPage) Body() %}
+{%= navbar() %}
+
+<div class="container">
+ <h1>Register</h1>
+
+ {% if p.Problem != "" %}
+ {%= ProblemBox(p.Problem) %}
+ {% endif %}
+
+ <form action="" method="post">
+ <div class="mb-3">
+ <label for="emailInput" class="form-label">Email address</label>
+ <input type="email" class="form-control" id="emailInput" name="email">
+ </div>
+ <div class="mb-3">
+ <label for="passwordInput" class="form-label">Password</label>
+ <input type="password" class="form-control" id="passwordInput" name="password">
+ </div>
+ <div class="mb-3">
+ <label for="passwordConfirmation" class="form-label">Retype password</label>
+ <input type="password" class="form-control" id="passwordConfirmation" name="passwordConfirmation">
+ </div>
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </form>
+ <br>
+ <a href="{%s= urls.AuthSignIn %}">Already got an account? Click here to sign in</a>
+</div>
+{% endfunc %} \ No newline at end of file
diff --git a/walrss/internal/http/views/register.qtpl.html.go b/walrss/internal/http/views/register.qtpl.html.go
new file mode 100644
index 0000000..d25fdfb
--- /dev/null
+++ b/walrss/internal/http/views/register.qtpl.html.go
@@ -0,0 +1,96 @@
+// Code generated by qtc from "register.qtpl.html". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+package views
+
+import "github.com/codemicro/walrss/walrss/internal/urls"
+
+import (
+ qtio422016 "io"
+
+ qt422016 "github.com/valyala/quicktemplate"
+)
+
+var (
+ _ = qtio422016.Copy
+ _ = qt422016.AcquireByteBuffer
+)
+
+type RegisterPage struct {
+ BasePage
+ Problem string
+}
+
+func (p *RegisterPage) StreamTitle(qw422016 *qt422016.Writer) {
+ qw422016.N().S(`Register`)
+}
+
+func (p *RegisterPage) WriteTitle(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamTitle(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *RegisterPage) Title() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteTitle(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func (p *RegisterPage) StreamBody(qw422016 *qt422016.Writer) {
+ qw422016.N().S(`
+`)
+ streamnavbar(qw422016)
+ qw422016.N().S(`
+
+<div class="container">
+ <h1>Register</h1>
+
+ `)
+ if p.Problem != "" {
+ qw422016.N().S(`
+ `)
+ StreamProblemBox(qw422016, p.Problem)
+ qw422016.N().S(`
+ `)
+ }
+ qw422016.N().S(`
+
+ <form action="" method="post">
+ <div class="mb-3">
+ <label for="emailInput" class="form-label">Email address</label>
+ <input type="email" class="form-control" id="emailInput" name="email">
+ </div>
+ <div class="mb-3">
+ <label for="passwordInput" class="form-label">Password</label>
+ <input type="password" class="form-control" id="passwordInput" name="password">
+ </div>
+ <div class="mb-3">
+ <label for="passwordConfirmation" class="form-label">Retype password</label>
+ <input type="password" class="form-control" id="passwordConfirmation" name="passwordConfirmation">
+ </div>
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </form>
+ <br>
+ <a href="`)
+ qw422016.N().S(urls.AuthSignIn)
+ qw422016.N().S(`">Already got an account? Click here to sign in</a>
+</div>
+`)
+}
+
+func (p *RegisterPage) WriteBody(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamBody(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *RegisterPage) Body() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteBody(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
diff --git a/walrss/internal/http/views/signin.qtpl.html b/walrss/internal/http/views/signin.qtpl.html
new file mode 100644
index 0000000..3022f7f
--- /dev/null
+++ b/walrss/internal/http/views/signin.qtpl.html
@@ -0,0 +1,33 @@
+{% import "github.com/codemicro/walrss/walrss/internal/urls" %}
+
+{% code type SignInPage struct {
+ BasePage
+ Problem string
+} %}
+
+{% func (p *SignInPage) Title() %}Sign in{% endfunc %}
+{% func (p *SignInPage) Body() %}
+{%= navbar() %}
+
+<div class="container">
+ <h1>Sign in</h1>
+
+ {% if p.Problem != "" %}
+ {%= ProblemBox(p.Problem) %}
+ {% endif %}
+
+ <form action="" method="post">
+ <div class="mb-3">
+ <label for="emailInput" class="form-label">Email address</label>
+ <input type="email" class="form-control" id="emailInput" name="email">
+ </div>
+ <div class="mb-3">
+ <label for="passwordInput" class="form-label">Password</label>
+ <input type="password" class="form-control" id="passwordInput" name="password">
+ </div>
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </form>
+ <br>
+ <a href="{%s= urls.AuthRegister %}">No account? Click here to register</a>
+</div>
+{% endfunc %} \ No newline at end of file
diff --git a/walrss/internal/http/views/signin.qtpl.html.go b/walrss/internal/http/views/signin.qtpl.html.go
new file mode 100644
index 0000000..ba433dd
--- /dev/null
+++ b/walrss/internal/http/views/signin.qtpl.html.go
@@ -0,0 +1,92 @@
+// Code generated by qtc from "signin.qtpl.html". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+package views
+
+import "github.com/codemicro/walrss/walrss/internal/urls"
+
+import (
+ qtio422016 "io"
+
+ qt422016 "github.com/valyala/quicktemplate"
+)
+
+var (
+ _ = qtio422016.Copy
+ _ = qt422016.AcquireByteBuffer
+)
+
+type SignInPage struct {
+ BasePage
+ Problem string
+}
+
+func (p *SignInPage) StreamTitle(qw422016 *qt422016.Writer) {
+ qw422016.N().S(`Sign in`)
+}
+
+func (p *SignInPage) WriteTitle(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamTitle(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *SignInPage) Title() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteTitle(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func (p *SignInPage) StreamBody(qw422016 *qt422016.Writer) {
+ qw422016.N().S(`
+`)
+ streamnavbar(qw422016)
+ qw422016.N().S(`
+
+<div class="container">
+ <h1>Sign in</h1>
+
+ `)
+ if p.Problem != "" {
+ qw422016.N().S(`
+ `)
+ StreamProblemBox(qw422016, p.Problem)
+ qw422016.N().S(`
+ `)
+ }
+ qw422016.N().S(`
+
+ <form action="" method="post">
+ <div class="mb-3">
+ <label for="emailInput" class="form-label">Email address</label>
+ <input type="email" class="form-control" id="emailInput" name="email">
+ </div>
+ <div class="mb-3">
+ <label for="passwordInput" class="form-label">Password</label>
+ <input type="password" class="form-control" id="passwordInput" name="password">
+ </div>
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </form>
+ <br>
+ <a href="`)
+ qw422016.N().S(urls.AuthRegister)
+ qw422016.N().S(`">No account? Click here to register</a>
+</div>
+`)
+}
+
+func (p *SignInPage) WriteBody(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamBody(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *SignInPage) Body() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteBody(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
diff --git a/walrss/internal/http/views/views.go b/walrss/internal/http/views/views.go
new file mode 100644
index 0000000..2c11f09
--- /dev/null
+++ b/walrss/internal/http/views/views.go
@@ -0,0 +1,19 @@
+package views
+
+import "github.com/gofiber/fiber/v2"
+
+//go:generate go install github.com/valyala/quicktemplate/qtc@latest
+//go:generate qtc -skipLineComments -ext qtpl.html
+
+func SendPage(ctx *fiber.Ctx, page Page) error {
+ ctx.Set(fiber.HeaderContentType, "html")
+ return ctx.SendString(RenderPage(page))
+}
+
+func makePageTitle(p Page) string {
+ t := p.Title()
+ if t == "" {
+ return "Walrss"
+ }
+ return t + " | Walrss"
+}