~evanj/cms

c0e6425139922453aaed202fd12253eeb6de14c6 — Evan J 2 years ago 630f31a tmp2
WIP(email): Email support for reset password.
M internal/c/content/content.go => internal/c/content/content.go +2 -1
@@ 37,7 37,8 @@ var (
// request/response handling. External server requests have a timeout of ten
// seconds.
func bgctx() context.Context {
	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	_ = cancel // Timeout will end downstream work.
	return ctx
}


M internal/c/user/user.go => internal/c/user/user.go +37 -1
@@ 27,11 27,17 @@ type User struct {
	db            dber
	signupEnabled bool
	stripe        Striper
	email         Emailer
}

type Emailer interface {
	Send(to, sub, msg string) error
}

type dber interface {
	UserNew(ctx context.Context, username, password, verifyPassword string) (user.User, error)
	UserGet(ctx context.Context, username, password string) (user.User, error)
	UserGetFromEmail(ctx context.Context, email string) (user.User, error)
	UserGetFromToken(ctx context.Context, token string) (user.User, error)
	UserSetEmail(ctx context.Context, u user.User, email string) (user.User, error)
	UserSetPassword(ctx context.Context, u user.User, current, password, verify string) (user.User, error)


@@ 43,13 49,14 @@ type Striper interface {
	CancelSubscription(ctx context.Context, user user.User) error
}

func New(c *c.Controller, log *log.Logger, db dber, signupEnabled bool, s Striper) *User {
func New(c *c.Controller, log *log.Logger, db dber, signupEnabled bool, s Striper, e Emailer) *User {
	return &User{
		c,
		log,
		db,
		signupEnabled,
		s,
		e,
	}
}



@@ 58,6 65,32 @@ func (l *User) logout(w http.ResponseWriter, r *http.Request) {
	l.Redirect(w, r, "/")
}

// forgot handles a request from user that user forgot password.
// An email will utlimately be sent to user, if user has an email
// associated with their account, which is not guaranteed.
func (l *User) forgot(w http.ResponseWriter, r *http.Request) {
	// TODO: Finish this impl.

	var (
		email   = r.FormValue("email")
		subject = "Skipper: forgot password link"
		message = "This functionality is not complete..."
	)

	user, err := l.db.UserGetFromEmail(r.Context(), email)
	if err != nil {
		l.Error(w, r, http.StatusNotFound, err)
		return
	}

	if err := l.email.Send(user.Email(), subject, message); err != nil {
		l.Error(w, r, http.StatusInternalServerError, err)
		return
	}

	l.Redirect(w, r, "/")
}

func (l *User) login(w http.ResponseWriter, r *http.Request) {
	username := r.FormValue("username")
	password := r.FormValue("password")


@@ 225,6 258,9 @@ func (l *User) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "/":
		l.home(w, r)
		return
	case "/user/forgot":
		l.forgot(w, r)
		return
	case "/user/login":
		l.login(w, r)
		return

M internal/s/db/user.go => internal/s/db/user.go +45 -0
@@ 28,6 28,16 @@ type User struct {
var (
	queryCreateNewUser = `INSERT INTO cms_user (NAME, HASH, ORG_ID, ROLE_ID) VALUES (?, ?, ?, (SELECT cms_role.ID FROM cms_role WHERE VALUE=?))`

	queryFindUserByEmail = `
		SELECT cms_user.ID, NAME, HASH, EMAIL, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER, cms_role.ID, cms_role.VALUE
		FROM cms_user 
		JOIN cms_org ON cms_org.ID=cms_user.ORG_ID
		join cms_role ON cms_role.ID=cms_user.ROLE_ID
		LEFT JOIN cms_billing ON cms_billing.ORG_ID=cms_org.ID
		LEFT JOIN cms_email ON cms_email.USER_ID=cms_user.ID 
		WHERE cms_email.EMAIL = ?
	`

	queryFindUserByID = `
		SELECT cms_user.ID, NAME, HASH, EMAIL, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER, cms_role.ID, cms_role.VALUE
		FROM cms_user 


@@ 137,6 147,41 @@ func (db *DB) userGet(ctx context.Context, t *sql.Tx, username, password string)
	return &user, nil
}

func (db *DB) UserGetFromEmail(ctx context.Context, email string) (user.User, error) {
	t, err := db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	user, err := db.userGetFromEmail(ctx, t, email)
	if err != nil {
		return nil, err
	}

	return user, t.Commit()
}

func (db *DB) userGetFromEmail(ctx context.Context, t *sql.Tx, email string) (user.User, error) {
	var user User
	if err := t.QueryRowContext(ctx, queryFindUserByEmail, email).Scan(
		&user.UserID, &user.UserName, &user.userHash, &user.userEmail,
		&user.UserOrg.OrgID, &user.UserOrg.OrgBillingTierName, &user.UserOrg.OrgPaymentCustomer,
		&user.UserRole.RoleID, &user.UserRole.RoleName,
	); err != nil {
		fmt.Println(err)
		return nil, fmt.Errorf("failed to find user")
	}

	tok, err := db.sec.TokenCreate(security.TokenMap{"ID": user.UserID})
	if err != nil {
		return nil, fmt.Errorf("failed to create token for user")
	}

	user.userToken = tok
	return &user, nil
}

func (db *DB) UserGetFromToken(ctx context.Context, token string) (user.User, error) {
	t, err := db.BeginTx(ctx, nil)
	if err != nil {

A internal/s/email/email.go => internal/s/email/email.go +54 -0
@@ 0,0 1,54 @@
package email

import (
	"bytes"
	"fmt"
	"html/template"
	"net/smtp"
)

type Email struct {
	from, domain string
	port         int
	auth         smtp.Auth
	tmpl         *template.Template
}

func New(username, password, from, domain string, port int) Email {
	var (
		ident      string
		tmplSource = "From: {{.From}}\r\nTo: {{.To}}\r\nSubject: {{.Subject}}\r\n\r\n{{.Body}}\r\n"
		tmpl       = template.Must(template.New("").Parse(tmplSource))
	)

	return Email{
		from,
		domain,
		port,
		smtp.PlainAuth(ident, username, password, domain),
		tmpl,
	}
}

func (e Email) Send(to, sub, msg string) error {
	var (
		buf  bytes.Buffer
		addr = fmt.Sprintf("%s:%d", e.domain, e.port)
		data = map[string]string{
			"From":    e.from,
			"To":      to,
			"Subject": sub,
			"Body":    msg,
		}
	)

	if err := e.tmpl.Execute(&buf, data); err != nil {
		return err
	}

	if err := smtp.SendMail(addr, e.auth, e.from, []string{to}, buf.Bytes()); err != nil {
		return err
	}

	return nil
}

A internal/s/email/email_test.go => internal/s/email/email_test.go +1 -0
@@ 0,0 1,1 @@
package email_test

M internal/v/html/_footer.html => internal/v/html/_footer.html +2 -0
@@ 102,6 102,8 @@
              </select>
            </div>
          </div>
          <p class='mt-3 mb-0'>For more information about what each specific role is allowed 
          <a target='_blank' href='/roles'>please read the roles guide.</a></p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>

M internal/v/tmpls_embed.go => internal/v/tmpls_embed.go +1 -1
@@ 20,7 20,7 @@ func init() {

	tmpls["css/mvp.css"] = tostring("OnJvb3QgewogICAgLS1ib3JkZXItcmFkaXVzOiA1cHg7CiAgICAtLWJveC1zaGFkb3c6IDJweCAycHggMTBweDsKICAgIC0tY29sb3I6ICMxMThiZWU7CiAgICAtLWNvbG9yLWFjY2VudDogIzExOGJlZTBiOwogICAgLS1jb2xvci1iZzogI2ZmZjsKICAgIC0tY29sb3ItYmctc2Vjb25kYXJ5OiAjZTllOWU5OwogICAgLS1jb2xvci1zZWNvbmRhcnk6ICM5MjBkZTk7CiAgICAtLWNvbG9yLXNlY29uZGFyeS1hY2NlbnQ6ICM5MjBkZTkwYjsKICAgIC0tY29sb3Itc2hhZG93OiAjZjRmNGY0OwogICAgLS1jb2xvci10ZXh0OiAjMDAwOwogICAgLS1jb2xvci10ZXh0LXNlY29uZGFyeTogIzk5OTsKICAgIC0taG92ZXItYnJpZ2h0bmVzczogMS4yOwogICAgLS1qdXN0aWZ5LWltcG9ydGFudDogY2VudGVyOwogICAgLS1qdXN0aWZ5LW5vcm1hbDogbGVmdDsKICAgIC0tbGluZS1oZWlnaHQ6IDE1MCU7CiAgICAtLXdpZHRoLWNhcmQ6IDI4NXB4OwogICAgLS13aWR0aC1jYXJkLW1lZGl1bTogNDYwcHg7CiAgICAtLXdpZHRoLWNhcmQtd2lkZTogODAwcHg7CiAgICAtLXdpZHRoLWNvbnRlbnQ6IDEwODBweDsKfQoKLyogTVZQLmNzcyB2MS4wIC0gYnkgQW5keSBCcmV3ZXIgKi8KCi8qIExheW91dCAqLwphcnRpY2xlIGFzaWRlIHsKICAgIGJhY2tncm91bmQ6IHZhcigtLWNvbG9yLXNlY29uZGFyeS1hY2NlbnQpOwogICAgYm9yZGVyLWxlZnQ6IDRweCBzb2xpZCB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgcGFkZGluZzogMC4wMXJlbSAwLjhyZW07Cn0KCmJvZHkgewogICAgYmFja2dyb3VuZDogdmFyKC0tY29sb3ItYmcpOwogICAgY29sb3I6IHZhcigtLWNvbG9yLXRleHQpOwogICAgZm9udC1mYW1pbHk6IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgUm9ib3RvLCBPeHlnZW4tU2FucywgVWJ1bnR1LCBDYW50YXJlbGwsICJIZWx2ZXRpY2EgTmV1ZSIsIHNhbnMtc2VyaWY7CiAgICBsaW5lLWhlaWdodDogdmFyKC0tbGluZS1oZWlnaHQpOwogICAgbWFyZ2luOiAwOwogICAgb3ZlcmZsb3cteDogaGlkZGVuOwogICAgcGFkZGluZzogMXJlbSAwOwp9Cgpmb290ZXIsCmhlYWRlciwKbWFpbiB7CiAgICBtYXJnaW46IDAgYXV0bzsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY29udGVudCk7CiAgICBwYWRkaW5nOiAycmVtIDFyZW07Cn0KCmhyIHsKICAgIGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWJnLXNlY29uZGFyeSk7CiAgICBib3JkZXI6IG5vbmU7CiAgICBoZWlnaHQ6IDFweDsKICAgIG1hcmdpbjogNHJlbSAwOwp9CgpzZWN0aW9uIHsKICAgIGRpc3BsYXk6IGZsZXg7CiAgICBmbGV4LXdyYXA6IHdyYXA7CiAgICBqdXN0aWZ5LWNvbnRlbnQ6IHZhcigtLWp1c3RpZnktaW1wb3J0YW50KTsKfQoKc2VjdGlvbiBhc2lkZSB7CiAgICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1jb2xvci1iZy1zZWNvbmRhcnkpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBib3gtc2hhZG93OiB2YXIoLS1ib3gtc2hhZG93KSB2YXIoLS1jb2xvci1zaGFkb3cpOwogICAgbWFyZ2luOiAxcmVtOwogICAgcGFkZGluZzogMS4yNXJlbTsKICAgIHdpZHRoOiB2YXIoLS13aWR0aC1jYXJkKTsKfQoKc2VjdGlvbiBhc2lkZTpob3ZlciB7CiAgICBib3gtc2hhZG93OiB2YXIoLS1ib3gtc2hhZG93KSB2YXIoLS1jb2xvci1iZy1zZWNvbmRhcnkpOwp9CgpzZWN0aW9uIGFzaWRlIGltZyB7CiAgICBtYXgtd2lkdGg6IDEwMCU7Cn0KCi8qIEhlYWRlcnMgKi8KYXJ0aWNsZSBoZWFkZXIsCmRpdiBoZWFkZXIsCm1haW4gaGVhZGVyIHsKICAgIHBhZGRpbmctdG9wOiAwOwp9CgpoZWFkZXIgewogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1pbXBvcnRhbnQpOwp9CgpoZWFkZXIgYSBiLApoZWFkZXIgYSBlbSwKaGVhZGVyIGEgaSwKaGVhZGVyIGEgc3Ryb25nIHsKICAgIG1hcmdpbi1sZWZ0OiAxcmVtOwogICAgbWFyZ2luLXJpZ2h0OiAxcmVtOwp9CgpoZWFkZXIgbmF2IGltZyB7CiAgICBtYXJnaW46IDFyZW0gMDsKfQoKc2VjdGlvbiBoZWFkZXIgewogICAgcGFkZGluZy10b3A6IDA7CiAgICB3aWR0aDogMTAwJTsKfQoKLyogTmF2ICovCm5hdiB7CiAgICBhbGlnbi1pdGVtczogY2VudGVyOwogICAgZGlzcGxheTogZmxleDsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuOwogICAgbWFyZ2luLWJvdHRvbTogN3JlbTsKfQoKbmF2IHVsIHsKICAgIGxpc3Qtc3R5bGU6IG5vbmU7CiAgICBwYWRkaW5nOiAwOwp9CgpuYXYgdWwgbGkgewogICAgZGlzcGxheTogaW5saW5lLWJsb2NrOwogICAgbWFyZ2luOiAwIDAuNXJlbTsKfQoKLyogVHlwb2dyYXBoeSAqLwpjb2RlIHsKICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICAgIG1hcmdpbjogMCAwLjFyZW07CiAgICBwYWRkaW5nOiAwcmVtIDAuNXJlbTsKfQoKY29kZSwKc2FtcCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci1hY2NlbnQpOwogICAgY29sb3I6IHZhcigtLWNvbG9yLXRleHQpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICB0ZXh0LWFsaWduOiB2YXIoLS1qdXN0aWZ5LW5vcm1hbCk7Cn0KCmgxLApoMiwKaDMsCmg0LApoNSwKaDYgewogICAgbGluZS1oZWlnaHQ6IHZhcigtLWxpbmUtaGVpZ2h0KTsKfQoKbWFyayB7CiAgICBwYWRkaW5nOiAwLjFyZW07Cn0KCm9sIGxpLAp1bCBsaSB7CiAgICBwYWRkaW5nOiAwLjJyZW0gMDsKfQoKcCB7CiAgICBtYXJnaW46IDAuNzVyZW0gMDsKICAgIHBhZGRpbmc6IDA7Cn0KCnNhbXAgewogICAgZGlzcGxheTogYmxvY2s7CiAgICBtYXJnaW46IDFyZW0gMDsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY2FyZC13aWRlKTsKICAgIHBhZGRpbmc6IDFyZW07Cn0KCnNtYWxsIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7Cn0KCnN1cCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwogICAgZm9udC1zaXplOiB4eC1zbWFsbDsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAgbWFyZ2luOiAwLjJyZW07CiAgICBwYWRkaW5nOiAwLjJyZW0gMC4zcmVtOwogICAgcG9zaXRpb246IHJlbGF0aXZlOwogICAgdG9wOiAtMnB4Owp9CgovKiBMaW5rcyAqLwphIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7Cn0KCmE6aG92ZXIgewogICAgZmlsdGVyOiBicmlnaHRuZXNzKHZhcigtLWhvdmVyLWJyaWdodG5lc3MpKTsKICAgIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lOwp9CgphIGIsCmEgZW0sCmEgaSwKYSBzdHJvbmcsCmJ1dHRvbiB7CiAgICBib3JkZXItcmFkaXVzOiB2YXIoLS1ib3JkZXItcmFkaXVzKTsKICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICAgIGZvbnQtc2l6ZTogbWVkaXVtOwogICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICBtYXJnaW46IDEuNXJlbSAwIDAuNXJlbSAwOwogICAgcGFkZGluZzogMXJlbSAycmVtOwp9CgppbnB1dFt0eXBlPXN1Ym1pdF06aG92ZXIsCmJ1dHRvbjpob3ZlciB7CiAgICBjdXJzb3I6IHBvaW50ZXI7CiAgICBmaWx0ZXI6IGJyaWdodG5lc3ModmFyKC0taG92ZXItYnJpZ2h0bmVzcykpOwp9CgphIGIsCmEgc3Ryb25nLAppbnB1dFt0eXBlPXN1Ym1pdF0sCmJ1dHRvbiB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvcik7CiAgICBib3JkZXI6IDJweCBzb2xpZCB2YXIoLS1jb2xvcik7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwp9CgphIGVtLAphIGkgewogICAgYm9yZGVyOiAycHggc29saWQgdmFyKC0tY29sb3IpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3IpOwogICAgZGlzcGxheTogaW5saW5lLWJsb2NrOwogICAgcGFkZGluZzogMXJlbSAycmVtOwp9CgovKiBJbWFnZXMgKi8KZmlndXJlIHsKICAgIG1hcmdpbjogMDsKICAgIHBhZGRpbmc6IDA7Cn0KCmZpZ3VyZSBmaWdjYXB0aW9uIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7Cn0KCi8qIEZvcm1zICovCmZvcm0gewogICAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItYmctc2Vjb25kYXJ5KTsKICAgIGJvcmRlci1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwogICAgYm94LXNoYWRvdzogdmFyKC0tYm94LXNoYWRvdykgdmFyKC0tY29sb3Itc2hhZG93KTsKICAgIGRpc3BsYXk6IGJsb2NrOwogICAgbWF4LXdpZHRoOiB2YXIoLS13aWR0aC1jYXJkLXdpZGUpOwogICAgbWluLXdpZHRoOiB2YXIoLS13aWR0aC1jYXJkKTsKICAgIHBhZGRpbmc6IDEuNXJlbTsKICAgIHRleHQtYWxpZ246IHZhcigtLWp1c3RpZnktbm9ybWFsKTsKfQoKZm9ybSBoZWFkZXIgewogICAgbWFyZ2luOiAxLjVyZW0gMDsKICAgIHBhZGRpbmc6IDEuNXJlbSAwOwp9CgppbnB1dCwKbGFiZWwsCnNlbGVjdCwKdGV4dGFyZWEgewogICAgZGlzcGxheTogYmxvY2s7CiAgICBmb250LXNpemU6IGluaGVyaXQ7CiAgICBtYXgtd2lkdGg6IHZhcigtLXdpZHRoLWNhcmQtd2lkZSk7Cn0KCmlucHV0LApzZWxlY3QsCnRleHRhcmVhIHsKICAgIG1hcmdpbi1ib3R0b206IDFyZW07Cn0KCmlucHV0LApzZWxlY3QsCnRleHRhcmVhIHsKICAgIGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWJnLXNlY29uZGFyeSk7CiAgICBib3JkZXItcmFkaXVzOiB2YXIoLS1ib3JkZXItcmFkaXVzKTsKICAgIHBhZGRpbmc6IDAuNHJlbSAwLjhyZW07Cn0KCmxhYmVsIHsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAgbWFyZ2luLWJvdHRvbTogMC4ycmVtOwp9CgovKiBUYWJsZXMgKi8KdGFibGUgewogICAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItYmctc2Vjb25kYXJ5KTsKICAgIGJvcmRlci1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwogICAgYm9yZGVyLXNwYWNpbmc6IDA7CiAgICBtYXgtd2lkdGg6IDEwMCU7CiAgICBvdmVyZmxvdzogaGlkZGVuOwogICAgcGFkZGluZzogMDsKfQoKdGFibGUgdGQsCnRhYmxlIHRoLAp0YWJsZSB0ciB7CiAgICBwYWRkaW5nOiAwLjRyZW0gMC44cmVtOwogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1pbXBvcnRhbnQpOwp9Cgp0YWJsZSB0aGVhZCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvcik7CiAgICBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwogICAgbWFyZ2luOiAwOwogICAgcGFkZGluZzogMDsKfQoKdGFibGUgdGhlYWQgdGg6Zmlyc3QtY2hpbGQgewogICAgYm9yZGVyLXRvcC1sZWZ0LXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7Cn0KCnRhYmxlIHRoZWFkIHRoOmxhc3QtY2hpbGQgewogICAgYm9yZGVyLXRvcC1yaWdodC1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwp9Cgp0YWJsZSB0aGVhZCB0aDpmaXJzdC1jaGlsZCwKdGFibGUgdHIgdGQ6Zmlyc3QtY2hpbGQgewogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1ub3JtYWwpOwp9CgovKiBRdW90ZXMgKi8KYmxvY2txdW90ZSB7CiAgICBkaXNwbGF5OiBibG9jazsKICAgIGZvbnQtc2l6ZTogeC1sYXJnZTsKICAgIGxpbmUtaGVpZ2h0OiB2YXIoLS1saW5lLWhlaWdodCk7CiAgICBtYXJnaW46IDFyZW0gYXV0bzsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY2FyZC1tZWRpdW0pOwogICAgcGFkZGluZzogMS41cmVtIDFyZW07CiAgICB0ZXh0LWFsaWduOiB2YXIoLS1qdXN0aWZ5LWltcG9ydGFudCk7Cn0KCmJsb2NrcXVvdGUgZm9vdGVyIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7CiAgICBkaXNwbGF5OiBibG9jazsKICAgIGZvbnQtc2l6ZTogc21hbGw7CiAgICBsaW5lLWhlaWdodDogdmFyKC0tbGluZS1oZWlnaHQpOwogICAgcGFkZGluZzogMS41cmVtIDA7Cn0KCi8qIEN1c3RvbSBzdHlsZXMgKi8K")

	tmpls["html/_footer.html"] = tostring("PGRpdiBjbGFzcz1jb250YWluZXI+CiAgPGZvb3RlciBjbGFzcz0icHQtNCBteS1tZC01IHB0LW1kLTUgYm9yZGVyLXRvcCI+CiAgICA8ZGl2IGNsYXNzPSJyb3ciPgogICAgICA8ZGl2IGNsYXNzPSJjb2wtNiBjb2wtbWQtMyBvZmZzZXQtbWQtMyB0ZXh0LW1kLXJpZ2h0Ij4KICAgICAgICA8aDU+TmF2aWdhdGlvbjwvaDU+CiAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIHRleHQtc21hbGwiPgogICAgICAgICAge3sgaWYgLlNwYWNlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnRUeXBlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Ib29rIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Db250ZW50IH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9jb250ZW50dHlwZS97eyAuU3BhY2UuSUR9fS97eyAuQ29udGVudFR5cGUuSUQgfX0nPnt7IC5Db250ZW50VHlwZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIGFuZCAuU3BhY2UgKG5vdCAuQ29udGVudFR5cGUpIChub3QgLkhvb2spIH19CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjY29weU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+Q29weTwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgYW5kIC5Db250ZW50VHlwZSAobm90IC5Db250ZW50KSB9fQogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnQgfX0KICAgICAgICAgICAgPGxpPjxpbnB1dCB0eXBlPXN1Ym1pdCBjbGFzcz0idGV4dC1kZWNvcmF0aW9uLW5vbmUgbS0wIHAtMCBidG4gYnRuLWxpbmsgdGV4dC1tdXRlZCBib3JkZXItMCIgdmFsdWU9U2F2ZSAvPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkhvb2sgfX0KICAgICAgICAgICAgPGxpPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNkZWxldGVNb2RhbCIgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgICAge3sgZW5kIH19CiAgICAgICAgICB7eyBpZiAuVXNlciB9fQogICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgPGZvcm0gbWV0aG9kPVBPU1QgYWN0aW9uPScvdXNlci9sb2dvdXQnIGVuY3R5cGU9J211bHRpcGFydC9mb3JtLWRhdGEnPgogICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJ0ZXh0LWRlY29yYXRpb24tbm9uZSBtLTAgcC0wIGJ0biBidG4tbGluayB0ZXh0LW11dGVkIGJvcmRlci0wIiB2YWx1ZT1Mb2dvdXQgLz4KICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgIHt7aWYgYW5kIC5Vc2VyICguVXNlciB8IHBhaWQpfX0KICAgICAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjaW52aXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5JbnZpdGU8L2E+PC9saT4KICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2JpbGxpbmcnPkJpbGxpbmc8L2E+PC9saT4KICAgICAgICAgIHt7IGVsc2UgfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvJz5Ib21lPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLyNzaWdudXAnPlNpZ251cDwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jbG9naW4nPkxvZ2luPC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvL2dpdC5zci5odC9+ZXZhbmovY21zJz5Tb3VyY2U8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLy9naXQuc3IuaHQvfmV2YW5qL2Ntcy90cmVlL21hc3Rlci9MSUNFTlNFJz5MaWNlbnNlPC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9ImNvbC02IGNvbC1tZC0zIj4KICAgICAgICA8aDU+UmVzb3VyY2VzPC9oNT4KICAgICAgICA8dWwgY2xhc3M9Imxpc3QtdW5zdHlsZWQgdGV4dC1zbWFsbCI+CiAgICAgICAgICB7e2lmIC5Vc2VyfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvcGFnZS9iaWxsaW5nJz5CaWxsaW5nPC9hPjwvbGk+CiAgICAgICAgICB7e2Vsc2V9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jcHJpY2luZyc+UHJpY2luZzwvYT48L2xpPgogICAgICAgICAge3tlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvdG91cic+VG91cjwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvZG9jJz5Eb2NzPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9mYXEiPkZBUTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvdGVybXMiPlRlcm1zPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9wcml2YWN5Ij5Qcml2YWN5PC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9jb250YWN0Ij5Db250YWN0PC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9cm93PgogICAgICA8cCBjbGFzcz0ndGV4dC1tdXRlZCB0ZXh0LWNlbnRlciBtdC01IHctMTAwIHRleHQtdHJ1bmNhdGUgb3ZlcmZsb3ctaGlkZGVuJz52Lnt7LkJ1aWxkfX08L3A+CiAgICA8L2Rpdj4KICA8L2Zvb3Rlcj4KPC9kaXY+Cgp7e2lmIGFuZCAuVXNlciAoLlVzZXIgfCBwYWlkKX19Cjxmb3JtIG1ldGhvZD1QT1NUIGFjdGlvbj0nL2ludml0ZScgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgPGlucHV0IHR5cGU9aGlkZGVuIG5hbWU9bWV0aG9kIHZhbHVlPVBPU1QgLz4KICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT1tZXRob2QgdmFsdWU9Int7LlVzZXIuT3JnLklEfX0iIC8+CiAgPGRpdiBjbGFzcz0ibW9kYWwgZmFkZSIgaWQ9Imludml0ZU1vZGFsIiB0YWJpbmRleD0iLTEiIHJvbGU9ImRpYWxvZyIgYXJpYS1sYWJlbGxlZGJ5PSJleGFtcGxlTW9kYWxMYWJlbCIgYXJpYS1oaWRkZW49InRydWUiPgogICAgPGRpdiBjbGFzcz0ibW9kYWwtZGlhbG9nIG1vZGFsLWRpYWxvZy1zY3JvbGxhYmxlIj4KICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtY29udGVudCI+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtaGVhZGVyIj4KICAgICAgICAgIDxoNSBjbGFzcz0ibW9kYWwtdGl0bGUiIGlkPSJleGFtcGxlTW9kYWxMYWJlbCI+SW52aXRlIHNvbWVvbmUgdG8geW91ciBzcGFjZShzKTwvaDU+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9ImJ1dHRvbiIgY2xhc3M9ImNsb3NlIiBkYXRhLWRpc21pc3M9Im1vZGFsIiBhcmlhLWxhYmVsPSJDbG9zZSI+CiAgICAgICAgICAgIDxzcGFuIGFyaWEtaGlkZGVuPSJ0cnVlIj4mdGltZXM7PC9zcGFuPgogICAgICAgICAgPC9idXR0b24+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtYm9keSI+CiAgICAgICAgICA8cD5XZSdsbCBnZW5lcmF0ZSBhIHNwZWNpYWwgbGluayBmb3IgeW91LiBTZW5kIHRoaXMgdG8geW91ciBmcmllbmQsIAogICAgICAgICAgY293b3JrZXIsIG9yIHdob2V2ZXIhPC9wPgogICAgICAgICAgPHA+VGhlIGludml0ZSB3aWxsIG9ubHkgYmUgYWN0aXZlIGZvciBvbmUgaG91ci48L3A+CiAgICAgICAgICA8ZGl2IGNsYXNzPSdmb3JtLWdyb3VwIHJvdyc+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J2NvbC0xMic+CiAgICAgICAgICAgICAgPGxhYmVsIGZvcj1yb2xlPlVzZXIncyBkZXNpcmVkIHJvbGU8L2xhYmVsPgogICAgICAgICAgICAgIDxzZWxlY3QgaWQ9cm9sZSBjbGFzcz0idy0xMDAgZm9ybS1jb250cm9sIiBuYW1lPXJvbGUgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICA8b3B0aW9uIGRpc2FibGVkIHZhbHVlPlJvbGU8L29wdGlvbj4KICAgICAgICAgICAgICAgIHt7cmFuZ2UgLlJvbGVzfX0KICAgICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9Int7Lk5hbWV9fSI+e3suTmFtZX19PC9vcHRpb24+CiAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgPC9zZWxlY3Q+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtZm9vdGVyIj4KICAgICAgICAgIDxidXR0b24gdHlwZT0iYnV0dG9uIiBjbGFzcz0iYnRuIGJ0bi1zZWNvbmRhcnkiIGRhdGEtZGlzbWlzcz0ibW9kYWwiPkNsb3NlPC9idXR0b24+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSI+R288L2J1dHRvbj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICA8L2Rpdj4KPC9mb3JtPgp7e2VuZH19Cgp7e2lmIC5BfX0KPGltZyBzdHlsZT0ncG9zaXRpb246IGZpeGVkOyBib3R0b206IDA7IHJpZ2h0OiAwOycgc3JjPSIvL3NraXBwZXJjbXMuZ29hdGNvdW50ZXIuY29tL2NvdW50P3A9e3suQS5QYXRofX17e2lmIC5BLlJlZmVycmVyfX0mcj17ey5BLlJlZmVycmVyfX17e2VuZH19JnJuZD17ey5BLlJORH19Ij4Ke3tlbmR9fQo=")
	tmpls["html/_footer.html"] = tostring("PGRpdiBjbGFzcz1jb250YWluZXI+CiAgPGZvb3RlciBjbGFzcz0icHQtNCBteS1tZC01IHB0LW1kLTUgYm9yZGVyLXRvcCI+CiAgICA8ZGl2IGNsYXNzPSJyb3ciPgogICAgICA8ZGl2IGNsYXNzPSJjb2wtNiBjb2wtbWQtMyBvZmZzZXQtbWQtMyB0ZXh0LW1kLXJpZ2h0Ij4KICAgICAgICA8aDU+TmF2aWdhdGlvbjwvaDU+CiAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIHRleHQtc21hbGwiPgogICAgICAgICAge3sgaWYgLlNwYWNlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnRUeXBlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Ib29rIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Db250ZW50IH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9jb250ZW50dHlwZS97eyAuU3BhY2UuSUR9fS97eyAuQ29udGVudFR5cGUuSUQgfX0nPnt7IC5Db250ZW50VHlwZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIGFuZCAuU3BhY2UgKG5vdCAuQ29udGVudFR5cGUpIChub3QgLkhvb2spIH19CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjY29weU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+Q29weTwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgYW5kIC5Db250ZW50VHlwZSAobm90IC5Db250ZW50KSB9fQogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnQgfX0KICAgICAgICAgICAgPGxpPjxpbnB1dCB0eXBlPXN1Ym1pdCBjbGFzcz0idGV4dC1kZWNvcmF0aW9uLW5vbmUgbS0wIHAtMCBidG4gYnRuLWxpbmsgdGV4dC1tdXRlZCBib3JkZXItMCIgdmFsdWU9U2F2ZSAvPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkhvb2sgfX0KICAgICAgICAgICAgPGxpPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNkZWxldGVNb2RhbCIgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgICAge3sgZW5kIH19CiAgICAgICAgICB7eyBpZiAuVXNlciB9fQogICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgPGZvcm0gbWV0aG9kPVBPU1QgYWN0aW9uPScvdXNlci9sb2dvdXQnIGVuY3R5cGU9J211bHRpcGFydC9mb3JtLWRhdGEnPgogICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJ0ZXh0LWRlY29yYXRpb24tbm9uZSBtLTAgcC0wIGJ0biBidG4tbGluayB0ZXh0LW11dGVkIGJvcmRlci0wIiB2YWx1ZT1Mb2dvdXQgLz4KICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgIHt7aWYgYW5kIC5Vc2VyICguVXNlciB8IHBhaWQpfX0KICAgICAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjaW52aXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5JbnZpdGU8L2E+PC9saT4KICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2JpbGxpbmcnPkJpbGxpbmc8L2E+PC9saT4KICAgICAgICAgIHt7IGVsc2UgfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvJz5Ib21lPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLyNzaWdudXAnPlNpZ251cDwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jbG9naW4nPkxvZ2luPC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvL2dpdC5zci5odC9+ZXZhbmovY21zJz5Tb3VyY2U8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLy9naXQuc3IuaHQvfmV2YW5qL2Ntcy90cmVlL21hc3Rlci9MSUNFTlNFJz5MaWNlbnNlPC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9ImNvbC02IGNvbC1tZC0zIj4KICAgICAgICA8aDU+UmVzb3VyY2VzPC9oNT4KICAgICAgICA8dWwgY2xhc3M9Imxpc3QtdW5zdHlsZWQgdGV4dC1zbWFsbCI+CiAgICAgICAgICB7e2lmIC5Vc2VyfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvcGFnZS9iaWxsaW5nJz5CaWxsaW5nPC9hPjwvbGk+CiAgICAgICAgICB7e2Vsc2V9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jcHJpY2luZyc+UHJpY2luZzwvYT48L2xpPgogICAgICAgICAge3tlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvdG91cic+VG91cjwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvZG9jJz5Eb2NzPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9mYXEiPkZBUTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvdGVybXMiPlRlcm1zPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9wcml2YWN5Ij5Qcml2YWN5PC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9jb250YWN0Ij5Db250YWN0PC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9cm93PgogICAgICA8cCBjbGFzcz0ndGV4dC1tdXRlZCB0ZXh0LWNlbnRlciBtdC01IHctMTAwIHRleHQtdHJ1bmNhdGUgb3ZlcmZsb3ctaGlkZGVuJz52Lnt7LkJ1aWxkfX08L3A+CiAgICA8L2Rpdj4KICA8L2Zvb3Rlcj4KPC9kaXY+Cgp7e2lmIGFuZCAuVXNlciAoLlVzZXIgfCBwYWlkKX19Cjxmb3JtIG1ldGhvZD1QT1NUIGFjdGlvbj0nL2ludml0ZScgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgPGlucHV0IHR5cGU9aGlkZGVuIG5hbWU9bWV0aG9kIHZhbHVlPVBPU1QgLz4KICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT1tZXRob2QgdmFsdWU9Int7LlVzZXIuT3JnLklEfX0iIC8+CiAgPGRpdiBjbGFzcz0ibW9kYWwgZmFkZSIgaWQ9Imludml0ZU1vZGFsIiB0YWJpbmRleD0iLTEiIHJvbGU9ImRpYWxvZyIgYXJpYS1sYWJlbGxlZGJ5PSJleGFtcGxlTW9kYWxMYWJlbCIgYXJpYS1oaWRkZW49InRydWUiPgogICAgPGRpdiBjbGFzcz0ibW9kYWwtZGlhbG9nIG1vZGFsLWRpYWxvZy1zY3JvbGxhYmxlIj4KICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtY29udGVudCI+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtaGVhZGVyIj4KICAgICAgICAgIDxoNSBjbGFzcz0ibW9kYWwtdGl0bGUiIGlkPSJleGFtcGxlTW9kYWxMYWJlbCI+SW52aXRlIHNvbWVvbmUgdG8geW91ciBzcGFjZShzKTwvaDU+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9ImJ1dHRvbiIgY2xhc3M9ImNsb3NlIiBkYXRhLWRpc21pc3M9Im1vZGFsIiBhcmlhLWxhYmVsPSJDbG9zZSI+CiAgICAgICAgICAgIDxzcGFuIGFyaWEtaGlkZGVuPSJ0cnVlIj4mdGltZXM7PC9zcGFuPgogICAgICAgICAgPC9idXR0b24+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtYm9keSI+CiAgICAgICAgICA8cD5XZSdsbCBnZW5lcmF0ZSBhIHNwZWNpYWwgbGluayBmb3IgeW91LiBTZW5kIHRoaXMgdG8geW91ciBmcmllbmQsIAogICAgICAgICAgY293b3JrZXIsIG9yIHdob2V2ZXIhPC9wPgogICAgICAgICAgPHA+VGhlIGludml0ZSB3aWxsIG9ubHkgYmUgYWN0aXZlIGZvciBvbmUgaG91ci48L3A+CiAgICAgICAgICA8ZGl2IGNsYXNzPSdmb3JtLWdyb3VwIHJvdyc+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J2NvbC0xMic+CiAgICAgICAgICAgICAgPGxhYmVsIGZvcj1yb2xlPlVzZXIncyBkZXNpcmVkIHJvbGU8L2xhYmVsPgogICAgICAgICAgICAgIDxzZWxlY3QgaWQ9cm9sZSBjbGFzcz0idy0xMDAgZm9ybS1jb250cm9sIiBuYW1lPXJvbGUgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICA8b3B0aW9uIGRpc2FibGVkIHZhbHVlPlJvbGU8L29wdGlvbj4KICAgICAgICAgICAgICAgIHt7cmFuZ2UgLlJvbGVzfX0KICAgICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9Int7Lk5hbWV9fSI+e3suTmFtZX19PC9vcHRpb24+CiAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgPC9zZWxlY3Q+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8cCBjbGFzcz0nbXQtMyBtYi0wJz5Gb3IgbW9yZSBpbmZvcm1hdGlvbiBhYm91dCB3aGF0IGVhY2ggc3BlY2lmaWMgcm9sZSBpcyBhbGxvd2VkIAogICAgICAgICAgPGEgdGFyZ2V0PSdfYmxhbmsnIGhyZWY9Jy9yb2xlcyc+cGxlYXNlIHJlYWQgdGhlIHJvbGVzIGd1aWRlLjwvYT48L3A+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtZm9vdGVyIj4KICAgICAgICAgIDxidXR0b24gdHlwZT0iYnV0dG9uIiBjbGFzcz0iYnRuIGJ0bi1zZWNvbmRhcnkiIGRhdGEtZGlzbWlzcz0ibW9kYWwiPkNsb3NlPC9idXR0b24+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSI+R288L2J1dHRvbj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICA8L2Rpdj4KPC9mb3JtPgp7e2VuZH19Cgp7e2lmIC5BfX0KPGltZyBzdHlsZT0ncG9zaXRpb246IGZpeGVkOyBib3R0b206IDA7IHJpZ2h0OiAwOycgc3JjPSIvL3NraXBwZXJjbXMuZ29hdGNvdW50ZXIuY29tL2NvdW50P3A9e3suQS5QYXRofX17e2lmIC5BLlJlZmVycmVyfX0mcj17ey5BLlJlZmVycmVyfX17e2VuZH19JnJuZD17ey5BLlJORH19Ij4Ke3tlbmR9fQo=")

	tmpls["html/_head.html"] = tostring("PG1ldGEgY2hhcnNldD0ndXRmLTgnPgo8bWV0YSBuYW1lPSd2aWV3cG9ydCcgY29udGVudD0nd2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEnPgo8bGluayByZWw9J2ljb24nIHR5cGU9J2ltYWdlL3gtaWNvbicgaHJlZj0naHR0cHM6Ly9mYXZpY29uLmV2YW5qb24uZXMvMC8xMDUvMjE3LzMyL2Zhdmljb24uaWNvJyAvPgo8bGluayByZWw9J3N0eWxlc2hlZXQnIGhyZWY9Jy9zdGF0aWMvY3NzL2Jvb3RzdHJhcC5taW4uY3NzJyAvPgo=")


M main.go => main.go +8 -0
@@ 23,6 23,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/c/user"
	"git.sr.ht/~evanj/cms/internal/s/cache"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"git.sr.ht/~evanj/cms/internal/s/email"
	webhook "git.sr.ht/~evanj/cms/internal/s/hook"
	"git.sr.ht/~evanj/cms/internal/s/rbac"
	"git.sr.ht/~evanj/cms/internal/s/rl"


@@ 57,6 58,10 @@ var (
	dynamicContentURL         = os.Getenv("DYNAMIC_CONTENT_URL")
	dynamicContentSpace       = mustInt(os.Getenv("DYNAMIC_CONTENT_SPACE"))
	dynamicContentContentType = mustInt(os.Getenv("DYNAMIC_CONTENT_CONTENTTYPE"))
	emailUser                 = os.Getenv("EMAIL_USER")
	emailPass                 = os.Getenv("EMAIL_PASS")
	emailDomain               = os.Getenv("EMAIL_DOMAIN")
	emailPort                 = mustInt(os.Getenv("EMAIL_PORT"))
)

func mustInt(val string) int {


@@ 72,6 77,8 @@ func main() {
		w         = os.Stdout
		applogger = log.New(w, "[cms] ", 0)

		emailer = email.New(emailUser, emailPass, emailUser, emailDomain, emailPort)

		db = db.New(
			log.New(w, "[cms:db] ", 0),
			dbtype,


@@ 130,6 137,7 @@ func main() {
					rbac,
					signupEnabled,
					libs,
					emailer,
				),
				"hook": hook.New(
					c,