Add server deletion
This commit is contained in:
parent
8e87b4b806
commit
fab4eff8dc
|
@ -22,6 +22,7 @@ import (
|
|||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -44,6 +45,29 @@ const adminPagesHeader = `<!DOCTYPE html>
|
|||
|
||||
const adminPagesFooter = `</main></body></html>`
|
||||
|
||||
const popOutCode = `
|
||||
btn.style.display = "inline";
|
||||
btn.addEventListener("click", () => {
|
||||
form.style.display = "block";
|
||||
form.style.transition = "ease-in-out 250ms transform";
|
||||
window.setTimeout(() => {
|
||||
form.style.transform = "scaleY(1)";
|
||||
form.style.maxHeight = form.scrollHeight.toString() + "px";
|
||||
btn.style.opacity = "0.5";
|
||||
btn.style.pointerEvents = "none";
|
||||
window.location.hash = "#" + form.id;
|
||||
}, 25);
|
||||
btn.blur();
|
||||
});
|
||||
function hideForm() {
|
||||
form.style.transition = "ease-in-out 250ms";
|
||||
form.style.transform = "scaleY(0)";
|
||||
form.style.maxHeight = "0";
|
||||
btn.style.opacity = "1";
|
||||
btn.style.pointerEvents = "";
|
||||
}
|
||||
`
|
||||
|
||||
const serverListTemplate = adminPagesHeader + `
|
||||
<h2>Server list</h2>
|
||||
<i>Total: {{len .Summaries}} server(s).</i>
|
||||
|
@ -110,34 +134,15 @@ const serverListTemplate = adminPagesHeader + `
|
|||
required="required" id="username-field" /><br/>
|
||||
<input type="submit" name="submit" class="button-primary"
|
||||
value="Create" />
|
||||
<button type="button" onclick="hideForm()">Cancel</button>
|
||||
<button type="button" onclick="hideForm2()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
let btn = document.getElementById("new-server");
|
||||
let form = document.getElementById("create-server");
|
||||
btn.style.display = "inline";
|
||||
btn.addEventListener("click", () => {
|
||||
form.style.display = "block";
|
||||
form.style.transition = "ease-in-out 250ms transform";
|
||||
window.setTimeout(() => {
|
||||
form.style.transform = "scaleY(1)";
|
||||
form.style.maxHeight = form.scrollHeight.toString() + "px";
|
||||
btn.style.opacity = "0.5";
|
||||
btn.style.pointerEvents = "none";
|
||||
window.location.hash = "#create-server";
|
||||
}, 25);
|
||||
btn.blur();
|
||||
});
|
||||
function hideForm() {
|
||||
form.style.transition = "ease-in-out 250ms";
|
||||
form.style.transform = "scaleY(0)";
|
||||
form.style.maxHeight = "0";
|
||||
btn.style.opacity = "1";
|
||||
btn.style.pointerEvents = "";
|
||||
}
|
||||
const btn = document.getElementById("new-server");
|
||||
const form = document.getElementById("create-server");
|
||||
` + popOutCode + `
|
||||
</script>
|
||||
{{else}}
|
||||
<i>You may not edit the database.</i>
|
||||
|
@ -157,8 +162,18 @@ const infoTemplate = adminPagesHeader + `
|
|||
color: inherit;
|
||||
}
|
||||
{{if .AllowEditing}}
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
#edit-btn, #edit-btn ~ .button {
|
||||
display: none;
|
||||
transition: ease-in-out 250ms;
|
||||
}
|
||||
#delete-server {
|
||||
display: none;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top center;
|
||||
max-height: 0;
|
||||
}
|
||||
{{end}}
|
||||
</style>
|
||||
|
@ -197,37 +212,18 @@ const infoTemplate = adminPagesHeader + `
|
|||
<br/>
|
||||
<button type="button" id="edit-btn"
|
||||
class="button-primary">Edit</button>
|
||||
<script>
|
||||
document.getElementById("edit-btn").style.display = "inline";
|
||||
</script>
|
||||
<input type="submit" value="Save" class="button button-primary"
|
||||
disabled="disabled" />
|
||||
<button type="button" id="delete-btn">Delete</button>
|
||||
<a href="{{.Server.UID}}" class="button">Cancel</a>
|
||||
<script>
|
||||
document.getElementById("edit-btn").style.display = "inline";
|
||||
document.getElementById("delete-btn").style.display = "inline";
|
||||
</script>
|
||||
{{end}}
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{{if .AllowEditing}}
|
||||
<script>
|
||||
let p = document.getElementById("form-inner");
|
||||
let btn = document.getElementById("edit-btn");
|
||||
btn.addEventListener("click", () => {
|
||||
let msg = document.getElementById("message");
|
||||
if (msg) {
|
||||
msg.style.fontSize = "0";
|
||||
msg.style.margin = "0";
|
||||
msg.style.padding = "0";
|
||||
}
|
||||
p.removeChild(btn);
|
||||
for (let elem of p.children) {
|
||||
if (elem.tagName.toLowerCase() === "input")
|
||||
elem.removeAttribute("disabled");
|
||||
}
|
||||
});
|
||||
window.history.replaceState(null, null, "/admin/edit/{{.Server.UID}}");
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<h4>History</h4>
|
||||
<table>
|
||||
<thead>
|
||||
|
@ -261,6 +257,47 @@ const infoTemplate = adminPagesHeader + `
|
|||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{if .AllowEditing}}
|
||||
<form autocomplete="off" method="post" action="/admin/delete"
|
||||
id="delete-server">
|
||||
<h3>Delete server</h3>
|
||||
<b>This action cannot be undone.</b><br/>
|
||||
To confirm the server deletion, please type the server's name
|
||||
(<code>{{.Server.Name}}</code>) below.<br/><br/>
|
||||
<input type="hidden" name="csrfToken" value={{.CSRFToken}} />
|
||||
<input type="hidden" name="server-uid" value={{.Server.UID}} />
|
||||
<input type="text" name="delete-uid" /><br/>
|
||||
<input type="submit" name="delete" class="button-primary"
|
||||
value="Delete server" />
|
||||
<button type="button" onclick="hideForm()">Cancel</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
const p = document.getElementById("form-inner");
|
||||
const editBtn = document.getElementById("edit-btn");
|
||||
const btn = document.getElementById("delete-btn");
|
||||
editBtn.addEventListener("click", () => {
|
||||
const msg = document.getElementById("message");
|
||||
if (msg) {
|
||||
msg.style.fontSize = "0";
|
||||
msg.style.margin = "0";
|
||||
msg.style.padding = "0";
|
||||
}
|
||||
p.removeChild(editBtn);
|
||||
p.removeChild(btn);
|
||||
for (let elem of p.children) {
|
||||
if (elem.tagName.toLowerCase() === "input")
|
||||
elem.removeAttribute("disabled");
|
||||
}
|
||||
});
|
||||
window.history.replaceState(null, null, "/admin/edit/{{.Server.UID}}");
|
||||
|
||||
const form = document.getElementById("delete-server");
|
||||
` + popOutCode + `
|
||||
</script>
|
||||
{{end}}
|
||||
` + adminPagesFooter
|
||||
|
||||
type adminPagesSummary struct {
|
||||
|
@ -329,6 +366,19 @@ func (self csrfTokenManager) Get(username string) string {
|
|||
return token
|
||||
}
|
||||
|
||||
func writeAdminErrorPage(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, adminPagesHeader+
|
||||
`<h2>An error has occurred!</h2>`+
|
||||
`<h5>`+html.EscapeString(msg)+`</h5>`+
|
||||
`<i>You can hurry back to the previous page, or learn to like`+
|
||||
` this error and then eventually grow old and die.</i>`+
|
||||
`<br/><br/>`+
|
||||
`<a class="button button-primary" href="/admin">Go back</a>`+
|
||||
adminPagesFooter)
|
||||
}
|
||||
|
||||
func addAdminPages(router *httprouter.Router, db lurkcoin.Database,
|
||||
loginDetails AdminLoginDetails) {
|
||||
// TODO: Regenerate this often
|
||||
|
@ -567,6 +617,31 @@ func addAdminPages(router *httprouter.Router, db lurkcoin.Database,
|
|||
serverInfo(w, r, uid, adminUser, strings.Join(msgs, "\n"))
|
||||
})
|
||||
|
||||
router.POST("/admin/delete", func(w http.ResponseWriter,
|
||||
r *http.Request, params httprouter.Params) {
|
||||
adminUser, authenticated := authenticateWithCSRF(w, r)
|
||||
if !authenticated {
|
||||
return
|
||||
}
|
||||
|
||||
serverUID := r.Form.Get("server-uid")
|
||||
if lurkcoin.HomogeniseUsername(r.Form.Get("delete-uid")) != serverUID {
|
||||
writeAdminErrorPage(w, "You didn't type the correct server UID!")
|
||||
return
|
||||
}
|
||||
|
||||
if db.DeleteServer(serverUID) {
|
||||
log.Printf(
|
||||
"[Admin] User %#v deleted server %#v",
|
||||
adminUser,
|
||||
serverUID,
|
||||
)
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
} else {
|
||||
writeAdminErrorPage(w, "Could not delete "+serverUID+"!")
|
||||
}
|
||||
})
|
||||
|
||||
router.POST("/admin/create-server", func(w http.ResponseWriter,
|
||||
r *http.Request, params httprouter.Params) {
|
||||
adminUser, authenticated := authenticateWithCSRF(w, r)
|
||||
|
@ -595,16 +670,7 @@ func addAdminPages(router *httprouter.Router, db lurkcoin.Database,
|
|||
msg = "The specified server already exists!"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, adminPagesHeader+
|
||||
`<h2>An error has occurred!</h2>`+
|
||||
`<h5>`+msg+`</h5>`+
|
||||
`<i>You can hurry back to the previous page, or learn to like`+
|
||||
` this error and then eventually grow old and die.</i>`+
|
||||
`<br/><br/>`+
|
||||
`<a class="button button-primary" href="/admin">Go back</a>`+
|
||||
adminPagesFooter)
|
||||
writeAdminErrorPage(w, msg)
|
||||
})
|
||||
|
||||
router.GET("/admin/backup", func(w http.ResponseWriter,
|
||||
|
|
|
@ -143,6 +143,19 @@ func (self *boltDatabase) ListServers() (res []string) {
|
|||
return
|
||||
}
|
||||
|
||||
func (self *boltDatabase) DeleteServer(name string) bool {
|
||||
ids := self.dblock.Lock([]string{name})
|
||||
defer self.dblock.UnlockIDs(ids)
|
||||
err := self.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte("lurkcoin"))
|
||||
if bucket != nil {
|
||||
return bucket.Delete([]byte(ids[0]))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func BoltDatabase(file string, _ map[string]string) (lurkcoin.Database, error) {
|
||||
db, err := bolt.Open(file, 0600, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -64,28 +64,7 @@ func (self *plaintextDatabase) GetServers(names []string) ([]*lurkcoin.Server, b
|
|||
return servers, ok, ""
|
||||
}
|
||||
|
||||
func (self *plaintextDatabase) FreeServers(servers []*lurkcoin.Server, save bool) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
self.dblock.Unlock(servers)
|
||||
|
||||
if !save {
|
||||
return
|
||||
}
|
||||
|
||||
modified := false
|
||||
for _, server := range servers {
|
||||
if server.IsModified() {
|
||||
modified = true
|
||||
encodedServer := server.Encode()
|
||||
self.db[server.UID] = &encodedServer
|
||||
}
|
||||
}
|
||||
|
||||
if !modified {
|
||||
return
|
||||
}
|
||||
|
||||
func (self *plaintextDatabase) save() {
|
||||
f, err := ioutil.TempFile(path.Dir(self.location), ".tmp")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -115,6 +94,29 @@ func (self *plaintextDatabase) FreeServers(servers []*lurkcoin.Server, save bool
|
|||
}
|
||||
}
|
||||
|
||||
func (self *plaintextDatabase) FreeServers(servers []*lurkcoin.Server, save bool) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
self.dblock.Unlock(servers)
|
||||
|
||||
if !save {
|
||||
return
|
||||
}
|
||||
|
||||
modified := false
|
||||
for _, server := range servers {
|
||||
if server.IsModified() {
|
||||
modified = true
|
||||
encodedServer := server.Encode()
|
||||
self.db[server.UID] = &encodedServer
|
||||
}
|
||||
}
|
||||
|
||||
if modified {
|
||||
self.save()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *plaintextDatabase) CreateServer(name string) (*lurkcoin.Server, bool) {
|
||||
ids := self.dblock.Lock([]string{name})
|
||||
id := ids[0]
|
||||
|
@ -142,6 +144,18 @@ func (self *plaintextDatabase) ListServers() []string {
|
|||
return res
|
||||
}
|
||||
|
||||
func (self *plaintextDatabase) DeleteServer(name string) (exists bool) {
|
||||
ids := self.dblock.Lock([]string{name})
|
||||
defer self.dblock.UnlockIDs(ids)
|
||||
id := ids[0]
|
||||
_, exists = self.db[id]
|
||||
if exists {
|
||||
delete(self.db, id)
|
||||
self.save()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func PlaintextDatabase(location string, _ map[string]string) (lurkcoin.Database, error) {
|
||||
db := &plaintextDatabase{
|
||||
make(map[string]*lurkcoin.EncodedServer),
|
||||
|
|
|
@ -41,6 +41,7 @@ type Database interface {
|
|||
|
||||
CreateServer(string) (*Server, bool)
|
||||
ListServers() []string
|
||||
DeleteServer(string) bool
|
||||
}
|
||||
|
||||
// An atomic database transaction.
|
||||
|
|
|
@ -35,7 +35,7 @@ import (
|
|||
)
|
||||
|
||||
const SYMBOL = "¤"
|
||||
const VERSION = "3.0.6"
|
||||
const VERSION = "3.0.7"
|
||||
|
||||
// Note that public source code is required by the AGPL
|
||||
const SOURCE_URL = "https://github.com/luk3yx/lurkcoin-core"
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// lurkcoin
|
||||
// Copyright © 2020 by luk3yx
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"lurkcoin"
|
||||
"lurkcoin/api"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
fmt.Println("This command takes exactly two arguments.")
|
||||
fmt.Println("Usage: ./restore-backup CONFIG BACKUP-FILE")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config, err := api.LoadConfig(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
lurkcoin.SeedPRNG()
|
||||
lurkcoin.PrintASCIIArt()
|
||||
|
||||
db, err := api.OpenDatabase(config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
backupFile := os.Args[2]
|
||||
log.Printf(
|
||||
"Restoring backup %#v into %#v...\n",
|
||||
backupFile,
|
||||
config.Database.Location,
|
||||
)
|
||||
file, err := os.Open(backupFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
err = lurkcoin.RestoreDatabase(db, file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Database backup restored!")
|
||||
}
|
Loading…
Reference in New Issue