Add server deletion
This commit is contained in:
parent
8e87b4b806
commit
fab4eff8dc
|
@ -22,6 +22,7 @@ import (
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -44,6 +45,29 @@ const adminPagesHeader = `<!DOCTYPE html>
|
||||||
|
|
||||||
const adminPagesFooter = `</main></body></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 + `
|
const serverListTemplate = adminPagesHeader + `
|
||||||
<h2>Server list</h2>
|
<h2>Server list</h2>
|
||||||
<i>Total: {{len .Summaries}} server(s).</i>
|
<i>Total: {{len .Summaries}} server(s).</i>
|
||||||
|
@ -110,34 +134,15 @@ const serverListTemplate = adminPagesHeader + `
|
||||||
required="required" id="username-field" /><br/>
|
required="required" id="username-field" /><br/>
|
||||||
<input type="submit" name="submit" class="button-primary"
|
<input type="submit" name="submit" class="button-primary"
|
||||||
value="Create" />
|
value="Create" />
|
||||||
<button type="button" onclick="hideForm()">Cancel</button>
|
<button type="button" onclick="hideForm2()">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
"use strict";
|
"use strict";
|
||||||
let btn = document.getElementById("new-server");
|
const btn = document.getElementById("new-server");
|
||||||
let form = document.getElementById("create-server");
|
const form = document.getElementById("create-server");
|
||||||
btn.style.display = "inline";
|
` + popOutCode + `
|
||||||
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 = "";
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{{else}}
|
{{else}}
|
||||||
<i>You may not edit the database.</i>
|
<i>You may not edit the database.</i>
|
||||||
|
@ -157,8 +162,18 @@ const infoTemplate = adminPagesHeader + `
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
{{if .AllowEditing}}
|
{{if .AllowEditing}}
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
#edit-btn, #edit-btn ~ .button {
|
#edit-btn, #edit-btn ~ .button {
|
||||||
display: none;
|
display: none;
|
||||||
|
transition: ease-in-out 250ms;
|
||||||
|
}
|
||||||
|
#delete-server {
|
||||||
|
display: none;
|
||||||
|
transform: scaleY(0);
|
||||||
|
transform-origin: top center;
|
||||||
|
max-height: 0;
|
||||||
}
|
}
|
||||||
{{end}}
|
{{end}}
|
||||||
</style>
|
</style>
|
||||||
|
@ -197,37 +212,18 @@ const infoTemplate = adminPagesHeader + `
|
||||||
<br/>
|
<br/>
|
||||||
<button type="button" id="edit-btn"
|
<button type="button" id="edit-btn"
|
||||||
class="button-primary">Edit</button>
|
class="button-primary">Edit</button>
|
||||||
<script>
|
|
||||||
document.getElementById("edit-btn").style.display = "inline";
|
|
||||||
</script>
|
|
||||||
<input type="submit" value="Save" class="button button-primary"
|
<input type="submit" value="Save" class="button button-primary"
|
||||||
disabled="disabled" />
|
disabled="disabled" />
|
||||||
|
<button type="button" id="delete-btn">Delete</button>
|
||||||
<a href="{{.Server.UID}}" class="button">Cancel</a>
|
<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}}
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</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>
|
<h4>History</h4>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -261,6 +257,47 @@ const infoTemplate = adminPagesHeader + `
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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
|
` + adminPagesFooter
|
||||||
|
|
||||||
type adminPagesSummary struct {
|
type adminPagesSummary struct {
|
||||||
|
@ -329,6 +366,19 @@ func (self csrfTokenManager) Get(username string) string {
|
||||||
return token
|
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,
|
func addAdminPages(router *httprouter.Router, db lurkcoin.Database,
|
||||||
loginDetails AdminLoginDetails) {
|
loginDetails AdminLoginDetails) {
|
||||||
// TODO: Regenerate this often
|
// 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"))
|
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,
|
router.POST("/admin/create-server", func(w http.ResponseWriter,
|
||||||
r *http.Request, params httprouter.Params) {
|
r *http.Request, params httprouter.Params) {
|
||||||
adminUser, authenticated := authenticateWithCSRF(w, r)
|
adminUser, authenticated := authenticateWithCSRF(w, r)
|
||||||
|
@ -595,16 +670,7 @@ func addAdminPages(router *httprouter.Router, db lurkcoin.Database,
|
||||||
msg = "The specified server already exists!"
|
msg = "The specified server already exists!"
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
writeAdminErrorPage(w, msg)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
router.GET("/admin/backup", func(w http.ResponseWriter,
|
router.GET("/admin/backup", func(w http.ResponseWriter,
|
||||||
|
|
|
@ -143,6 +143,19 @@ func (self *boltDatabase) ListServers() (res []string) {
|
||||||
return
|
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) {
|
func BoltDatabase(file string, _ map[string]string) (lurkcoin.Database, error) {
|
||||||
db, err := bolt.Open(file, 0600, nil)
|
db, err := bolt.Open(file, 0600, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -64,28 +64,7 @@ func (self *plaintextDatabase) GetServers(names []string) ([]*lurkcoin.Server, b
|
||||||
return servers, ok, ""
|
return servers, ok, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *plaintextDatabase) FreeServers(servers []*lurkcoin.Server, save bool) {
|
func (self *plaintextDatabase) save() {
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := ioutil.TempFile(path.Dir(self.location), ".tmp")
|
f, err := ioutil.TempFile(path.Dir(self.location), ".tmp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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) {
|
func (self *plaintextDatabase) CreateServer(name string) (*lurkcoin.Server, bool) {
|
||||||
ids := self.dblock.Lock([]string{name})
|
ids := self.dblock.Lock([]string{name})
|
||||||
id := ids[0]
|
id := ids[0]
|
||||||
|
@ -142,6 +144,18 @@ func (self *plaintextDatabase) ListServers() []string {
|
||||||
return res
|
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) {
|
func PlaintextDatabase(location string, _ map[string]string) (lurkcoin.Database, error) {
|
||||||
db := &plaintextDatabase{
|
db := &plaintextDatabase{
|
||||||
make(map[string]*lurkcoin.EncodedServer),
|
make(map[string]*lurkcoin.EncodedServer),
|
||||||
|
|
|
@ -41,6 +41,7 @@ type Database interface {
|
||||||
|
|
||||||
CreateServer(string) (*Server, bool)
|
CreateServer(string) (*Server, bool)
|
||||||
ListServers() []string
|
ListServers() []string
|
||||||
|
DeleteServer(string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// An atomic database transaction.
|
// An atomic database transaction.
|
||||||
|
|
|
@ -35,7 +35,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const SYMBOL = "¤"
|
const SYMBOL = "¤"
|
||||||
const VERSION = "3.0.6"
|
const VERSION = "3.0.7"
|
||||||
|
|
||||||
// Note that public source code is required by the AGPL
|
// Note that public source code is required by the AGPL
|
||||||
const SOURCE_URL = "https://github.com/luk3yx/lurkcoin-core"
|
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