~evanj/cms

cfe18d3a080d0764c64b5b263c8d2168d8beee8a — Evan M Jones 10 months ago 41280cc
Feat(password): Password update on billing page.
M TODO => TODO +2 -3
@@ 14,7 14,6 @@ Optional memcached
When editing existing references don't blow away prev inputs
Restrict API requests for free users (limit users<->org)
Forgot password
Upgrade
Cancel
Update password
Warn & delete excess users/spaces on downgrade.
Fix styling on Billing page.
Styling on Billing page.

M internal/c/user/user.go => internal/c/user/user.go +19 -0
@@ 33,6 33,7 @@ type dber interface {
	UserGet(username, password string) (user.User, error)
	UserGetFromToken(token string) (user.User, error)
	UserSetEmail(u user.User, email string) (user.User, error)
	UserSetPassword(u user.User, current, password, verify string) (user.User, error)
	SpacesPerUser(user user.User, before int) (space.SpaceList, error)
}



@@ 152,6 153,21 @@ func (l *User) updateEmail(w http.ResponseWriter, r *http.Request) {
	l.Redirect(w, r, "/page/billing")
}

func (l *User) updatePassword(w http.ResponseWriter, r *http.Request) {
	u, err := l.GetCookieUser(w, r)
	if err != nil {
		l.Error2(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		return
	}

	if _, err := l.db.UserSetPassword(u, r.FormValue("current"), r.FormValue("password"), r.FormValue("verify")); err != nil {
		l.Error2(w, r, http.StatusInternalServerError, err)
		return
	}

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

func (l *User) updateBilling(w http.ResponseWriter, r *http.Request) {
	u, err := l.GetCookieUser(w, r)
	if err != nil {


@@ 216,6 232,9 @@ func (l *User) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "/user/update/email":
		l.updateEmail(w, r)
		return
	case "/user/update/password":
		l.updatePassword(w, r)
		return
	case "/user/update/billing":
		l.updateBilling(w, r)
		return

M internal/s/db/user.go => internal/s/db/user.go +36 -0
@@ 60,6 60,10 @@ func (db *DB) UserNew(username, password, verifyPassword string) (user.User, err
}

func (db *DB) userNew(t *sql.Tx, username, password, verifyPassword string) (user.User, error) {
	if password == "" {
		return nil, fmt.Errorf("no password entered")
	}

	if password != verifyPassword {
		return nil, fmt.Errorf("passwords do not match")
	}


@@ 161,6 165,38 @@ func (db *DB) UserSetEmail(u user.User, email string) (user.User, error) {
	return db.UserGetFromToken(u.Token())
}

func (db *DB) UserSetPassword(u user.User, current, password, verifyPassword string) (user.User, error) {
	var currentHash string
	if err := db.QueryRow("SELECT HASH FROM cms_user WHERE ID=?", u.ID()).Scan(&currentHash); err != nil {
		return nil, err
	}

	if err := db.sec.HashCompare(u.Name(), current, currentHash); err != nil {
		return nil, fmt.Errorf("your previous password is incorrect")
	}

	if password == current {
		return nil, fmt.Errorf("this is already your password")
	}
	if password == "" {
		return nil, fmt.Errorf("no password entered")
	}
	if password != verifyPassword {
		return nil, fmt.Errorf("passwords do not match")
	}

	hash, err := db.sec.HashCreate(u.Name(), password)
	if err != nil {
		return nil, fmt.Errorf("failed to create password hash")
	}

	if _, err := db.Exec("UPDATE cms_user SET HASH=? WHERE ID=?", hash, u.ID()); err != nil {
		return nil, err
	}

	return db.UserGet(u.Name(), password)
}

func (u *User) ID() string     { return u.UserID }
func (u *User) Name() string   { return u.UserName }
func (u *User) Token() string  { return u.userToken }

M internal/v/html/billing.html => internal/v/html/billing.html +12 -0
@@ 27,6 27,18 @@
              <input id=email name=email type=email class="mb-3 form-control" placeholder="email" required {{if .User.HasEmail}}value="{{.User.Email}}"{{end}}>
              <button type="submit" class="btn btn-primary">Go</button>
            </form>
            <div class='text-center mt-3'>
              <p>Update your password.</p>
            </div>
            <form action='/user/update/password' method=POST>
              <label for=current >Current Password</label>
              <input id=current name=current type=password class="mb-3 form-control" placeholder="current" required>
              <label for=password>New Password</label>
              <input id=password name=password type=password class="mb-3 form-control" placeholder="password" required>
              <label for=verify>Verify</label>
              <input id=verify name=verify type=password class="mb-3 form-control" placeholder="verify" required>
              <button type="submit" class="btn btn-primary">Go</button>
            </form>
          </div>
          <div class="col-12 col-md-6 mb-5">
            {{if .User | paid}}

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

	tmpls["html/_scripts.html"] = tostring("PHNjcmlwdCBzcmM9Jy9zdGF0aWMvanMvcG9wcGVyLm1pbi5qcyc+PC9zY3JpcHQ+CjxzY3JpcHQgc3JjPScvc3RhdGljL2pzL2Jvb3RzdHJhcC5taW4uanMnPjwvc2NyaXB0Pgo=")

	tmpls["html/billing.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPkNNUyB8IEJpbGxpbmc8L3RpdGxlPgo8L2hlYWQ+Cjxib2R5IGNsYXNzPSdwYWdlIGJnLWxpZ2h0Jz4KICA8c3R5bGU+e3sgdGVtcGxhdGUgImNzcy9tYWluLmNzcyIgfX08L3N0eWxlPgogIDxtYWluPgogICAge3sgdGVtcGxhdGUgImh0bWwvX2hlYWRlci5odG1sIiAkIH19CiAgICA8ZGl2IGNsYXNzPSJwcmljaW5nLWhlYWRlciBweC0zIHB5LTMgcHQtbWQtNSBwYi1tZC00IG14LWF1dG8gdGV4dC1jZW50ZXIiPgogICAgICA8aDEgY2xhc3M9ImRpc3BsYXktNCI+QmlsbGluZzwvaDE+CiAgICA8L2Rpdj4KICAgIHt7aWYgLlVzZXJ9fQogICAgICA8ZGl2IGNsYXNzPSdjb250YWluZXInPgogICAgICAgIDxkaXYgY2xhc3M9J3Jvdyc+CiAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtMTIgY29sLW1kLTYgbWItNSI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J3RleHQtY2VudGVyJz4KICAgICAgICAgICAgICB7e2lmIC5Vc2VyLkhhc0VtYWlsfX0KICAgICAgICAgICAgICA8cD5VcGRhdGUgeW91ciBlbWFpbC48L3A+CiAgICAgICAgICAgICAge3tlbHNlfX0KICAgICAgICAgICAgICA8cD5TZXQgeW91ciBlbWFpbCBpbiBjYXNlIHlvdSBnZXQgbG9ja2VkIG91dCBvZiB5b3VyIGFjY291bnQuPC9wPgogICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDxmb3JtIGFjdGlvbj0nL3VzZXIvdXBkYXRlL2VtYWlsJyBtZXRob2Q9UE9TVD4KICAgICAgICAgICAgICA8bGFiZWwgZm9yPWVtYWlsPkVtYWlsPC9sYWJlbD4KICAgICAgICAgICAgICA8aW5wdXQgaWQ9ZW1haWwgbmFtZT1lbWFpbCB0eXBlPWVtYWlsIGNsYXNzPSJtYi0zIGZvcm0tY29udHJvbCIgcGxhY2Vob2xkZXI9ImVtYWlsIiByZXF1aXJlZCB7e2lmIC5Vc2VyLkhhc0VtYWlsfX12YWx1ZT0ie3suVXNlci5FbWFpbH19Int7ZW5kfX0+CiAgICAgICAgICAgICAgPGJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzPSJidG4gYnRuLXByaW1hcnkiPkdvPC9idXR0b24+CiAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPGRpdiBjbGFzcz0iY29sLTEyIGNvbC1tZC02IG1iLTUiPgogICAgICAgICAgICB7e2lmIC5Vc2VyIHwgcGFpZH19CiAgICAgICAgICAgICAgPGZvcm0gYWN0aW9uPScvdXNlci9jYW5jZWwvYmlsbGluZycgbWV0aG9kPVBPU1QgY2xhc3M9InRleHQtY2VudGVyIj4KICAgICAgICAgICAgICAgIDxwIGNsYXNzPSdtYi01Jz5DYW5jZWwgeW91ciB7ey5Vc2VyLk9yZy5UaWVyLk5hbWV9fSBzdWJzY3JpcHRpb24uPC9wPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iZC1pbmxpbmUtYmxvY2siPgogICAgICAgICAgICAgICAgICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT10aWVyIHZhbHVlPSJ7ey5Vc2VyLk9yZy5UaWVyLk5hbWV9fSIgLz4KICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZCBtYi00IHNoYWRvdy1zbSI+CiAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICA8aDQgY2xhc3M9Im15LTAgZm9udC13ZWlnaHQtbm9ybWFsIj57ey5Vc2VyLk9yZy5UaWVyLk5hbWV9fTwvaDQ+CiAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgIDxoMSBjbGFzcz0iY2FyZC10aXRsZSBwcmljaW5nLWNhcmQtdGl0bGUiPnt7LlVzZXIuT3JnLlRpZXIuUHJpY2V9fSA8c21hbGwgY2xhc3M9InRleHQtbXV0ZWQiPi8ge3suVXNlci5PcmcuVGllci5UaW1lVW5pdH19PC9zbWFsbD48L2gxPgogICAgICAgICAgICAgICAgICAgIDx1bCBjbGFzcz0ibGlzdC11bnN0eWxlZCBtdC0zIG1iLTQiPgogICAgICAgICAgICAgICAgICAgICAge3tyYW5nZSAuVXNlci5PcmcuVGllci5PcHRzfX0KICAgICAgICAgICAgICAgICAgICAgICAgPGxpPnt7LlRleHR9fTwvbGk+CiAgICAgICAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgICAgICAgPC91bD4KICAgICAgICAgICAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSB3LTEwMCI+Q2FuY2VsPC9idXR0b24+CiAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgPC9mb3JtPgogICAgICAgICAgICB7e2Vsc2V9fQogICAgICAgICAgICAgIDxwIGNsYXNzPSd0ZXh0LWNlbnRlciBtYi01Jz5VcGdyYWRlIHRvIGEgcGFpZCB0aWVyLjxicj5HZXQgbW9yZSBhY2Nlc3MuPC9wPgogICAgICAgICAgICAgIDxkaXYgY2xhc3M9InJvdyByb3ctY29scy0xIHJvdy1jb2xzLW1kLTIgcm93LWNvbHMtbGctMyBtYi01IHRleHQtY2VudGVyIj4KICAgICAgICAgICAgICAgIHt7cmFuZ2UgLlRpZXJzfX0KICAgICAgICAgICAgICAgICAge3tpZiBub3QgKC58aXNGcmVlKX19CiAgICAgICAgICAgICAgICAgICAgPGZvcm0gYWN0aW9uPScvdXNlci91cGRhdGUvYmlsbGluZycgbWV0aG9kPVBPU1QgY2xhc3M9ImNvbCI+CiAgICAgICAgICAgICAgICAgICAgICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT10aWVyIHZhbHVlPSJ7ey5OYW1lfX0iIC8+CiAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIG1iLTQgc2hhZG93LXNtIj4KICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGg0IGNsYXNzPSJteS0wIGZvbnQtd2VpZ2h0LW5vcm1hbCI+e3suTmFtZX19PC9oND4KICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPGgxIGNsYXNzPSJjYXJkLXRpdGxlIHByaWNpbmctY2FyZC10aXRsZSI+e3suUHJpY2V9fSA8c21hbGwgY2xhc3M9InRleHQtbXV0ZWQiPi8ge3suVGltZVVuaXR9fTwvc21hbGw+PC9oMT4KICAgICAgICAgICAgICAgICAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIG10LTMgbWItNCI+CiAgICAgICAgICAgICAgICAgICAgICAgICAge3tyYW5nZSAuT3B0c319CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8bGk+e3suVGV4dH19PC9saT4KICAgICAgICAgICAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdWw+CiAgICAgICAgICAgICAgICAgICAgICAgIDxidXR0b24gdHlwZT0ic3VibWl0IiBjbGFzcz0iYnRuIGJ0bi1wcmltYXJ5IHctMTAwIj5HbzwvYnV0dG9uPgogICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPC9mb3JtPgogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIHt7ZWxzZX19CiAgICAgIDxkaXYgY2xhc3M9J2NvbnRhaW5lcic+CiAgICAgICAgPGRpdiBjbGFzcz0ncm93Jz4KICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbC0xMiI+CiAgICAgICAgICAgIDxoMT5Pb3BzPC9oMT4KICAgICAgICAgICAgPHA+U29ycnksIG91ciBkZXZlbG9wZXJzIGFyZSBsYXp5LiBUaGlzIHNob3VsZCByZWFsbHkgcmVkaXJlY3QgeW91LjwvcD4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIHt7ZW5kfX0KICAgIHt7IHRlbXBsYXRlICJodG1sL19mb290ZXIuaHRtbCIgJCB9fQogIDwvbWFpbj4KICB7eyB0ZW1wbGF0ZSAiaHRtbC9fc2NyaXB0cy5odG1sIiB9fQo8L2JvZHk+CjwvaHRtbD4K")
	tmpls["html/billing.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPkNNUyB8IEJpbGxpbmc8L3RpdGxlPgo8L2hlYWQ+Cjxib2R5IGNsYXNzPSdwYWdlIGJnLWxpZ2h0Jz4KICA8c3R5bGU+e3sgdGVtcGxhdGUgImNzcy9tYWluLmNzcyIgfX08L3N0eWxlPgogIDxtYWluPgogICAge3sgdGVtcGxhdGUgImh0bWwvX2hlYWRlci5odG1sIiAkIH19CiAgICA8ZGl2IGNsYXNzPSJwcmljaW5nLWhlYWRlciBweC0zIHB5LTMgcHQtbWQtNSBwYi1tZC00IG14LWF1dG8gdGV4dC1jZW50ZXIiPgogICAgICA8aDEgY2xhc3M9ImRpc3BsYXktNCI+QmlsbGluZzwvaDE+CiAgICA8L2Rpdj4KICAgIHt7aWYgLlVzZXJ9fQogICAgICA8ZGl2IGNsYXNzPSdjb250YWluZXInPgogICAgICAgIDxkaXYgY2xhc3M9J3Jvdyc+CiAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtMTIgY29sLW1kLTYgbWItNSI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J3RleHQtY2VudGVyJz4KICAgICAgICAgICAgICB7e2lmIC5Vc2VyLkhhc0VtYWlsfX0KICAgICAgICAgICAgICA8cD5VcGRhdGUgeW91ciBlbWFpbC48L3A+CiAgICAgICAgICAgICAge3tlbHNlfX0KICAgICAgICAgICAgICA8cD5TZXQgeW91ciBlbWFpbCBpbiBjYXNlIHlvdSBnZXQgbG9ja2VkIG91dCBvZiB5b3VyIGFjY291bnQuPC9wPgogICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDxmb3JtIGFjdGlvbj0nL3VzZXIvdXBkYXRlL2VtYWlsJyBtZXRob2Q9UE9TVD4KICAgICAgICAgICAgICA8bGFiZWwgZm9yPWVtYWlsPkVtYWlsPC9sYWJlbD4KICAgICAgICAgICAgICA8aW5wdXQgaWQ9ZW1haWwgbmFtZT1lbWFpbCB0eXBlPWVtYWlsIGNsYXNzPSJtYi0zIGZvcm0tY29udHJvbCIgcGxhY2Vob2xkZXI9ImVtYWlsIiByZXF1aXJlZCB7e2lmIC5Vc2VyLkhhc0VtYWlsfX12YWx1ZT0ie3suVXNlci5FbWFpbH19Int7ZW5kfX0+CiAgICAgICAgICAgICAgPGJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzPSJidG4gYnRuLXByaW1hcnkiPkdvPC9idXR0b24+CiAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgICAgPGRpdiBjbGFzcz0ndGV4dC1jZW50ZXIgbXQtMyc+CiAgICAgICAgICAgICAgPHA+VXBkYXRlIHlvdXIgcGFzc3dvcmQuPC9wPgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGZvcm0gYWN0aW9uPScvdXNlci91cGRhdGUvcGFzc3dvcmQnIG1ldGhvZD1QT1NUPgogICAgICAgICAgICAgIDxsYWJlbCBmb3I9Y3VycmVudCA+Q3VycmVudCBQYXNzd29yZDwvbGFiZWw+CiAgICAgICAgICAgICAgPGlucHV0IGlkPWN1cnJlbnQgbmFtZT1jdXJyZW50IHR5cGU9cGFzc3dvcmQgY2xhc3M9Im1iLTMgZm9ybS1jb250cm9sIiBwbGFjZWhvbGRlcj0iY3VycmVudCIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgPGxhYmVsIGZvcj1wYXNzd29yZD5OZXcgUGFzc3dvcmQ8L2xhYmVsPgogICAgICAgICAgICAgIDxpbnB1dCBpZD1wYXNzd29yZCBuYW1lPXBhc3N3b3JkIHR5cGU9cGFzc3dvcmQgY2xhc3M9Im1iLTMgZm9ybS1jb250cm9sIiBwbGFjZWhvbGRlcj0icGFzc3dvcmQiIHJlcXVpcmVkPgogICAgICAgICAgICAgIDxsYWJlbCBmb3I9dmVyaWZ5PlZlcmlmeTwvbGFiZWw+CiAgICAgICAgICAgICAgPGlucHV0IGlkPXZlcmlmeSBuYW1lPXZlcmlmeSB0eXBlPXBhc3N3b3JkIGNsYXNzPSJtYi0zIGZvcm0tY29udHJvbCIgcGxhY2Vob2xkZXI9InZlcmlmeSIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgPGJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzPSJidG4gYnRuLXByaW1hcnkiPkdvPC9idXR0b24+CiAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPGRpdiBjbGFzcz0iY29sLTEyIGNvbC1tZC02IG1iLTUiPgogICAgICAgICAgICB7e2lmIC5Vc2VyIHwgcGFpZH19CiAgICAgICAgICAgICAgPGZvcm0gYWN0aW9uPScvdXNlci9jYW5jZWwvYmlsbGluZycgbWV0aG9kPVBPU1QgY2xhc3M9InRleHQtY2VudGVyIj4KICAgICAgICAgICAgICAgIDxwIGNsYXNzPSdtYi01Jz5DYW5jZWwgeW91ciB7ey5Vc2VyLk9yZy5UaWVyLk5hbWV9fSBzdWJzY3JpcHRpb24uPC9wPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iZC1pbmxpbmUtYmxvY2siPgogICAgICAgICAgICAgICAgICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT10aWVyIHZhbHVlPSJ7ey5Vc2VyLk9yZy5UaWVyLk5hbWV9fSIgLz4KICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZCBtYi00IHNoYWRvdy1zbSI+CiAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICA8aDQgY2xhc3M9Im15LTAgZm9udC13ZWlnaHQtbm9ybWFsIj57ey5Vc2VyLk9yZy5UaWVyLk5hbWV9fTwvaDQ+CiAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgIDxoMSBjbGFzcz0iY2FyZC10aXRsZSBwcmljaW5nLWNhcmQtdGl0bGUiPnt7LlVzZXIuT3JnLlRpZXIuUHJpY2V9fSA8c21hbGwgY2xhc3M9InRleHQtbXV0ZWQiPi8ge3suVXNlci5PcmcuVGllci5UaW1lVW5pdH19PC9zbWFsbD48L2gxPgogICAgICAgICAgICAgICAgICAgIDx1bCBjbGFzcz0ibGlzdC11bnN0eWxlZCBtdC0zIG1iLTQiPgogICAgICAgICAgICAgICAgICAgICAge3tyYW5nZSAuVXNlci5PcmcuVGllci5PcHRzfX0KICAgICAgICAgICAgICAgICAgICAgICAgPGxpPnt7LlRleHR9fTwvbGk+CiAgICAgICAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgICAgICAgPC91bD4KICAgICAgICAgICAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSB3LTEwMCI+Q2FuY2VsPC9idXR0b24+CiAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgPC9mb3JtPgogICAgICAgICAgICB7e2Vsc2V9fQogICAgICAgICAgICAgIDxwIGNsYXNzPSd0ZXh0LWNlbnRlciBtYi01Jz5VcGdyYWRlIHRvIGEgcGFpZCB0aWVyLjxicj5HZXQgbW9yZSBhY2Nlc3MuPC9wPgogICAgICAgICAgICAgIDxkaXYgY2xhc3M9InJvdyByb3ctY29scy0xIHJvdy1jb2xzLW1kLTIgcm93LWNvbHMtbGctMyBtYi01IHRleHQtY2VudGVyIj4KICAgICAgICAgICAgICAgIHt7cmFuZ2UgLlRpZXJzfX0KICAgICAgICAgICAgICAgICAge3tpZiBub3QgKC58aXNGcmVlKX19CiAgICAgICAgICAgICAgICAgICAgPGZvcm0gYWN0aW9uPScvdXNlci91cGRhdGUvYmlsbGluZycgbWV0aG9kPVBPU1QgY2xhc3M9ImNvbCI+CiAgICAgICAgICAgICAgICAgICAgICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT10aWVyIHZhbHVlPSJ7ey5OYW1lfX0iIC8+CiAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIG1iLTQgc2hhZG93LXNtIj4KICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGg0IGNsYXNzPSJteS0wIGZvbnQtd2VpZ2h0LW5vcm1hbCI+e3suTmFtZX19PC9oND4KICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPGgxIGNsYXNzPSJjYXJkLXRpdGxlIHByaWNpbmctY2FyZC10aXRsZSI+e3suUHJpY2V9fSA8c21hbGwgY2xhc3M9InRleHQtbXV0ZWQiPi8ge3suVGltZVVuaXR9fTwvc21hbGw+PC9oMT4KICAgICAgICAgICAgICAgICAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIG10LTMgbWItNCI+CiAgICAgICAgICAgICAgICAgICAgICAgICAge3tyYW5nZSAuT3B0c319CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8bGk+e3suVGV4dH19PC9saT4KICAgICAgICAgICAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdWw+CiAgICAgICAgICAgICAgICAgICAgICAgIDxidXR0b24gdHlwZT0ic3VibWl0IiBjbGFzcz0iYnRuIGJ0bi1wcmltYXJ5IHctMTAwIj5HbzwvYnV0dG9uPgogICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPC9mb3JtPgogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIHt7ZWxzZX19CiAgICAgIDxkaXYgY2xhc3M9J2NvbnRhaW5lcic+CiAgICAgICAgPGRpdiBjbGFzcz0ncm93Jz4KICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbC0xMiI+CiAgICAgICAgICAgIDxoMT5Pb3BzPC9oMT4KICAgICAgICAgICAgPHA+U29ycnksIG91ciBkZXZlbG9wZXJzIGFyZSBsYXp5LiBUaGlzIHNob3VsZCByZWFsbHkgcmVkaXJlY3QgeW91LjwvcD4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIHt7ZW5kfX0KICAgIHt7IHRlbXBsYXRlICJodG1sL19mb290ZXIuaHRtbCIgJCB9fQogIDwvbWFpbj4KICB7eyB0ZW1wbGF0ZSAiaHRtbC9fc2NyaXB0cy5odG1sIiB9fQo8L2JvZHk+CjwvaHRtbD4K")

	tmpls["html/contact.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPkNNUyB8IENvbnRhY3Q8L3RpdGxlPgo8L2hlYWQ+Cjxib2R5IGNsYXNzPSdwYWdlIGJnLWxpZ2h0Jz4KICA8c3R5bGU+e3sgdGVtcGxhdGUgImNzcy9tYWluLmNzcyIgfX08L3N0eWxlPgogIDxtYWluPgogICAge3sgdGVtcGxhdGUgImh0bWwvX2hlYWRlci5odG1sIiAkIH19CiAgICA8ZGl2IGNsYXNzPSJwcmljaW5nLWhlYWRlciBweC0zIHB5LTMgcHQtbWQtNSBwYi1tZC00IG14LWF1dG8gdGV4dC1jZW50ZXIiPgogICAgICA8aDEgY2xhc3M9ImRpc3BsYXktNCI+Q29udGFjdDwvaDE+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9J2NvbnRhaW5lcic+CiAgICAgIDxkaXYgY2xhc3M9J3Jvdyc+CiAgICAgICAgPGRpdiBjbGFzcz0iY29sLTEyIG9mZnNldC0wIGNvbC1sZy04IG9mZnNldC1sZy0yIj4KICAgICAgICAgIFRPRE8KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICAgIHt7IHRlbXBsYXRlICJodG1sL19mb290ZXIuaHRtbCIgJCB9fQogIDwvbWFpbj4KICB7eyB0ZW1wbGF0ZSAiaHRtbC9fc2NyaXB0cy5odG1sIiB9fQo8L2JvZHk+CjwvaHRtbD4K")