Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

T319 admin delete acct #203

Merged
merged 5 commits into from Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions admin.go
Expand Up @@ -189,6 +189,7 @@ func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Requ
*AdminPage
Config config.AppCfg
Message string
Flashes []string

Users *[]User
CurPage int
Expand All @@ -201,6 +202,7 @@ func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Requ
Message: r.FormValue("m"),
}

p.Flashes, _ = getSessionFlashes(app, w, r, nil)
p.TotalUsers = app.db.GetAllUsersCount()
ttlPages := p.TotalUsers / adminUsersPerPage
p.TotalPages = []int{}
Expand Down Expand Up @@ -312,6 +314,37 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
return nil
}

func handleAdminDeleteUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
if !u.IsAdmin() {
return impart.HTTPError{http.StatusForbidden, "Administrator privileges required for this action"}
}

vars := mux.Vars(r)
username := vars["username"]
confirmUsername := r.PostFormValue("confirm-username")

if confirmUsername != username {
return impart.HTTPError{http.StatusBadRequest, "Username was not confirmed"}
}

user, err := app.db.GetUserForAuth(username)
if err == ErrUserNotFound {
return impart.HTTPError{http.StatusNotFound, fmt.Sprintf("User '%s' was not found", username)}
} else if err != nil {
log.Error("get user for deletion: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user with username '%s': %v", username, err)}
}

err = app.db.DeleteAccount(user.ID)
if err != nil {
log.Error("delete user %s: %v", user.Username, err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete user account for '%s': %v", username, err)}
}

_ = addSessionFlash(app, w, r, fmt.Sprintf("User \"%s\" was deleted successfully.", username), nil)
return impart.HTTPError{http.StatusFound, "/admin/users"}
}

func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]
Expand Down
13 changes: 13 additions & 0 deletions less/core.less
Expand Up @@ -1044,6 +1044,19 @@ li {
background-color: #dff0d8;
border-color: #d6e9c6;
}
&.danger {
border-color: #856404;
background-color: white;
h3 {
margin: 0 0 0.5em 0;
font-size: 1em;
font-weight: bold;
color: black !important;
}
h3 + p, button {
font-size: 0.86em;
}
}

p {
margin: 0;
Expand Down
9 changes: 9 additions & 0 deletions less/pad.less
Expand Up @@ -340,6 +340,15 @@ body#pad {
}
}

.body {
line-height: 1.5;

input[type=text].confirm {
width: 100%;
box-sizing: border-box;
}
}

.short {
text-align: center;
}
Expand Down
1 change: 1 addition & 0 deletions routes.go
Expand Up @@ -166,6 +166,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc("/admin/settings", handler.Admin(handleViewAdminSettings)).Methods("GET")
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
write.HandleFunc("/admin/user/{username}/delete", handler.Admin(handleAdminDeleteUser)).Methods("POST")
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST")
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
Expand Down
24 changes: 24 additions & 0 deletions static/js/modals.js
@@ -0,0 +1,24 @@
/*
* Copyright © 2016-2021 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/

function showModal(id) {
document.getElementById('overlay').style.display = 'block';
document.getElementById('modal-'+id).style.display = 'block';
}

var closeModals = function(e) {
e.preventDefault();
document.getElementById('overlay').style.display = 'none';
var modals = document.querySelectorAll('.modal');
for (var i=0; i<modals.length; i++) {
modals[i].style.display = 'none';
}
};
H.getEl('overlay').on('click', closeModals);
6 changes: 6 additions & 0 deletions templates/user/admin/users.tmpl
Expand Up @@ -4,6 +4,12 @@
<div class="snug content-container">
{{template "admin-header" .}}

<!-- TODO: if other use for flashes use patern like account_import.go -->
{{if .Flashes}}
<p class="alert success">
{{range .Flashes}}{{.}}{{end}}
</p>
{{end}}
<div class="row admin-actions" style="justify-content: space-between;">
<span style="font-style: italic; font-size: 1.2em">{{.TotalUsers}} {{pluralize "user" "users" .TotalUsers}}</span>
<a class="btn cta" href="/me/invites">+ Invite people</a>
Expand Down
56 changes: 56 additions & 0 deletions templates/user/admin/view-user.tmpl
Expand Up @@ -32,8 +32,13 @@ input.copy-text {
width: 100%;
box-sizing: border-box;
}
.modal {
position: fixed;
}
</style>
<div class="snug content-container">
<div id="overlay"></div>

{{template "admin-header" .}}

<h2 id="posts-header">{{.User.Username}}</h2>
Expand Down Expand Up @@ -139,9 +144,60 @@ input.copy-text {
{{end}}
</table>
{{end}}

{{ if not .User.IsAdmin }}
<h2>Incinerator</h2>
<div class="alert danger">
<div class="row">
<div>
<h3>Delete this user</h3>
<p>Permanently erase all user data, with no way to recover it.</p>
</div>
<button class="cta danger" onclick="prepareDeleteUser()">Delete this user...</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 permanently erase all traces of this user, <strong>{{.User.Username}}</strong>, including their account information, blogs, and posts.</p>
<p>Please type <strong>{{.User.Username}}</strong> to confirm.</p>

<ul id="delete-errors" class="errors"></ul>

<form action="/admin/user/{{.User.Username}}/delete" method="post" onsubmit="confirmDeletion()">
<input id="confirm-text" placeholder="{{.User.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 this user" disabled />
</div>
</div>
</div>

<script src="/js/h.js"></script>
<script src="/js/modals.js"></script>
<script type="text/javascript">
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 !== '{{.User.Username}}'
});

function prepareDeleteUser() {
$confirmText.value = ''
showModal('delete-user')
$confirmText.focus()
}

function confirmDeletion() {
$confirmDelBtn.disabled = true
$confirmDelBtn.value = 'Deleting...'
}

function confirmSilence() {
return confirm("Silence this user? They'll still be able to log in and access their posts, but no one else will be able to see them anymore. You can reverse this decision at any time.");
}
Expand Down
17 changes: 1 addition & 16 deletions templates/user/collection.tmpl
Expand Up @@ -193,25 +193,10 @@ textarea.section.norm {
</div>

<script src="/js/h.js"></script>
<script src="/js/modals.js"></script>
<script src="/js/ace.js" type="text/javascript" charset="utf-8"></script>
<script>
// Begin shared modal code
function showModal(id) {
document.getElementById('overlay').style.display = 'block';
document.getElementById('modal-'+id).style.display = 'block';
}

var closeModals = function(e) {
e.preventDefault();
document.getElementById('overlay').style.display = 'none';
var modals = document.querySelectorAll('.modal');
for (var i=0; i<modals.length; i++) {
modals[i].style.display = 'none';
}
};
H.getEl('overlay').on('click', closeModals);
H.getEl('cancel-delete').on('click', closeModals);
// end
var deleteBlog = function(e) {
if (document.getElementById('confirm-text').value != '{{.Alias}}') {
document.getElementById('delete-errors').innerHTML = '<li class="urgent">Enter <strong>{{.Alias}}</strong> in the box below.</li>';
Expand Down