diff --git a/lurkcoin/api/admin-pages.go b/lurkcoin/api/admin-pages.go index 30364d5..24528bf 100644 --- a/lurkcoin/api/admin-pages.go +++ b/lurkcoin/api/admin-pages.go @@ -22,6 +22,7 @@ import ( "crypto/sha512" "encoding/hex" "github.com/julienschmidt/httprouter" + "html" "html/template" "io" "log" @@ -44,6 +45,29 @@ const adminPagesHeader = ` const adminPagesFooter = `` +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 + `

Server list

Total: {{len .Summaries}} server(s). @@ -110,34 +134,15 @@ const serverListTemplate = adminPagesHeader + ` required="required" id="username-field" />
- + {{else}} You may not edit the database. @@ -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}} @@ -197,37 +212,18 @@ const infoTemplate = adminPagesHeader + `
- + Cancel + {{end}}

-{{if .AllowEditing}} - -{{end}} -

History

@@ -261,6 +257,47 @@ const infoTemplate = adminPagesHeader + ` {{end}}
+ +{{if .AllowEditing}} +
+

Delete server

+ This action cannot be undone.
+ To confirm the server deletion, please type the server's name + ({{.Server.Name}}) below.

+ + +
+ + +
+ + +{{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+ + `

An error has occurred!

`+ + `
`+html.EscapeString(msg)+`
`+ + `You can hurry back to the previous page, or learn to like`+ + ` this error and then eventually grow old and die.`+ + `

`+ + `Go back`+ + 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+ - `

An error has occurred!

`+ - `
`+msg+`
`+ - `You can hurry back to the previous page, or learn to like`+ - ` this error and then eventually grow old and die.`+ - `

`+ - `Go back`+ - adminPagesFooter) + writeAdminErrorPage(w, msg) }) router.GET("/admin/backup", func(w http.ResponseWriter, diff --git a/lurkcoin/databases/bbolt.go b/lurkcoin/databases/bbolt.go index 86cafd6..96c2d64 100644 --- a/lurkcoin/databases/bbolt.go +++ b/lurkcoin/databases/bbolt.go @@ -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 { diff --git a/lurkcoin/databases/plaintext.go b/lurkcoin/databases/plaintext.go index f41eca8..0e466d1 100644 --- a/lurkcoin/databases/plaintext.go +++ b/lurkcoin/databases/plaintext.go @@ -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), diff --git a/lurkcoin/db-helpers.go b/lurkcoin/db-helpers.go index 87a9ba3..f302260 100644 --- a/lurkcoin/db-helpers.go +++ b/lurkcoin/db-helpers.go @@ -41,6 +41,7 @@ type Database interface { CreateServer(string) (*Server, bool) ListServers() []string + DeleteServer(string) bool } // An atomic database transaction. diff --git a/lurkcoin/misc.go b/lurkcoin/misc.go index dccdb36..50a76fe 100644 --- a/lurkcoin/misc.go +++ b/lurkcoin/misc.go @@ -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" diff --git a/restore-backup.go b/restore-backup.go new file mode 100644 index 0000000..9ed0714 --- /dev/null +++ b/restore-backup.go @@ -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 . +// + +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!") +}