This project was made into a library.
This commit is contained in:
parent
be5d0782b6
commit
4bb06cb2a1
29
Makefile
29
Makefile
@ -1,29 +0,0 @@
|
|||||||
PACKAGE_NAME=justguestbook
|
|
||||||
TARGET_DAEMON=${PACKAGE_NAME}d
|
|
||||||
|
|
||||||
SYSCTL=${shell which systemctl}
|
|
||||||
SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
|
|
||||||
SYSDDIR=${SYSDDIR_:/%=%}
|
|
||||||
DESTDIR=/
|
|
||||||
|
|
||||||
LDFLAGS=-ldflags "-s -w -X main.version=1.0.0" -tags osusergo,netgo
|
|
||||||
|
|
||||||
all: ${TARGET_DAEMON}
|
|
||||||
|
|
||||||
.PHONY: ${TARGET_DAEMON}
|
|
||||||
|
|
||||||
${TARGET_DAEMON}:
|
|
||||||
go build -o bin/$@ ${LDFLAGS} cmd/$@/main.go
|
|
||||||
|
|
||||||
run:
|
|
||||||
bin/${TARGET_DAEMON} -conf configs/config.test.yaml
|
|
||||||
|
|
||||||
install:
|
|
||||||
install -Dm 0755 bin/${TARGET_DAEMON} ${DESTDIR}usr/bin/${TARGET_DAEMON}
|
|
||||||
install -Dm 0644 LICENSE ${DESTDIR}usr/share/licenses/${PACKAGE_NAME}/LICENSE
|
|
||||||
install -Dm 0644 init/systemd/${PACKAGE_NAME}.service ${DESTDIR}${SYSDDIR}/${PACKAGE_NAME}.service
|
|
||||||
|
|
||||||
uninstall:
|
|
||||||
rm ${DESTDIR}usr/bin/${TARGET_DAEMON}
|
|
||||||
rm ${DESTDIR}usr/share/licenses/${PACKAGE_NAME}/LICENSE
|
|
||||||
rm ${DESTDIR}${SYSDDIR}/${PACKAGE_NAME}.service
|
|
84
README.md
84
README.md
@ -1,86 +1,4 @@
|
|||||||
justguestbook ver. 1.0.0
|
justguestbook ver. 1.0.0
|
||||||
========================
|
========================
|
||||||
|
|
||||||
## Usage
|
A library implementing simple guestbook with replies.
|
||||||
|
|
||||||
justguestbookd -conf /etc/justguestbook.yaml
|
|
||||||
|
|
||||||
## API summary
|
|
||||||
|
|
||||||
GET /?p=&psz=
|
|
||||||
POST /
|
|
||||||
data: captcha_id=&name=&message=[&website=&hide_website=1]
|
|
||||||
DELETE /:entry
|
|
||||||
|
|
||||||
POST /:entry/reply
|
|
||||||
data: reply=
|
|
||||||
DELETE /:entry/reply
|
|
||||||
|
|
||||||
## Public API
|
|
||||||
|
|
||||||
### Get a list of guestbook entries
|
|
||||||
|
|
||||||
GET /?p=&psz=
|
|
||||||
|
|
||||||
#### URL query parameters
|
|
||||||
- `p=` is a page number. By default first page will be returned.
|
|
||||||
- `psz=` is a page size. By default will be used page size set by a service.
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `200` if there are entries.
|
|
||||||
- `500` if for some reason a list cannot be sent.
|
|
||||||
|
|
||||||
### Post a new guestbook entry
|
|
||||||
|
|
||||||
POST /
|
|
||||||
|
|
||||||
#### Fields
|
|
||||||
- `captcha_id` is an ID of CAPTCHA stored in a justcaptcha service.
|
|
||||||
- `name` of a poster. Optional. Service's default if not set.
|
|
||||||
- `message` a poster want to post. Required.
|
|
||||||
- `website` of a poster. Optional.
|
|
||||||
- `hide_website` if true hide website. Optional. Hide by default.
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `201` if created
|
|
||||||
- `422` if some required field doesn't present.
|
|
||||||
- `500` if for some reason post cannot be stored.
|
|
||||||
|
|
||||||
A `500` response should return a `message` back.
|
|
||||||
|
|
||||||
## Administration API
|
|
||||||
|
|
||||||
All such commands must have a password supplied in `X-Password` header.
|
|
||||||
|
|
||||||
### Send a reply to entry
|
|
||||||
|
|
||||||
POST /:entry/reply
|
|
||||||
|
|
||||||
#### Fields
|
|
||||||
- `message` for an entry. Required.
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `201` if created or modified successfully.
|
|
||||||
- `403` if unauthorised.
|
|
||||||
- `404` if there's no such entry.
|
|
||||||
- `500` if there are internal problems with service.
|
|
||||||
|
|
||||||
### Delete an entry
|
|
||||||
|
|
||||||
DELETE /:entry
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `204` if was successfully deleted.
|
|
||||||
- `403` if unauthorised.
|
|
||||||
- `404` if there's no such entry.
|
|
||||||
- `500` if there are internal problems with service.
|
|
||||||
|
|
||||||
### Delete a reply
|
|
||||||
|
|
||||||
DELETE /:entry/reply
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `204` if was successfully deleted.
|
|
||||||
- `403` if unauthorised.
|
|
||||||
- `404` if there's no such entry or reply.
|
|
||||||
- `500` if there are internal problems with service.
|
|
@ -1,30 +0,0 @@
|
|||||||
# Maintainer: Alexander "Arav" Andreev <me@arav.top>
|
|
||||||
pkgname=justguestbook-git
|
|
||||||
pkgver=1.0.0
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc="Just a simple guestbook with owner's replies"
|
|
||||||
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
|
||||||
url="https://git.arav.top/Arav/justguestbook"
|
|
||||||
license=('MIT')
|
|
||||||
groups=()
|
|
||||||
depends=()
|
|
||||||
makedepends=('git' 'go')
|
|
||||||
provides=('justguestbook')
|
|
||||||
conflicts=('justguestbook')
|
|
||||||
replaces=()
|
|
||||||
backup=('/etc/justguestbook.yaml')
|
|
||||||
options=()
|
|
||||||
install=
|
|
||||||
source=('justguestbook-git::git+https://git.arav.top/Arav/justguestbook.git')
|
|
||||||
noextract=()
|
|
||||||
md5sums=('SKIP')
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd "$srcdir/$pkgname"
|
|
||||||
make DESTDIR="$pkgdir/"
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cd "$srcdir/$pkgname"
|
|
||||||
make DESTDIR="$pkgdir/" install
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"justguestbook/internal/configuration"
|
|
||||||
"justguestbook/internal/database/sqlite"
|
|
||||||
"justguestbook/internal/handlers"
|
|
||||||
"justguestbook/pkg/server"
|
|
||||||
"log"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
var version string
|
|
||||||
|
|
||||||
var configFilePath *string = flag.String("conf", "/etc/justguestbook.yaml", "path to a configuration file")
|
|
||||||
var showVersion *bool = flag.Bool("v", false, "show version")
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *showVersion {
|
|
||||||
fmt.Println("justguestbookd ver.", version, "\nCopyright (c) 2022 Alexander \"Arav\" Andreev <me@arav.top>")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.SetFlags(0)
|
|
||||||
|
|
||||||
config, err := configuration.LoadConfiguration(*configFilePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(config.Owner.Password) == 0 {
|
|
||||||
log.Fatalln("empty password is not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := sqlite.New(config.DBPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("failed to init DB:", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
hand := handlers.New(config.Owner.Name, config.Owner.Password, config.AnonymousPosterName, config.PageSize, db, config.CaptchaAddr)
|
|
||||||
|
|
||||||
srv := server.NewHttpServer()
|
|
||||||
|
|
||||||
srv.GET("/", hand.Entries)
|
|
||||||
srv.POST("/", hand.New)
|
|
||||||
srv.PUT("/:entry", hand.Update)
|
|
||||||
srv.DELETE("/:entry", hand.Delete)
|
|
||||||
srv.POST("/:entry/reply", hand.Reply)
|
|
||||||
srv.PUT("/:entry/reply", hand.Update)
|
|
||||||
srv.DELETE("/:entry/reply", hand.Delete)
|
|
||||||
|
|
||||||
var network string
|
|
||||||
if !strings.ContainsRune(config.ListenOn, ':') {
|
|
||||||
network = "unix"
|
|
||||||
defer os.Remove(config.ListenOn)
|
|
||||||
} else {
|
|
||||||
ap, err := netip.ParseAddrPort(config.ListenOn)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ap.Addr().Is4() {
|
|
||||||
network = "tcp4"
|
|
||||||
} else if ap.Addr().Is6() {
|
|
||||||
network = "tcp6"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.Start(network, config.ListenOn); err != nil {
|
|
||||||
log.Fatalln("failed to start a server:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
doneSignal := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(doneSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
|
|
||||||
<-doneSignal
|
|
||||||
|
|
||||||
if err := srv.Stop(); err != nil {
|
|
||||||
log.Fatalln("failed to properly shutdown a server:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
# address:port or /path/to/unix.sock
|
|
||||||
listen_on: "/var/run/justguestbook/g.sock"
|
|
||||||
owner:
|
|
||||||
# Name to be displayed in replies.
|
|
||||||
name: "Admin"
|
|
||||||
# Password for administrative commands.
|
|
||||||
password: ""
|
|
||||||
# A name for anonymous posters.
|
|
||||||
anonymous_poster_name: "Anonymous"
|
|
||||||
# How many entries to display on one page.
|
|
||||||
page_size: 60
|
|
||||||
# Path to SQLite database file where all entries are stored.
|
|
||||||
db_path: "/var/lib/justguestbook/guestbook.sqlite"
|
|
||||||
# Address of justcaptcha service (TCP only)
|
|
||||||
captcha_addr: "http://startpage.arav.home.arpa/captcha/"
|
|
6
go.mod
6
go.mod
@ -1,10 +1,8 @@
|
|||||||
module justguestbook
|
module git.arav.top/Arav/justguestbook
|
||||||
|
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/mattn/go-sqlite3 v1.14.15
|
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
10
go.sum
10
go.sum
@ -1,10 +1,4 @@
|
|||||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Just a simple guestbook service with owner's replies
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
Restart=on-failure
|
|
||||||
DynamicUser=yes
|
|
||||||
ExecStart=/usr/bin/justguestbookd -conf /etc/justguestbook.yaml
|
|
||||||
|
|
||||||
ReadOnlyPaths=/
|
|
||||||
NoExecPaths=/
|
|
||||||
ExecPaths=/usr/bin/justguestbookd /usr/lib64
|
|
||||||
|
|
||||||
RuntimeDirectory=justguestbook
|
|
||||||
StateDirectory=justguestbook
|
|
||||||
|
|
||||||
AmbientCapabilities=
|
|
||||||
CapabilityBoundingSet=
|
|
||||||
|
|
||||||
LockPersonality=true
|
|
||||||
MemoryDenyWriteExecute=true
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateDevices=true
|
|
||||||
ProtectClock=true
|
|
||||||
ProtectControlGroups=true
|
|
||||||
ProtectHome=true
|
|
||||||
ProtectKernelLogs=true
|
|
||||||
ProtectKernelModules=true
|
|
||||||
ProtectKernelTunables=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
|
||||||
RestrictNamespaces=true
|
|
||||||
RestrictRealtime=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
SystemCallArchitectures=native
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
@ -1,45 +0,0 @@
|
|||||||
package configuration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Configuration struct {
|
|
||||||
ListenOn string `yaml:"listen_on"`
|
|
||||||
Owner struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Password string `yaml:"password"`
|
|
||||||
} `yaml:"owner"`
|
|
||||||
AnonymousPosterName string `yaml:"anonymous_poster_name"`
|
|
||||||
PageSize int64 `yaml:"page_size"`
|
|
||||||
DBPath string `yaml:"db_path"`
|
|
||||||
CaptchaAddr string `yaml:"captcha_addr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfiguration(path string) (*Configuration, error) {
|
|
||||||
configFile, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to open configuration file")
|
|
||||||
}
|
|
||||||
defer configFile.Close()
|
|
||||||
|
|
||||||
config := &Configuration{}
|
|
||||||
|
|
||||||
if err := yaml.NewDecoder(configFile).Decode(config); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to parse configuration file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitNetworkAddress splits ListenOn option and returns as two strings
|
|
||||||
// network type (e.g. tcp, unix, udp) and address:port or /path/to/prog.socket
|
|
||||||
// to listen on.
|
|
||||||
func (c *Configuration) SplitNetworkAddress() (string, string) {
|
|
||||||
s := strings.Split(c.ListenOn, " ")
|
|
||||||
return s[0], s[1]
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
UPDATE OR REPLACE `entry`
|
|
||||||
SET
|
|
||||||
`entry_id` = ?
|
|
||||||
`name` = ?,
|
|
||||||
`message` = ?,
|
|
||||||
`website` = ?,
|
|
||||||
`hide_website` = ?,
|
|
||||||
WHERE `entry_id` = ?;
|
|
@ -1,6 +0,0 @@
|
|||||||
UPDATE OR REPLACE `reply`
|
|
||||||
SET
|
|
||||||
`entry_id` = ?,
|
|
||||||
`created` = ?,
|
|
||||||
`message` = ?,
|
|
||||||
WHERE `entry_id` = ?;
|
|
@ -1,250 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"justguestbook/internal/guestbook"
|
|
||||||
"justguestbook/pkg/justcaptcha"
|
|
||||||
"justguestbook/pkg/server"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GuestbookHandlers struct {
|
|
||||||
owner string
|
|
||||||
password string
|
|
||||||
anonymousName string
|
|
||||||
defaultPageSize int64
|
|
||||||
db guestbook.Guestbook
|
|
||||||
captchaAddr string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(owner, password, anonymousName string, defaultPageSize int64, guestbook guestbook.Guestbook, captchaAddr string) *GuestbookHandlers {
|
|
||||||
return &GuestbookHandlers{
|
|
||||||
owner: owner,
|
|
||||||
password: password,
|
|
||||||
anonymousName: anonymousName,
|
|
||||||
defaultPageSize: defaultPageSize,
|
|
||||||
db: guestbook,
|
|
||||||
captchaAddr: captchaAddr}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *GuestbookHandlers) Entries(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
var page_num int64 = 1
|
|
||||||
if r.URL.Query().Get("p") != "" {
|
|
||||||
page_num, err = strconv.ParseInt(r.URL.Query().Get("p"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
page_num = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var page_size int64 = h.defaultPageSize
|
|
||||||
if r.URL.Query().Get("ps") != "" {
|
|
||||||
page_size, err = strconv.ParseInt(r.URL.Query().Get("ps"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
page_size = h.defaultPageSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := h.db.Entries(page_num, page_size)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
log.Println("failed to retrieve entries:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guestbookEntries := struct {
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
Entries []*guestbook.Entry `json:"entries"`
|
|
||||||
}{
|
|
||||||
Owner: h.owner,
|
|
||||||
Entries: entries}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(&guestbookEntries); err != nil {
|
|
||||||
log.Println("failed to encode entries:", err)
|
|
||||||
http.Error(w, fmt.Sprintln("failed to encode entries:", err.Error()), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *GuestbookHandlers) New(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var entry *guestbook.Entry
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
|
|
||||||
r.ParseForm()
|
|
||||||
|
|
||||||
if r.FormValue("captcha_id") == "" {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
solved, err := justcaptcha.CheckCaptcha(r.FormValue("captcha_id"), h.captchaAddr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
log.Println("justcaptcha:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !solved {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
name := r.FormValue("name")
|
|
||||||
if name == "" {
|
|
||||||
name = h.anonymousName
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, err = guestbook.NewEntry(name, r.FormValue("message"),
|
|
||||||
r.FormValue("website"), len(r.FormValue("hide_website")) != 0)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if r.Header.Get("Content-Type") == "application/json" {
|
|
||||||
cid := struct {
|
|
||||||
CaptchaID string `json:"captcha_id"`
|
|
||||||
}{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&cid); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
solved, err := justcaptcha.CheckCaptcha(cid.CaptchaID, h.captchaAddr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
log.Println("justcaptcha:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !solved {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(entry); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.db.NewEntry(entry)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, entry.Message, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *GuestbookHandlers) Reply(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var reply *guestbook.Reply
|
|
||||||
|
|
||||||
if r.Header.Get("X-Password") != h.password {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := strconv.ParseInt(server.GetURLParam(r, "entry"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
|
|
||||||
r.ParseForm()
|
|
||||||
|
|
||||||
reply, err = guestbook.NewReply(id, r.FormValue("reply"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if r.Header.Get("Content-Type") == "application/json" {
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(reply); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.db.NewReply(reply); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *GuestbookHandlers) Update(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Header.Get("X-Password") != h.password {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entryID, err := strconv.ParseInt(server.GetURLParam(r, "entry"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(r.URL.Path, "reply") {
|
|
||||||
rp := guestbook.Reply{}
|
|
||||||
json.NewDecoder(r.Body).Decode(&rp)
|
|
||||||
|
|
||||||
isCreated, err := h.db.UpdateReply(entryID, &rp)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isCreated {
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(&rp)
|
|
||||||
} else {
|
|
||||||
et := guestbook.Entry{}
|
|
||||||
json.NewDecoder(r.Body).Decode(&et)
|
|
||||||
|
|
||||||
isCreated, err := h.db.UpdateEntry(entryID, &et)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isCreated {
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(&et)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *GuestbookHandlers) Delete(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Header.Get("X-Password") != h.password {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entryID, err := strconv.ParseInt(server.GetURLParam(r, "entry"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(r.URL.Path, "reply") {
|
|
||||||
if err := h.db.DeleteReply(entryID); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := h.db.DeleteEntry(entryID); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,17 +4,18 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"justguestbook/internal/guestbook"
|
|
||||||
|
|
||||||
|
"git.arav.top/Arav/justguestbook/pkg/guestbook"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed queries/schema.sql
|
//go:embed queries/schema.sql
|
||||||
queryCreateDatabase string
|
querySchema string
|
||||||
|
|
||||||
//go:embed queries/entryGetAll.sql
|
//go:embed queries/entryGetAll.sql
|
||||||
queryGetEntries string
|
queryGetAll string
|
||||||
//go:embed queries/entryCount.sql
|
//go:embed queries/entryCount.sql
|
||||||
queryCount string
|
queryCount string
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
stmtGetEntries *sql.Stmt
|
stmtGetAll *sql.Stmt
|
||||||
stmtCount *sql.Stmt
|
stmtCount *sql.Stmt
|
||||||
stmtNewEntry *sql.Stmt
|
stmtNewEntry *sql.Stmt
|
||||||
stmtUpdateEntry *sql.Stmt
|
stmtUpdateEntry *sql.Stmt
|
||||||
@ -44,49 +45,51 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func initDBStatements(db *sql.DB) error {
|
func initDBStatements(db *sql.DB) error {
|
||||||
_, err := db.Exec(queryCreateDatabase)
|
db.Exec("PRAGMA foreign_keys = ON;")
|
||||||
|
|
||||||
|
_, err := db.Exec(querySchema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to init schema")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmtGetEntries, err = db.Prepare(queryGetEntries)
|
stmtGetAll, err = db.Prepare(queryGetAll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to prepare queryGetAll")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmtCount, err = db.Prepare(queryCount)
|
stmtCount, err = db.Prepare(queryCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to prepare queryCount")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmtNewEntry, err = db.Prepare(queryNewEntry)
|
stmtNewEntry, err = db.Prepare(queryNewEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to prepare queryNewEntry")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmtUpdateEntry, err = db.Prepare(queryUpdateEntry)
|
stmtUpdateEntry, err = db.Prepare(queryUpdateEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to prepare queryUpdateEntry")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmtDeleteEntry, err = db.Prepare(queryDeleteEntry)
|
stmtDeleteEntry, err = db.Prepare(queryDeleteEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to prepare queryDeleteEntry")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmtNewReply, err = db.Prepare(queryNewReply)
|
stmtNewReply, err = db.Prepare(queryNewReply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to prepare queryNewReply")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmtUpdateReply, err = db.Prepare(queryUpdateReply)
|
stmtUpdateReply, err = db.Prepare(queryUpdateReply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to prepare queryUpdateReply")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmtDeleteReply, err = db.Prepare(queryDeleteReply)
|
stmtDeleteReply, err = db.Prepare(queryDeleteReply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to prepare queryDeleteReply")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -116,7 +119,7 @@ func (d *SQLiteDatabase) Entries(page, pageSize int64) (entries []*guestbook.Ent
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
rows, err := tx.Stmt(stmtGetEntries).Query(pageSize, (page-1)*pageSize)
|
rows, err := tx.Stmt(stmtGetAll).Query(pageSize, (page-1)*pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -128,13 +131,14 @@ func (d *SQLiteDatabase) Entries(page, pageSize int64) (entries []*guestbook.Ent
|
|||||||
var reply_message sql.NullString
|
var reply_message sql.NullString
|
||||||
if err = rows.Scan(
|
if err = rows.Scan(
|
||||||
&entry.ID, &entry.Created, &entry.Name,
|
&entry.ID, &entry.Created, &entry.Name,
|
||||||
&entry.Website, &entry.Message,
|
&entry.Message, &entry.Website,
|
||||||
&reply_created, &reply_message); err != nil {
|
&reply_created, &reply_message); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if reply_message.Valid /* reply_created is also valid if reply is */ {
|
if reply_message.Valid /* reply_created is also valid if reply is */ {
|
||||||
entry.Reply = &guestbook.Reply{
|
entry.Reply = &guestbook.Reply{
|
||||||
|
ID: entry.ID,
|
||||||
Created: reply_created.String,
|
Created: reply_created.String,
|
||||||
Message: reply_message.String}
|
Message: reply_message.String}
|
||||||
}
|
}
|
||||||
@ -147,22 +151,25 @@ func (d *SQLiteDatabase) Entries(page, pageSize int64) (entries []*guestbook.Ent
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDatabase) Count() (int64, error) {
|
// Count returns how much entries are in an `entry` table.
|
||||||
|
func (d *SQLiteDatabase) Count() (count int64, err error) {
|
||||||
tx, err := d.db.Begin()
|
tx, err := d.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, err
|
return -1, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
var count int64
|
|
||||||
err = tx.Stmt(stmtCount).QueryRow().Scan(&count)
|
err = tx.Stmt(stmtCount).QueryRow().Scan(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, err
|
return -1, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewEntry inserts a passed Entry struct and fills its ID field if successful.
|
||||||
func (d *SQLiteDatabase) NewEntry(entry *guestbook.Entry) error {
|
func (d *SQLiteDatabase) NewEntry(entry *guestbook.Entry) error {
|
||||||
tx, err := d.db.Begin()
|
tx, err := d.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -170,93 +177,99 @@ func (d *SQLiteDatabase) NewEntry(entry *guestbook.Entry) error {
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
_, err = tx.Stmt(stmtNewEntry).Exec(entry.Created, entry.Name, entry.Message,
|
r, err := tx.Stmt(stmtNewEntry).Exec(entry.Created, entry.Name, entry.Message,
|
||||||
entry.Website, entry.HideWebsite)
|
entry.Website, entry.HideWebsite)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.ID, err = r.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDatabase) UpdateEntry(entryID int64, entry *guestbook.Entry) (bool, error) {
|
// UpdateEntry
|
||||||
|
func (d *SQLiteDatabase) UpdateEntry(entry *guestbook.Entry) (*guestbook.Entry, error) {
|
||||||
tx, err := d.db.Begin()
|
tx, err := d.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
res, err := tx.Stmt(stmtUpdateEntry).Exec(entry.ID, entry.Name, entry.Message, entry.Website, entry.HideWebsite, entryID)
|
row := tx.Stmt(stmtUpdateEntry).QueryRow(entry.Name, entry.Message, entry.Website, entry.HideWebsite, entry.ID)
|
||||||
|
|
||||||
|
uEntry := guestbook.Entry{}
|
||||||
|
err = row.Scan(&uEntry.ID, &uEntry.Created, &uEntry.Name, &uEntry.Message, &uEntry.Website, &uEntry.HideWebsite)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ra, err := res.RowsAffected()
|
tx.Commit()
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ra > 0, nil
|
return &uEntry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDatabase) DeleteEntry(entryID int64) error {
|
func (d *SQLiteDatabase) DeleteEntry(entryID int64) (int64, error) {
|
||||||
tx, err := d.db.Begin()
|
tx, err := d.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return -1, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
res, err := tx.Stmt(stmtDeleteEntry).Exec(entryID)
|
res, err := tx.Stmt(stmtDeleteEntry).Exec(entryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return -1, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = res.RowsAffected()
|
c, err := res.RowsAffected()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return -1, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
tx.Commit()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDatabase) NewReply(reply *guestbook.Reply) (err error) {
|
func (d *SQLiteDatabase) NewReply(reply *guestbook.Reply) error {
|
||||||
tx, err := d.db.Begin()
|
tx, err := d.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
_, err = tx.Stmt(stmtNewReply).Exec(reply.ID, reply.Created, reply.Message)
|
_, err = tx.Stmt(stmtNewReply).Exec(reply.ID, reply.Created, reply.Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateEntry
|
func (d *SQLiteDatabase) UpdateReply(reply *guestbook.Reply) (*guestbook.Reply, error) {
|
||||||
func (d *SQLiteDatabase) UpdateReply(entryID int64, reply *guestbook.Reply) (bool, error) {
|
|
||||||
tx, err := d.db.Begin()
|
tx, err := d.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
res, err := tx.Stmt(stmtUpdateReply).Exec(reply.ID, reply.Created, reply.Message, entryID)
|
uReply := guestbook.Reply{}
|
||||||
|
err = tx.Stmt(stmtUpdateReply).QueryRow(
|
||||||
|
reply.Created, reply.Message, reply.ID).Scan(&uReply.ID, &uReply.Created, &uReply.Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ra, err := res.RowsAffected()
|
tx.Commit()
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ra > 0, nil
|
return &uReply, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDatabase) DeleteReply(entryID int64) error {
|
func (d *SQLiteDatabase) DeleteReply(entryID int64) error {
|
||||||
@ -276,10 +289,21 @@ func (d *SQLiteDatabase) DeleteReply(entryID int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDatabase) Close() error {
|
func (d *SQLiteDatabase) Close() error {
|
||||||
|
stmtCount.Close()
|
||||||
|
stmtDeleteEntry.Close()
|
||||||
|
stmtDeleteReply.Close()
|
||||||
|
stmtGetAll.Close()
|
||||||
|
stmtNewEntry.Close()
|
||||||
|
stmtNewReply.Close()
|
||||||
|
stmtUpdateEntry.Close()
|
||||||
|
stmtUpdateReply.Close()
|
||||||
|
|
||||||
return d.db.Close()
|
return d.db.Close()
|
||||||
}
|
}
|
||||||
|
|
150
pkg/database/sqlite/database_test.go
Normal file
150
pkg/database/sqlite/database_test.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package sqlite_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.arav.top/Arav/justguestbook/pkg/database/sqlite"
|
||||||
|
"git.arav.top/Arav/justguestbook/pkg/guestbook"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testEMsg = "kek"
|
||||||
|
testRMsg = "lol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func genTestDB() (db *sqlite.SQLiteDatabase, err error) {
|
||||||
|
db, err = sqlite.New(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to init DB")
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := guestbook.NewEntry("Anonymous", testEMsg, "", true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create a new entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.NewEntry(e); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to insert a new entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := guestbook.NewReply(e.ID, testRMsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create a new reply")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.NewReply(r); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to insert a new reply")
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqliteInsertAndGetAll(t *testing.T) {
|
||||||
|
db, err := genTestDB()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = db.Close(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
es, err := db.Entries(1, 30)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := db.Count(); err != nil || int64(len(es)) != c {
|
||||||
|
t.Errorf("entries count mismatch (%d != %d). Error: %s", len(es), c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if es[0].Reply.Message != testRMsg {
|
||||||
|
t.Error("reply isn't", testRMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqliteUpdateEntry(t *testing.T) {
|
||||||
|
db, err := genTestDB()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = db.Close(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
es0, err := db.Entries(1, 30)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to obtain entries. Error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
me := es0[0]
|
||||||
|
|
||||||
|
me.ID = es0[0].ID
|
||||||
|
me.Name = "NotSoAnonymous"
|
||||||
|
|
||||||
|
ne, err := db.UpdateEntry(me)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ne.Name != me.Name {
|
||||||
|
t.Error(ne.Name, "!=", me.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqliteUpdateReply(t *testing.T) {
|
||||||
|
db, err := genTestDB()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = db.Close(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
es0, err := db.Entries(1, 30)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to obtain entries. Error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mr := es0[0].Reply
|
||||||
|
|
||||||
|
mr.ID = es0[0].ID
|
||||||
|
mr.Message = "bur"
|
||||||
|
|
||||||
|
nr, err := db.UpdateReply(mr)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nr.Message != mr.Message {
|
||||||
|
t.Error(nr.Message, "!=", mr.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqliteDeleteEntry(t *testing.T) {
|
||||||
|
db, err := genTestDB()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = db.Close(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dc, err := db.DeleteEntry(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("cannot delete entry", err, dc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := db.Count(); err != nil || c != 0 {
|
||||||
|
t.Error(c, err, dc)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
pkg/database/sqlite/queries/entryUpdate.sql
Normal file
8
pkg/database/sqlite/queries/entryUpdate.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
UPDATE OR REPLACE `entry`
|
||||||
|
SET
|
||||||
|
`name` = ?,
|
||||||
|
`message` = ?,
|
||||||
|
`website` = ?,
|
||||||
|
`hide_website` = ?
|
||||||
|
WHERE `entry_id` = ?
|
||||||
|
RETURNING `entry_id`, `created`, `name`, `message`, `website`, `hide_website`;
|
6
pkg/database/sqlite/queries/replyUpdate.sql
Normal file
6
pkg/database/sqlite/queries/replyUpdate.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
UPDATE OR REPLACE `reply`
|
||||||
|
SET
|
||||||
|
`created` = ?,
|
||||||
|
`message` = ?
|
||||||
|
WHERE `entry_id` = ?
|
||||||
|
RETURNING `entry_id`, `created`, `message`;
|
@ -6,10 +6,10 @@ type Guestbook interface {
|
|||||||
Entries(page, pageSize int64) ([]*Entry, error)
|
Entries(page, pageSize int64) ([]*Entry, error)
|
||||||
Count() (int64, error)
|
Count() (int64, error)
|
||||||
NewEntry(entry *Entry) error
|
NewEntry(entry *Entry) error
|
||||||
UpdateEntry(entryID int64, entry *Entry) (bool, error)
|
UpdateEntry(entry *Entry) (*Entry, error)
|
||||||
DeleteEntry(entryID int64) error
|
DeleteEntry(entryID int64) error
|
||||||
NewReply(reply *Reply) error
|
NewReply(reply *Reply) error
|
||||||
UpdateReply(entryID int64, reply *Reply) (bool, error)
|
UpdateReply(reply *Reply) (*Entry, error)
|
||||||
DeleteReply(entryID int64) error
|
DeleteReply(entryID int64) error
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Reply struct {
|
type Reply struct {
|
||||||
ID int64 `json:"id,omitempty"`
|
ID int64 `json:"-"`
|
||||||
Created string `json:"created,omitempty"`
|
Created string `json:"created,omitempty"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
@ -1,20 +0,0 @@
|
|||||||
package justcaptcha
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CheckCaptcha performs a request to a justcaptcha service and returns wether
|
|
||||||
// CAPTCHA was solved or not. If there is a problem with connection to the
|
|
||||||
// service it will return an error.
|
|
||||||
func CheckCaptcha(id string, serviceURL string) (bool, error) {
|
|
||||||
path, _ := url.JoinPath(serviceURL, id)
|
|
||||||
|
|
||||||
r, err := http.Get(path)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.StatusCode == 202, nil
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HttpServer struct {
|
|
||||||
server *http.Server
|
|
||||||
router *httprouter.Router
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHttpServer() *HttpServer {
|
|
||||||
r := httprouter.New()
|
|
||||||
return &HttpServer{
|
|
||||||
server: &http.Server{
|
|
||||||
ReadTimeout: 3 * time.Second,
|
|
||||||
WriteTimeout: 3 * time.Second,
|
|
||||||
Handler: r,
|
|
||||||
},
|
|
||||||
router: r,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) GET(path string, handler http.HandlerFunc) {
|
|
||||||
s.router.Handler(http.MethodGet, path, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) POST(path string, handler http.HandlerFunc) {
|
|
||||||
s.router.Handler(http.MethodPost, path, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) PATCH(path string, handler http.HandlerFunc) {
|
|
||||||
s.router.Handler(http.MethodPatch, path, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) PUT(path string, handler http.HandlerFunc) {
|
|
||||||
s.router.Handler(http.MethodPut, path, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) DELETE(path string, handler http.HandlerFunc) {
|
|
||||||
s.router.Handler(http.MethodDelete, path, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) SetNotFoundHandler(handler http.HandlerFunc) {
|
|
||||||
s.router.NotFound = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetURLParam wrapper around underlying router for getting URL parameters.
|
|
||||||
func GetURLParam(r *http.Request, param string) string {
|
|
||||||
return httprouter.ParamsFromContext(r.Context()).ByName(param)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) Start(network, address string) error {
|
|
||||||
listener, err := net.Listen(network, address)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if listener.Addr().Network() == "unix" {
|
|
||||||
os.Chmod(address, 0777)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) Stop() error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := s.server.Shutdown(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user