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 => +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,