255 lines
7.6 KiB
Go
255 lines
7.6 KiB
Go
package guestbook
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
gemini "github.com/kulak/gemini"
|
|
)
|
|
|
|
const adminUserId = "6a1171c755cbde90373bee2ac08c6aa4607ecf8ed3f50c55cf76c6a0e27dfa5c"
|
|
|
|
type GuestBook struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewGuestBook(db *sql.DB) *GuestBook {
|
|
err := initDB(db)
|
|
if err != nil {
|
|
slog.Error("Error migrating sqlite database", "error", err)
|
|
panic(err)
|
|
}
|
|
return &GuestBook{
|
|
db,
|
|
}
|
|
}
|
|
|
|
func initDB(db *sql.DB) error {
|
|
_, err := db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS guestbook (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
approved BOOLEAN NOT NULL,
|
|
message TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (book *GuestBook) HandleRequest(w gemini.ResponseWriter, req *gemini.Request) {
|
|
path := req.URL.Path
|
|
|
|
switch {
|
|
case path == "/guestbook" || path == "/guestbook/":
|
|
book.serveIndex(w, req)
|
|
case path == "/guestbook/new":
|
|
book.serveNewEntry(w, req)
|
|
case path == "/guestbook/admin":
|
|
book.serveAdmin(w, req)
|
|
case strings.HasPrefix(path, "/guestbook/approve"):
|
|
book.serveApprove(w, req)
|
|
case strings.HasPrefix(path, "/guestbook/delete"):
|
|
book.serveDelete(w, req)
|
|
default:
|
|
w.WriteStatusMsg(gemini.StatusNotFound, "Page not found")
|
|
}
|
|
}
|
|
|
|
func UserIDFromCert(cert *x509.Certificate) (string, error) {
|
|
pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash := sha256.Sum256(pubKeyBytes)
|
|
return hex.EncodeToString(hash[:]), nil
|
|
}
|
|
|
|
func (book *GuestBook) serveDelete(w gemini.ResponseWriter, req *gemini.Request) {
|
|
if req.Certificate() == nil {
|
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
|
return
|
|
}
|
|
userId, err := UserIDFromCert(req.Certificate())
|
|
if err != nil {
|
|
slog.Error("Error creating user id from cert", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error generating user ID")
|
|
return
|
|
}
|
|
if userId != adminUserId {
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Permission denied")
|
|
return
|
|
}
|
|
entryId := req.URL.Query().Get("id")
|
|
slog.Info("Deleting", "userId", userId, "entryId", entryId)
|
|
_, err = book.db.Exec("DELETE FROM guestbook WHERE id = ?", entryId)
|
|
if err != nil {
|
|
slog.Error("Error deleting guestbook entry", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error deleting guestbook entry")
|
|
return
|
|
}
|
|
book.serveAdmin(w, req)
|
|
}
|
|
|
|
func (book *GuestBook) serveApprove(w gemini.ResponseWriter, req *gemini.Request) {
|
|
if req.Certificate() == nil {
|
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
|
return
|
|
}
|
|
userId, err := UserIDFromCert(req.Certificate())
|
|
if err != nil {
|
|
slog.Error("Error creating user id from cert", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error generating user ID")
|
|
return
|
|
}
|
|
if userId != adminUserId {
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Permission denied")
|
|
return
|
|
}
|
|
entryId := req.URL.Query().Get("id")
|
|
slog.Info("Approving", "userId", userId, "entryId", entryId)
|
|
_, err = book.db.Exec("UPDATE guestbook SET approved = true WHERE id = ?", entryId)
|
|
if err != nil {
|
|
slog.Error("Error approving guestbook entry", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error approving guestbook entry")
|
|
return
|
|
}
|
|
book.serveAdmin(w, req)
|
|
}
|
|
|
|
func (book *GuestBook) serveNewEntry(w gemini.ResponseWriter, req *gemini.Request) {
|
|
if req.Certificate() == nil {
|
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
|
return
|
|
}
|
|
if req.URL.RawQuery == "" {
|
|
w.WriteStatusMsg(gemini.StatusPlainInput, "Your guestbook entry:")
|
|
return
|
|
}
|
|
// w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
|
name := req.Certificate().Subject.CommonName
|
|
userId, err := UserIDFromCert(req.Certificate())
|
|
slog.Info("User ID created", "user_id", userId)
|
|
if err != nil {
|
|
slog.Error("Error creating user id from cert", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error generating user ID")
|
|
return
|
|
}
|
|
txt := req.URL.RawQuery
|
|
_, err = book.db.Exec("INSERT INTO guestbook(name, user_id, approved, message) VALUES(?, ?, ?, ?)", name, userId, false, txt)
|
|
if err != nil {
|
|
slog.Error("Error writing to database", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error writing to database")
|
|
return
|
|
}
|
|
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
|
var content strings.Builder
|
|
content.WriteString("# Success!\n")
|
|
content.WriteString("Your entry was added to the guestbook\n\n")
|
|
content.WriteString(fmt.Sprintf("username: %s\nentry:\n> %s\n\n", name, txt))
|
|
content.WriteString("For now since the entry has not been approved it will appear in the list with xxxxx in place of chars.\n")
|
|
content.WriteString("\n\n=> /guestbook Back to guestbook")
|
|
w.WriteBody([]byte(content.String()))
|
|
}
|
|
|
|
func (book *GuestBook) serveIndex(w gemini.ResponseWriter, req *gemini.Request) {
|
|
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
|
var content strings.Builder
|
|
page, err := os.ReadFile("./pages/guestbook.gmi")
|
|
if err != nil {
|
|
slog.Error("Problem reading guestbook.gmi", "error", err)
|
|
return
|
|
}
|
|
content.Write(page)
|
|
content.WriteString("\n")
|
|
|
|
rows, err := book.db.Query("SELECT id, name, approved, created_at, message FROM guestbook ORDER BY created_at DESC")
|
|
if err != nil {
|
|
slog.Error("Error querying database", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error writing to database")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id int
|
|
var name string
|
|
var approved bool
|
|
var createdAt time.Time
|
|
var message string
|
|
rows.Scan(&id, &name, &approved, &createdAt, &message)
|
|
if !approved {
|
|
// Overwrite all non-space characters in message with "x"
|
|
masked := []rune(message)
|
|
for i, c := range masked {
|
|
if c != ' ' {
|
|
masked[i] = 'x'
|
|
}
|
|
}
|
|
message = string(masked)
|
|
}
|
|
content.WriteString(fmt.Sprintf("%s :: %s\n> %s\n", createdAt.Format("2006-01-02"), name, message))
|
|
content.WriteString("\n")
|
|
}
|
|
defer rows.Close()
|
|
|
|
content.WriteString("=> / Back to home\n")
|
|
w.WriteBody([]byte(content.String()))
|
|
}
|
|
|
|
func (book *GuestBook) serveAdmin(w gemini.ResponseWriter, req *gemini.Request) {
|
|
if req.Certificate() == nil {
|
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
|
return
|
|
}
|
|
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
|
|
|
userId, err := UserIDFromCert(req.Certificate())
|
|
if err != nil {
|
|
slog.Error("Error creating user id from cert", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error generating user ID")
|
|
return
|
|
}
|
|
if userId != adminUserId {
|
|
w.WriteStatusMsg(gemini.StatusBadRequest, "UserId != adminUserId")
|
|
return
|
|
}
|
|
|
|
var content strings.Builder
|
|
rows, err := book.db.Query("SELECT id, user_id, name, approved, created_at, message FROM guestbook ORDER BY created_at DESC")
|
|
if err != nil {
|
|
slog.Error("Error querying database", "error", err)
|
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error writing to database")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id int
|
|
var userId string
|
|
var name string
|
|
var approved bool
|
|
var createdAt time.Time
|
|
var message string
|
|
rows.Scan(&id, &userId, &name, &approved, &createdAt, &message)
|
|
content.WriteString(fmt.Sprintf("%s :: %s\n> %s\n", createdAt.Format("2006-01-02"), name, message))
|
|
if !approved {
|
|
content.WriteString(fmt.Sprintf("=> /guestbook/approve?id=%d Approve\n", id))
|
|
}
|
|
content.WriteString(fmt.Sprintf("=> /guestbook/delete?id=%d Delete\n", id))
|
|
content.WriteString("\n")
|
|
}
|
|
defer rows.Close()
|
|
|
|
content.WriteString("=> /guestbook Back to guesbook\n")
|
|
content.WriteString("=> / Back to home\n")
|
|
w.WriteBody([]byte(content.String()))
|
|
}
|