T319 user delete acctpull/460/head
@@ -20,6 +20,7 @@ import ( | |||||
"sync" | "sync" | ||||
"time" | "time" | ||||
"github.com/gorilla/csrf" | |||||
"github.com/gorilla/mux" | "github.com/gorilla/mux" | ||||
"github.com/gorilla/sessions" | "github.com/gorilla/sessions" | ||||
"github.com/guregu/null/zero" | "github.com/guregu/null/zero" | ||||
@@ -1082,6 +1083,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err | |||||
HasPass bool | HasPass bool | ||||
IsLogOut bool | IsLogOut bool | ||||
Silenced bool | Silenced bool | ||||
CSRFField template.HTML | |||||
OauthSection bool | OauthSection bool | ||||
OauthAccounts []oauthAccountInfo | OauthAccounts []oauthAccountInfo | ||||
OauthSlack bool | OauthSlack bool | ||||
@@ -1098,6 +1100,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err | |||||
HasPass: passIsSet, | HasPass: passIsSet, | ||||
IsLogOut: r.FormValue("logout") == "1", | IsLogOut: r.FormValue("logout") == "1", | ||||
Silenced: fullUser.IsSilenced(), | Silenced: fullUser.IsSilenced(), | ||||
CSRFField: csrf.TemplateField(r), | |||||
OauthSection: displayOauthSection, | OauthSection: displayOauthSection, | ||||
OauthAccounts: oauthAccounts, | OauthAccounts: oauthAccounts, | ||||
OauthSlack: enableOauthSlack, | OauthSlack: enableOauthSlack, | ||||
@@ -1152,6 +1155,32 @@ func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) s | |||||
return s | return s | ||||
} | } | ||||
func handleUserDelete(app *App, u *User, w http.ResponseWriter, r *http.Request) error { | |||||
if !app.cfg.App.OpenDeletion { | |||||
return impart.HTTPError{http.StatusForbidden, "Open account deletion is disabled on this instance."} | |||||
} | |||||
confirmUsername := r.PostFormValue("confirm-username") | |||||
if u.Username != confirmUsername { | |||||
return impart.HTTPError{http.StatusBadRequest, "Confirmation username must match your username exactly."} | |||||
} | |||||
// Check for account deletion safeguards in place | |||||
if u.IsAdmin() { | |||||
return impart.HTTPError{http.StatusForbidden, "Cannot delete admin."} | |||||
} | |||||
err := app.db.DeleteAccount(u.ID) | |||||
if err != nil { | |||||
log.Error("user delete account: %v", err) | |||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete account: %v", err)} | |||||
} | |||||
// FIXME: This doesn't ever appear to the user, as (I believe) the value is erased when the session cookie is reset | |||||
_ = addSessionFlash(app, w, r, "Thanks for writing with us! You account was deleted successfully.", nil) | |||||
return impart.HTTPError{http.StatusFound, "/me/logout"} | |||||
} | |||||
func removeOauth(app *App, u *User, w http.ResponseWriter, r *http.Request) error { | func removeOauth(app *App, u *User, w http.ResponseWriter, r *http.Request) error { | ||||
provider := r.FormValue("provider") | provider := r.FormValue("provider") | ||||
clientID := r.FormValue("client_id") | clientID := r.FormValue("client_id") | ||||
@@ -1173,6 +1202,7 @@ func prepareUserEmail(input string, emailKey []byte) zero.String { | |||||
log.Error("Unable to encrypt email: %s\n", err) | log.Error("Unable to encrypt email: %s\n", err) | ||||
} else { | } else { | ||||
email.String = string(encEmail) | email.String = string(encEmail) | ||||
} | } | ||||
} | } | ||||
return email | return email | ||||
@@ -555,6 +555,7 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt | |||||
apper.App().cfg.App.SiteDesc = r.FormValue("site_desc") | apper.App().cfg.App.SiteDesc = r.FormValue("site_desc") | ||||
apper.App().cfg.App.Landing = r.FormValue("landing") | apper.App().cfg.App.Landing = r.FormValue("landing") | ||||
apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on" | apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on" | ||||
apper.App().cfg.App.OpenDeletion = r.FormValue("open_deletion") == "on" | |||||
mul, err := strconv.Atoi(r.FormValue("min_username_len")) | mul, err := strconv.Atoi(r.FormValue("min_username_len")) | ||||
if err == nil { | if err == nil { | ||||
apper.App().cfg.App.MinUsernameLen = mul | apper.App().cfg.App.MinUsernameLen = mul | ||||
@@ -166,6 +166,14 @@ func (app *App) LoadKeys() error { | |||||
if debugging { | if debugging { | ||||
log.Info(" %s", emailKeyPath) | log.Info(" %s", emailKeyPath) | ||||
} | } | ||||
executable, err := os.Executable() | |||||
if err != nil { | |||||
executable = "writefreely" | |||||
} else { | |||||
executable = filepath.Base(executable) | |||||
} | |||||
app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath) | app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath) | ||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
@@ -187,6 +195,22 @@ func (app *App) LoadKeys() error { | |||||
return err | return err | ||||
} | } | ||||
if debugging { | |||||
log.Info(" %s", csrfKeyPath) | |||||
} | |||||
app.keys.CSRFKey, err = ioutil.ReadFile(csrfKeyPath) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
log.Error(`Missing key: %s. | |||||
Run this command to generate missing keys: | |||||
%s keys generate | |||||
`, csrfKeyPath, executable) | |||||
} | |||||
return err | |||||
} | |||||
return nil | return nil | ||||
} | } | ||||
@@ -637,6 +661,10 @@ func GenerateKeyFiles(app *App) error { | |||||
if err != nil { | if err != nil { | ||||
keyErrs = err | keyErrs = err | ||||
} | } | ||||
err = generateKey(csrfKeyPath) | |||||
if err != nil { | |||||
keyErrs = err | |||||
} | |||||
return keyErrs | return keyErrs | ||||
} | } | ||||
@@ -1,5 +1,5 @@ | |||||
/* | /* | ||||
* Copyright © 2018-2020 A Bunch Tell LLC. | |||||
* Copyright © 2018-2021 A Bunch Tell LLC. | |||||
* | * | ||||
* This file is part of WriteFreely. | * This file is part of WriteFreely. | ||||
* | * | ||||
@@ -139,6 +139,7 @@ type ( | |||||
// Users | // Users | ||||
SingleUser bool `ini:"single_user"` | SingleUser bool `ini:"single_user"` | ||||
OpenRegistration bool `ini:"open_registration"` | OpenRegistration bool `ini:"open_registration"` | ||||
OpenDeletion bool `ini:"open_deletion"` | |||||
MinUsernameLen int `ini:"min_username_len"` | MinUsernameLen int `ini:"min_username_len"` | ||||
MaxBlogs int `ini:"max_blogs"` | MaxBlogs int `ini:"max_blogs"` | ||||
@@ -7,6 +7,7 @@ require ( | |||||
github.com/go-sql-driver/mysql v1.6.0 | github.com/go-sql-driver/mysql v1.6.0 | ||||
github.com/go-test/deep v1.0.1 // indirect | github.com/go-test/deep v1.0.1 // indirect | ||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect | ||||
github.com/gorilla/csrf v1.7.0 | |||||
github.com/gorilla/feeds v1.1.1 | github.com/gorilla/feeds v1.1.1 | ||||
github.com/gorilla/mux v1.8.0 | github.com/gorilla/mux v1.8.0 | ||||
github.com/gorilla/schema v1.2.0 | github.com/gorilla/schema v1.2.0 | ||||
@@ -22,7 +23,6 @@ require ( | |||||
github.com/microcosm-cc/bluemonday v1.0.5 | github.com/microcosm-cc/bluemonday v1.0.5 | ||||
github.com/mitchellh/go-wordwrap v1.0.1 | github.com/mitchellh/go-wordwrap v1.0.1 | ||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d | ||||
github.com/pkg/errors v0.8.1 // indirect | |||||
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469 | github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469 | ||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect | ||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect | github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect | ||||
@@ -44,6 +44,8 @@ github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c= | |||||
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U= | github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U= | ||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= | ||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | ||||
github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y= | |||||
github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= | |||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= | github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= | ||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= | ||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= | github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= | ||||
@@ -99,6 +101,8 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/ | |||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= | ||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= | ||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | |||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | |||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469 h1:rAbv2gekFbUcjhUkruwo0vMJ0JqhUgg9tz7t+bxHbN4= | github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469 h1:rAbv2gekFbUcjhUkruwo0vMJ0JqhUgg9tz7t+bxHbN4= | ||||
@@ -1,5 +1,5 @@ | |||||
/* | /* | ||||
* Copyright © 2019 A Bunch Tell LLC. | |||||
* Copyright © 2019, 2021 A Bunch Tell LLC. | |||||
* | * | ||||
* This file is part of WriteFreely. | * This file is part of WriteFreely. | ||||
* | * | ||||
@@ -20,7 +20,7 @@ const ( | |||||
) | ) | ||||
type Keychain struct { | type Keychain struct { | ||||
EmailKey, CookieAuthKey, CookieKey []byte | |||||
EmailKey, CookieAuthKey, CookieKey, CSRFKey []byte | |||||
} | } | ||||
// GenerateKeys generates necessary keys for the app on the given Keychain, | // GenerateKeys generates necessary keys for the app on the given Keychain, | ||||
@@ -47,6 +47,12 @@ func (keys *Keychain) GenerateKeys() error { | |||||
keyErrs = err | keyErrs = err | ||||
} | } | ||||
} | } | ||||
if len(keys.CSRFKey) == 0 { | |||||
keys.CSRFKey, err = GenerateBytes(EncKeysBytes) | |||||
if err != nil { | |||||
keyErrs = err | |||||
} | |||||
} | |||||
return keyErrs | return keyErrs | ||||
} | } | ||||
@@ -26,6 +26,7 @@ var ( | |||||
emailKeyPath = filepath.Join(keysDir, "email.aes256") | emailKeyPath = filepath.Join(keysDir, "email.aes256") | ||||
cookieAuthKeyPath = filepath.Join(keysDir, "cookies_auth.aes256") | cookieAuthKeyPath = filepath.Join(keysDir, "cookies_auth.aes256") | ||||
cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256") | cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256") | ||||
csrfKeyPath = filepath.Join(keysDir, "csrf.aes256") | |||||
) | ) | ||||
// InitKeys loads encryption keys into memory via the given Apper interface | // InitKeys loads encryption keys into memory via the given Apper interface | ||||
@@ -42,6 +43,7 @@ func initKeyPaths(app *App) { | |||||
emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath) | emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath) | ||||
cookieAuthKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieAuthKeyPath) | cookieAuthKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieAuthKeyPath) | ||||
cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath) | cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath) | ||||
csrfKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, csrfKeyPath) | |||||
} | } | ||||
// generateKey generates a key at the given path used for the encryption of | // generateKey generates a key at the given path used for the encryption of | ||||
@@ -16,6 +16,7 @@ import ( | |||||
"path/filepath" | "path/filepath" | ||||
"strings" | "strings" | ||||
"github.com/gorilla/csrf" | |||||
"github.com/gorilla/mux" | "github.com/gorilla/mux" | ||||
"github.com/writeas/go-webfinger" | "github.com/writeas/go-webfinger" | ||||
"github.com/writeas/web-core/log" | "github.com/writeas/web-core/log" | ||||
@@ -98,6 +99,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { | |||||
me.HandleFunc("/c/", handler.User(viewCollections)).Methods("GET") | me.HandleFunc("/c/", handler.User(viewCollections)).Methods("GET") | ||||
me.HandleFunc("/c/{collection}", handler.User(viewEditCollection)).Methods("GET") | me.HandleFunc("/c/{collection}", handler.User(viewEditCollection)).Methods("GET") | ||||
me.HandleFunc("/c/{collection}/stats", handler.User(viewStats)).Methods("GET") | me.HandleFunc("/c/{collection}/stats", handler.User(viewStats)).Methods("GET") | ||||
me.Path("/delete").Handler(csrf.Protect(apper.App().keys.CSRFKey)(handler.User(handleUserDelete))).Methods("POST") | |||||
me.HandleFunc("/posts", handler.Redirect("/me/posts/", UserLevelUser)).Methods("GET") | me.HandleFunc("/posts", handler.Redirect("/me/posts/", UserLevelUser)).Methods("GET") | ||||
me.HandleFunc("/posts/", handler.User(viewArticles)).Methods("GET") | me.HandleFunc("/posts/", handler.User(viewArticles)).Methods("GET") | ||||
me.HandleFunc("/posts/export.csv", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET") | me.HandleFunc("/posts/export.csv", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET") | ||||
@@ -106,7 +108,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { | |||||
me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET") | me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET") | ||||
me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET") | me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET") | ||||
me.HandleFunc("/import", handler.User(viewImport)).Methods("GET") | me.HandleFunc("/import", handler.User(viewImport)).Methods("GET") | ||||
me.HandleFunc("/settings", handler.User(viewSettings)).Methods("GET") | |||||
me.Path("/settings").Handler(csrf.Protect(apper.App().keys.CSRFKey)(handler.User(viewSettings))).Methods("GET") | |||||
me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET") | me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET") | ||||
me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET") | me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET") | ||||
@@ -76,6 +76,14 @@ select { | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="features row"> | <div class="features row"> | ||||
<div{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_deletion"> | |||||
Allow account deletion | |||||
<p>Allow all users to delete their account. Admins can always delete users.</p> | |||||
</label></div> | |||||
<div{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_deletion" id="open_deletion" {{if .Config.OpenDeletion}}checked="checked"{{end}} /> | |||||
</div> | |||||
</div> | |||||
<div class="features row"> | |||||
<div{{if .Config.SingleUser}} class="invisible"{{end}}><label for="user_invites"> | <div{{if .Config.SingleUser}} class="invisible"{{end}}><label for="user_invites"> | ||||
Allow invitations from... | Allow invitations from... | ||||
<p>Choose who is allowed to invite new people.</p> | <p>Choose who is allowed to invite new people.</p> | ||||
@@ -3,6 +3,9 @@ | |||||
<style type="text/css"> | <style type="text/css"> | ||||
.option { margin: 2em 0em; } | .option { margin: 2em 0em; } | ||||
h2 { | |||||
margin-top: 2.5em; | |||||
} | |||||
h3 { font-weight: normal; } | h3 { font-weight: normal; } | ||||
.section p, .section label { | .section p, .section label { | ||||
font-size: 0.86em; | font-size: 0.86em; | ||||
@@ -11,8 +14,13 @@ h3 { font-weight: normal; } | |||||
max-height: 2.75em; | max-height: 2.75em; | ||||
vertical-align: middle; | vertical-align: middle; | ||||
} | } | ||||
.modal { | |||||
position: fixed; | |||||
} | |||||
</style> | </style> | ||||
<div class="content-container snug"> | <div class="content-container snug"> | ||||
<div id="overlay"></div> | |||||
{{if .Silenced}} | {{if .Silenced}} | ||||
{{template "user-silenced"}} | {{template "user-silenced"}} | ||||
{{end}} | {{end}} | ||||
@@ -76,8 +84,6 @@ h3 { font-weight: normal; } | |||||
{{end}} | {{end}} | ||||
{{ if .OauthSection }} | {{ if .OauthSection }} | ||||
<hr /> | |||||
{{ if .OauthAccounts }} | {{ if .OauthAccounts }} | ||||
<div class="option"> | <div class="option"> | ||||
<h2>Linked Accounts</h2> | <h2>Linked Accounts</h2> | ||||
@@ -151,8 +157,41 @@ h3 { font-weight: normal; } | |||||
</div> | </div> | ||||
{{ end }} | {{ end }} | ||||
{{ end }} | {{ end }} | ||||
{{ if and .OpenDeletion (not .IsAdmin) }} | |||||
<h2>Incinerator</h2> | |||||
<div class="alert danger"> | |||||
<div class="row"> | |||||
<div> | |||||
<h3>Delete your account</h3> | |||||
<p>Permanently erase all your data, with no way to recover it.</p> | |||||
</div> | |||||
<button class="cta danger" onclick="prepareDeleteUser()">Delete your account...</button> | |||||
</div> | |||||
</div> | |||||
{{end}} | |||||
</div> | |||||
<div id="modal-delete-user" class="modal"> | |||||
<h2>Are you sure?</h2> | |||||
<div class="body"> | |||||
<p style="text-align:left">This action <strong>cannot</strong> be undone. It will immediately and permanently erase your account, including your blogs and posts. Before continuing, you might want to <a href="/me/export">export your data</a>.</p> | |||||
<p>If you're sure, please type <strong>{{.Username}}</strong> to confirm.</p> | |||||
<ul id="delete-errors" class="errors"></ul> | |||||
<form action="/me/delete" method="post" onsubmit="confirmDeletion()"> | |||||
{{ .CSRFField }} | |||||
<input id="confirm-text" placeholder="{{.Username}}" type="text" class="confirm boxy" name="confirm-username" style="margin-top: 0.5em;" /> | |||||
<div style="text-align:right; margin-top: 1em;"> | |||||
<a id="cancel-delete" style="margin-right:2em" href="#">Cancel</a> | |||||
<input class="danger" type="submit" id="confirm-delete" value="Delete your account" disabled /> | |||||
</div> | |||||
</div> | |||||
</div> | </div> | ||||
<script src="/js/h.js"></script> | |||||
<script src="/js/modals.js"></script> | |||||
<script> | <script> | ||||
var showChecks = document.querySelectorAll('input.show'); | var showChecks = document.querySelectorAll('input.show'); | ||||
for (var i=0; i<showChecks.length; i++) { | for (var i=0; i<showChecks.length; i++) { | ||||
@@ -165,6 +204,27 @@ for (var i=0; i<showChecks.length; i++) { | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
{{ if and .OpenDeletion (not .IsAdmin) }} | |||||
H.getEl('cancel-delete').on('click', closeModals); | |||||
let $confirmDelBtn = document.getElementById('confirm-delete'); | |||||
let $confirmText = document.getElementById('confirm-text') | |||||
$confirmText.addEventListener('input', function() { | |||||
$confirmDelBtn.disabled = this.value !== '{{.Username}}' | |||||
}); | |||||
function prepareDeleteUser() { | |||||
$confirmText.value = '' | |||||
showModal('delete-user') | |||||
$confirmText.focus() | |||||
} | |||||
function confirmDeletion() { | |||||
$confirmDelBtn.disabled = true | |||||
$confirmDelBtn.value = 'Deleting...' | |||||
} | |||||
{{ end }} | |||||
</script> | </script> | ||||
{{template "footer" .}} | {{template "footer" .}} | ||||