Add database-based settings, settings screen and migrate sections to db and allow to configure them

This commit is contained in:
Jan-Lukas Else 2022-07-16 21:09:43 +02:00
parent 22fce13246
commit cd9d500a55
44 changed files with 589 additions and 429 deletions

View File

@ -5,7 +5,6 @@ import (
"encoding/pem"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -15,15 +14,10 @@ import (
func Test_loadActivityPubPrivateKey(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
},
cfg: createDefaultTestConfig(t),
}
err := app.initDatabase(false)
err := app.initConfig(false)
require.NoError(t, err)
defer app.db.close()
require.NotNil(t, app.db)
// Generate
@ -55,9 +49,7 @@ func Test_webfinger(t *testing.T) {
cfg: createDefaultTestConfig(t),
}
app.cfg.Server.PublicAddress = "https://example.com"
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
app.prepareWebfinger()

View File

@ -29,9 +29,7 @@ func Test_authMiddleware(t *testing.T) {
},
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
app.d = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

View File

@ -4,7 +4,6 @@ import (
"context"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -17,13 +16,12 @@ func Test_blogroll(t *testing.T) {
app := &goBlog{
httpClient: fc.Client,
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{},
DefaultBlog: "en",
Blogs: map[string]*configBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Cache.Enable = false
app.cfg.DefaultBlog = "en"
app.cfg.Blogs = map[string]*configBlog{
"en": {
Lang: "en",
Blogroll: &configBlogroll{
@ -35,16 +33,9 @@ func Test_blogroll(t *testing.T) {
Categories: []string{"A", "B"},
},
},
},
User: &configUser{},
Cache: &configCache{
Enable: false,
},
},
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
fc.setFakeResponse(http.StatusOK, `

View File

@ -5,7 +5,6 @@ import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -14,15 +13,12 @@ import (
)
func Test_blogStats(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.com",
},
Blogs: map[string]*configBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Blogs = map[string]*configBlog{
"en": {
Lang: "en",
BlogStats: &configBlogStats{
@ -33,17 +29,10 @@ func Test_blogStats(t *testing.T) {
"test": {},
},
},
},
DefaultBlog: "en",
User: &configUser{},
Webmention: &configWebmention{
DisableSending: true,
},
},
}
app.cfg.DefaultBlog = "en"
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
// Insert post

View File

@ -23,9 +23,7 @@ func Test_captchaMiddleware(t *testing.T) {
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
app.d = alice.New(app.checkIsCaptcha, app.captchaMiddleware).ThenFunc(func(rw http.ResponseWriter, r *http.Request) {

View File

@ -5,7 +5,6 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"strings"
"testing"
@ -18,25 +17,20 @@ import (
func Test_comments(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.com",
},
Blogs: map[string]*configBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Blogs = map[string]*configBlog{
"en": {
Lang: "en",
Comments: &configComments{
Enabled: true,
},
},
DefaultBlog: "en",
User: &configUser{},
},
}
app.cfg.DefaultBlog = "en"
_ = app.initDatabase(false)
defer app.db.close()
err := app.initConfig(false)
require.NoError(t, err)
app.initComponents(false)
t.Run("Successful comment", func(t *testing.T) {
@ -44,7 +38,7 @@ func Test_comments(t *testing.T) {
// Create comment
data := url.Values{}
data.Add("target", "https://example.com/test")
data.Add("target", "http://localhost:8080/test")
data.Add("comment", "This is just a test")
data.Add("name", "Test name")
data.Add("website", "https://goblog.app")
@ -115,7 +109,7 @@ func Test_comments(t *testing.T) {
// Create comment
data := url.Values{}
data.Add("target", "https://example.com/test")
data.Add("target", "http://localhost:8080/test")
data.Add("comment", "This is just a test")
req := httptest.NewRequest(http.MethodPost, commentPath, strings.NewReader(data.Encode()))
@ -151,7 +145,7 @@ func Test_comments(t *testing.T) {
t.Run("Empty comment", func(t *testing.T) {
data := url.Values{}
data.Add("target", "https://example.com/test")
data.Add("target", "http://localhost:8080/test")
data.Add("comment", "")
req := httptest.NewRequest(http.MethodPost, commentPath, strings.NewReader(data.Encode()))

View File

@ -341,13 +341,17 @@ func (a *goBlog) loadConfigFile(file string) error {
return v.Unmarshal(a.cfg)
}
func (a *goBlog) initConfig() error {
func (a *goBlog) initConfig(logging bool) error {
if a.cfg == nil {
a.cfg = createDefaultConfig()
}
if a.cfg.initialized {
return nil
}
// Init database
if err := a.initDatabase(logging); err != nil {
return err
}
// Check config
// Parse addresses and hostnames
if a.cfg.Server.PublicAddress == "" {
@ -402,12 +406,24 @@ func (a *goBlog) initConfig() error {
b.Comments = &configComments{Enabled: false}
}
}
// Check if sections already migrated to db
const sectionMigrationKey = "sections_migrated"
if val, err := a.getSettingValue(sectionMigrationKey); err != nil {
return err
} else if val == "" {
if err = a.saveAllSections(); err != nil {
return err
}
if err = a.saveSettingValue(sectionMigrationKey, "1"); err != nil {
return err
}
}
// Load db sections
if err = a.loadSections(); err != nil {
return err
}
// Check config for each blog
for _, blog := range a.cfg.Blogs {
// Copy sections key to section name
for k, s := range blog.Sections {
s.Name = k
}
// Check if language is set
if blog.Lang == "" {
blog.Lang = "en"

View File

@ -5,7 +5,6 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"strings"
"testing"
@ -24,14 +23,9 @@ func Test_contact(t *testing.T) {
// Init everything
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.com",
},
Blogs: map[string]*configBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Blogs = map[string]*configBlog{
"en": {
Lang: "en",
// Config for contact
@ -46,13 +40,10 @@ func Test_contact(t *testing.T) {
EmailSubject: "Neue Kontaktnachricht",
},
},
},
DefaultBlog: "en",
User: &configUser{},
},
}
_ = app.initDatabase(false)
defer app.db.close()
app.cfg.DefaultBlog = "en"
_ = app.initConfig(false)
app.initComponents(false)
// Make contact form request

View File

@ -31,6 +31,9 @@ type database struct {
}
func (a *goBlog) initDatabase(logging bool) (err error) {
if a.db != nil {
return
}
if logging {
log.Println("Initialize database...")
}

9
dbmigrations/00029.sql Normal file
View File

@ -0,0 +1,9 @@
create table sections (
blog text not null,
name text not null,
title text not null default '',
description text not null default '',
pathtemplate text not null default '',
showfull boolean not null default false,
primary key (blog, name)
);

5
dbmigrations/00030.sql Normal file
View File

@ -0,0 +1,5 @@
create table settings (
name text not null,
value text not null default '',
primary key (name)
);

View File

@ -15,9 +15,7 @@ func Test_editorPreview(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
h := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

View File

@ -4,7 +4,6 @@ import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -13,25 +12,10 @@ import (
func Test_errors(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.com",
},
Blogs: map[string]*configBlog{
"en": {
Lang: "en",
},
},
DefaultBlog: "en",
User: &configUser{},
},
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
t.Run("Test 404, no HTML", func(t *testing.T) {

View File

@ -11,14 +11,9 @@ import (
func Test_export(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
},
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initMarkdown()
err := app.db.savePost(&post{

View File

@ -15,10 +15,9 @@ func Test_feeds(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
app.d = app.buildRouter()
handlerClient := newHandlerClient(app.d)

View File

@ -2,7 +2,6 @@ package main
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -11,24 +10,18 @@ import (
func Test_geoTrack(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{},
Blogs: map[string]*configBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Blogs = map[string]*configBlog{
"en": {
Lang: "en",
},
"de": {
Lang: "de",
},
},
},
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
// First test (just with track)

View File

@ -16,9 +16,8 @@ func Test_geo(t *testing.T) {
httpClient: fc.Client,
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
p := &post{

10
http.go
View File

@ -32,7 +32,7 @@ const (
func (a *goBlog) startServer() (err error) {
log.Println("Start server(s)...")
// Load router
a.d = a.buildRouter()
a.reloadRouter()
// Set basic middlewares
h := alice.New()
h = h.Append(middleware.Heartbeat("/ping"))
@ -43,7 +43,9 @@ func (a *goBlog) startServer() (err error) {
if a.httpsConfigured(false) {
h = h.Append(a.securityHeaders)
}
finalHandler := h.Then(a.d)
finalHandler := h.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
a.d.ServeHTTP(w, r)
})
// Start Onion service
if a.cfg.Server.Tor {
go func() {
@ -116,6 +118,10 @@ const (
feedPath = ".{feed:(rss|json|atom)}"
)
func (a *goBlog) reloadRouter() {
a.d = a.buildRouter()
}
func (a *goBlog) buildRouter() http.Handler {
mapRouter := &maprouter.MapRouter{
Handlers: map[string]http.Handler{},

View File

@ -15,7 +15,7 @@ func Test_httpLogsConfig(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initConfig(false)
assert.Equal(t, false, app.cfg.Server.Logging)
assert.Equal(t, "data/access.log", app.cfg.Server.LogFile)

View File

@ -170,6 +170,9 @@ func (a *goBlog) blogRouter(blog string, conf *configBlog) func(r chi.Router) {
// Sitemap
r.Group(a.blogSitemapRouter(conf))
// Settings
r.Route(conf.getRelativePath(settingsPath), a.blogSettingsRouter(conf))
}
}
@ -442,3 +445,14 @@ func (a *goBlog) blogSitemapRouter(conf *configBlog) func(r chi.Router) {
r.Get(conf.getRelativePath(sitemapBlogPostsPath), a.serveSitemapBlogPosts)
}
}
// Blog - Settings
func (a *goBlog) blogSettingsRouter(_ *configBlog) func(r chi.Router) {
return func(r chi.Router) {
r.Use(a.authMiddleware)
r.Get("/", a.serveSettings)
r.Post(settingsDeleteSectionPath, a.settingsDeleteSection)
r.Post(settingsCreateSectionPath, a.settingsCreateSection)
r.Post(settingsUpdateSectionPath, a.settingsUpdateSection)
}
}

View File

@ -19,9 +19,8 @@ func Test_indexNow(t *testing.T) {
httpClient: fc.Client,
}
app.cfg.IndexNow = &configIndexNow{Enabled: true}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
// Create http router

View File

@ -5,7 +5,6 @@ import (
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
@ -22,35 +21,25 @@ func Test_indieAuthServer(t *testing.T) {
app := &goBlog{
httpClient: newFakeHttpClient().Client,
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.org",
},
DefaultBlog: "en",
Blogs: map[string]*configBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Server.PublicAddress = "https://example.org"
app.cfg.Blogs = map[string]*configBlog{
"en": {
Lang: "en",
},
},
User: &configUser{
}
app.cfg.User = &configUser{
Name: "John Doe",
Nick: "jdoe",
},
Cache: &configCache{
Enable: false,
},
},
}
app.cfg.Cache.Enable = false
_ = app.initConfig(false)
app.initComponents(false)
app.d = app.buildRouter()
_ = app.initDatabase(false)
defer app.db.close()
app.initComponents(false)
app.ias.Client = newHandlerClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))

View File

@ -3,7 +3,6 @@ package main
import (
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
@ -15,22 +14,10 @@ func Test_checkIndieAuth(t *testing.T) {
app := &goBlog{
httpClient: newFakeHttpClient().Client,
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{},
DefaultBlog: "en",
Blogs: map[string]*configBlog{
"en": {
Lang: "en",
},
},
},
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
req := httptest.NewRequest(http.MethodGet, "/", nil)

View File

@ -64,7 +64,7 @@ func main() {
app.logErrAndQuit("Failed to load config file:", err.Error())
return
}
if err = app.initConfig(); err != nil {
if err = app.initConfig(false); err != nil {
app.logErrAndQuit("Failed to init config:", err.Error())
return
}
@ -129,12 +129,6 @@ func main() {
// Execute pre-start hooks
app.preStartHooks()
// Initialize database
if err = app.initDatabase(true); err != nil {
app.logErrAndQuit("Failed to init database:", err.Error())
return
}
// Link check tool after init of markdown
if len(os.Args) >= 2 && os.Args[1] == "check" {
app.initMarkdown()

View File

@ -14,9 +14,7 @@ func Test_micropubQuery(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
// Create a test post with tags

View File

@ -18,9 +18,7 @@ func Test_ntfySending(t *testing.T) {
httpClient: fakeClient.Client,
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
t.Run("Default", func(t *testing.T) {

View File

@ -1,7 +1,6 @@
package main
import (
"path/filepath"
"testing"
"time"
@ -25,9 +24,7 @@ func Test_postsDb(t *testing.T) {
},
},
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
now := toLocalSafe(time.Now().String())
@ -229,14 +226,9 @@ func Test_ftsWithoutTitle(t *testing.T) {
// Added because there was a bug where there were no search results without title
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
},
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initMarkdown()
err := app.db.savePost(&post{
@ -261,14 +253,9 @@ func Test_postsPriority(t *testing.T) {
// Added because there was a bug where there were no search results without title
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
},
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initMarkdown()
err := app.db.savePost(&post{
@ -312,14 +299,9 @@ func Test_postsPriority(t *testing.T) {
func Test_usesOfMediaFile(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
},
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
err := app.db.savePost(&post{
Path: "/test/abc",
@ -368,8 +350,7 @@ func Test_replaceParams(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
err := app.db.savePost(&post{
Path: "/test/abc",
@ -399,9 +380,7 @@ func Test_postDeletesParams(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
err := app.createPost(&post{

View File

@ -11,9 +11,8 @@ func Test_checkDeletedPosts(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
// Create a post

View File

@ -1,7 +1,6 @@
package main
import (
"path/filepath"
"testing"
"time"
@ -12,28 +11,18 @@ import (
func Test_postsScheduler(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.com",
},
DefaultBlog: "en",
Blogs: map[string]*configBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Blogs = map[string]*configBlog{
"en": {
Sections: map[string]*configSection{
"test": {},
},
Lang: "en",
},
},
Micropub: &configMicropub{},
},
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
err := app.db.savePost(&post{

View File

@ -16,9 +16,7 @@ func Test_serveDate(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
app.d = app.buildRouter()
@ -106,9 +104,7 @@ func Test_servePost(t *testing.T) {
Username: "test",
Password: "test",
})
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
app.d = app.buildRouter()

View File

@ -3,7 +3,6 @@ package main
import (
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/go-chi/chi/v5/middleware"
@ -16,15 +15,11 @@ func Test_privateMode(t *testing.T) {
// Init
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "db.db"),
},
Server: &configServer{},
PrivateMode: &configPrivateMode{
Enabled: true,
},
User: &configUser{
cfg: createDefaultTestConfig(t),
}
app.cfg.PrivateMode = &configPrivateMode{Enabled: true}
app.cfg.User =
&configUser{
Name: "Test",
Nick: "test",
Password: "testpw",
@ -34,18 +29,14 @@ func Test_privateMode(t *testing.T) {
Password: "pw",
},
},
},
DefaultBlog: "en",
Blogs: map[string]*configBlog{
}
app.cfg.Blogs = map[string]*configBlog{
"en": {
Lang: "en",
},
},
},
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
handler := alice.New(middleware.WithValue(blogKey, "en"), app.privateModeHandler).ThenFunc(func(rw http.ResponseWriter, r *http.Request) {

View File

@ -2,7 +2,6 @@ package main
import (
"context"
"path/filepath"
"testing"
"time"
@ -12,14 +11,9 @@ import (
func Test_queue(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
},
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
time1 := time.Now()
@ -65,30 +59,3 @@ func Test_queue(t *testing.T) {
require.Equal(t, []byte("1"), qi.content)
}
func Benchmark_queue(b *testing.B) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(b.TempDir(), "test.db"),
},
},
}
_ = app.initDatabase(false)
defer app.db.close()
err := app.enqueue("test", []byte("1"), time.Now())
require.NoError(b, err)
b.Run("Peek with item", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = app.peekQueue(context.Background(), "test")
}
})
b.Run("Peek without item", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = app.peekQueue(context.Background(), "abc")
}
})
}

View File

@ -15,9 +15,7 @@ func Test_reactionsLowLevel(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
err := app.saveReaction("🖕", "/testpost")
@ -97,9 +95,7 @@ func Test_reactionsHighLevel(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
// Send unsuccessful reaction

129
settings.go Normal file
View File

@ -0,0 +1,129 @@
package main
import (
"net/http"
"sort"
"github.com/samber/lo"
)
const settingsPath = "/settings"
func (a *goBlog) serveSettings(w http.ResponseWriter, r *http.Request) {
blog, bc := a.getBlog(r)
sections := lo.Values(bc.Sections)
sort.Slice(sections, func(i, j int) bool { return sections[i].Name < sections[j].Name })
a.render(w, r, a.renderSettings, &renderData{
Data: &settingsRenderData{
blog: blog,
sections: sections,
},
})
}
const settingsDeleteSectionPath = "/deletesection"
func (a *goBlog) settingsDeleteSection(w http.ResponseWriter, r *http.Request) {
blog, bc := a.getBlog(r)
section := r.FormValue("sectionname")
// Check if any post uses this section
count, err := a.db.countPosts(&postsRequestConfig{
blog: blog,
sections: []string{section},
})
if err != nil {
a.serveError(w, r, "Failed to check if section is still used", http.StatusInternalServerError)
return
}
if count > 0 {
a.serveError(w, r, "Section is still used", http.StatusBadRequest)
return
}
// Delete section
err = a.deleteSection(blog, section)
if err != nil {
a.serveError(w, r, "Failed to delete section from the database", http.StatusInternalServerError)
return
}
// Reload sections
err = a.loadSections()
if err != nil {
a.serveError(w, r, "Failed to reload section configuration from the database", http.StatusInternalServerError)
return
}
a.reloadRouter()
a.cache.purge()
http.Redirect(w, r, bc.getRelativePath(settingsPath), http.StatusFound)
}
const settingsCreateSectionPath = "/createsection"
func (a *goBlog) settingsCreateSection(w http.ResponseWriter, r *http.Request) {
blog, bc := a.getBlog(r)
// Read values
sectionName := r.FormValue("sectionname")
sectionTitle := r.FormValue("sectiontitle")
if sectionName == "" || sectionTitle == "" {
a.serveError(w, r, "Missing values for name or title", http.StatusBadRequest)
return
}
// Create section
section := &configSection{
Name: sectionName,
Title: sectionTitle,
}
err := a.addSection(blog, section)
if err != nil {
a.serveError(w, r, "Failed to insert section into database", http.StatusInternalServerError)
return
}
// Reload sections
err = a.loadSections()
if err != nil {
a.serveError(w, r, "Failed to reload section configuration from the database", http.StatusInternalServerError)
return
}
a.reloadRouter()
a.cache.purge()
http.Redirect(w, r, bc.getRelativePath(settingsPath), http.StatusFound)
}
const settingsUpdateSectionPath = "/updatesection"
func (a *goBlog) settingsUpdateSection(w http.ResponseWriter, r *http.Request) {
blog, bc := a.getBlog(r)
// Read values
sectionName := r.FormValue("sectionname")
sectionTitle := r.FormValue("sectiontitle")
if sectionName == "" || sectionTitle == "" {
a.serveError(w, r, "Missing values for name or title", http.StatusBadRequest)
return
}
sectionDescription := r.FormValue("sectiondescription")
sectionPathTemplate := r.FormValue("sectionpathtemplate")
sectionShowFull := r.FormValue("sectionshowfull") == "on"
// Create section
section := &configSection{
Name: sectionName,
Title: sectionTitle,
Description: sectionDescription,
PathTemplate: sectionPathTemplate,
ShowFull: sectionShowFull,
}
err := a.updateSection(blog, sectionName, section)
if err != nil {
a.serveError(w, r, "Failed to update section in database", http.StatusInternalServerError)
return
}
// Reload sections
err = a.loadSections()
if err != nil {
a.serveError(w, r, "Failed to reload section configuration from the database", http.StatusInternalServerError)
return
}
a.reloadRouter()
a.cache.purge()
http.Redirect(w, r, bc.getRelativePath(settingsPath), http.StatusFound)
}

103
settingsDb.go Normal file
View File

@ -0,0 +1,103 @@
package main
import (
"database/sql"
"errors"
)
func (a *goBlog) getSettingValue(name string) (string, error) {
row, err := a.db.queryRow("select value from settings where name = @name", sql.Named("name", name))
if err != nil {
return "",
err
}
var value string
err = row.Scan(&value)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
} else if err != nil {
return "", err
}
return value, nil
}
func (a *goBlog) saveSettingValue(name, value string) error {
_, err := a.db.exec(
"insert into settings (name, value) values (@name, @value) on conflict (name) do update set value = @value2",
sql.Named("name", name),
sql.Named("value", value),
sql.Named("value2", value),
)
return err
}
func (a *goBlog) loadSections() error {
for blog, bc := range a.cfg.Blogs {
sections, err := a.getSections(blog)
if err != nil {
return err
}
bc.Sections = sections
}
return nil
}
func (a *goBlog) getSections(blog string) (map[string]*configSection, error) {
rows, err := a.db.query("select name, title, description, pathtemplate, showfull from sections where blog = @blog", sql.Named("blog", blog))
if err != nil {
return nil, err
}
sections := map[string]*configSection{}
for rows.Next() {
section := &configSection{}
err = rows.Scan(&section.Name, &section.Title, &section.Description, &section.PathTemplate, &section.ShowFull)
if err != nil {
return nil, err
}
sections[section.Name] = section
}
return sections, nil
}
func (a *goBlog) saveAllSections() error {
for blog, bc := range a.cfg.Blogs {
for k, s := range bc.Sections {
s.Name = k
if err := a.addSection(blog, s); err != nil {
return err
}
}
}
return nil
}
func (a *goBlog) addSection(blog string, section *configSection) error {
_, err := a.db.exec(
"insert into sections (blog, name, title, description, pathtemplate, showfull) values (@blog, @name, @title, @description, @pathtemplate, @showfull)",
sql.Named("blog", blog),
sql.Named("name", section.Name),
sql.Named("title", section.Title),
sql.Named("description", section.Description),
sql.Named("pathtemplate", section.PathTemplate),
sql.Named("showfull", section.ShowFull),
)
return err
}
func (a *goBlog) deleteSection(blog string, name string) error {
_, err := a.db.exec("delete from sections where blog = @blog and name = @name", sql.Named("blog", blog), sql.Named("name", name))
return err
}
func (a *goBlog) updateSection(blog string, name string, section *configSection) error {
_, err := a.db.exec(
"update sections set title = @title, description = @description, pathtemplate = @pathtemplate, showfull = @showfull where blog = @blog and name = @name",
sql.Named("title", section.Title),
sql.Named("description", section.Description),
sql.Named("pathtemplate", section.PathTemplate),
sql.Named("showfull", section.ShowFull),
sql.Named("blog", blog),
sql.Named("name", section.Name),
)
return err
}

View File

@ -1,7 +1,6 @@
package main
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -10,14 +9,10 @@ import (
func Test_shortenPath(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
},
cfg: createDefaultTestConfig(t),
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
db := app.db
res1, err := db.shortenPath("/a")

View File

@ -16,9 +16,8 @@ func Test_sitemap(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
app.d = app.buildRouter()

View File

@ -43,6 +43,7 @@ noposts: "Hier sind keine Posts."
oldcontent: "⚠️ Dieser Eintrag ist bereits über ein Jahr alt. Er ist möglicherweise nicht mehr aktuell. Meinungen können sich geändert haben."
pinned: "Angepinnt"
posts: "Posts"
postsections: "Post-Bereiche"
prev: "Zurück"
privateposts: "Private Posts"
privatepostsdesc: "Posts mit dem Status `private`, die nur eingeloggt sichtbar sind."
@ -51,7 +52,13 @@ replyto: "Antwort an"
scheduledposts: "Geplante Posts"
scheduledpostsdesc: "Beiträge mit dem Status `scheduled`, die veröffentlicht werden, wenn das `published`-Datum erreicht ist."
search: "Suchen"
sectiondescription: "Beschreibung"
sectionname: "Name"
sectionpathtemplate: "Pfadvorlage"
sectionshowfull: "Vollständigen Inhalt in der Zusammenfassung anzeigen"
sectiontitle: "Title"
send: "Senden (zur Überprüfung)"
settings: "Einstellungen"
share: "Online teilen"
shorturl: "Kurz-Link:"
speak: "Vorlesen"

View File

@ -53,6 +53,7 @@ oldcontent: "⚠️ This entry is already over one year old. It may no longer be
password: "Password"
pinned: "Pinned"
posts: "Posts"
postsections: "Post sections"
prev: "Previous"
privateposts: "Private posts"
privatepostsdesc: "Posts with status `private` that are visible only when logged in."
@ -63,7 +64,13 @@ scheduledposts: "Scheduled posts"
scheduledpostsdesc: "Posts with status `scheduled` that are published when the `published` date is reached."
scopes: "Scopes"
search: "Search"
sectiondescription: "Description"
sectionname: "Name"
sectionpathtemplate: "Path template"
sectionshowfull: "Show full content in summary"
sectiontitle: "Title"
send: "Send (to review)"
settings: "Settings"
share: "Share online"
shorturl: "Short link:"
speak: "Read aloud"

View File

@ -122,9 +122,7 @@ func Test_telegram(t *testing.T) {
cfg: cfg,
httpClient: fakeClient.Client,
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initMarkdown()
app.initTelegram()
@ -156,9 +154,7 @@ func Test_telegram(t *testing.T) {
httpClient: fakeClient.Client,
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initTelegram()

105
ui.go
View File

@ -125,6 +125,10 @@ func (a *goBlog) renderBase(hb *htmlBuilder, rd *renderData, title, main func(hb
hb.writeElementClose("a")
}
hb.write(" &bull; ")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/settings"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "settings"))
hb.writeElementClose("a")
hb.write(" &bull; ")
hb.writeElementOpen("a", "href", "/logout")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "logout"))
hb.writeElementClose("a")
@ -1480,3 +1484,104 @@ func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) {
},
)
}
type settingsRenderData struct {
blog string
sections []*configSection
}
func (a *goBlog) renderSettings(hb *htmlBuilder, rd *renderData) {
srd, ok := rd.Data.(*settingsRenderData)
if !ok {
return
}
a.renderBase(
hb, rd,
func(hb *htmlBuilder) {
a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "settings"))
},
func(hb *htmlBuilder) {
hb.writeElementOpen("main")
// Title
hb.writeElementOpen("h1")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "settings"))
hb.writeElementClose("h1")
// Post sections
hb.writeElementOpen("h2")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "postsections"))
hb.writeElementClose("h2")
for _, section := range srd.sections {
hb.writeElementOpen("details")
hb.writeElementOpen("summary")
hb.writeElementOpen("h3")
hb.writeEscaped(section.Name)
hb.writeElementClose("h3")
hb.writeElementClose("summary")
hb.writeElementOpen("form", "class", "fw p", "method", "post")
hb.writeElementOpen("input", "type", "hidden", "name", "sectionname", "value", section.Name)
// Title
hb.writeElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "", "value", section.Title)
// Description
hb.writeElementOpen(
"textarea",
"name", "sectiondescription",
"class", "monospace",
"placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiondescription"),
)
hb.writeEscaped(section.Description)
hb.writeElementClose("textarea")
// Path template
hb.writeElementOpen("input", "type", "text", "name", "sectionpathtemplate", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionpathtemplate"), "value", section.PathTemplate)
// Show full
hb.writeElementOpen("input", "type", "checkbox", "name", "sectionshowfull", "id", "showfull-"+section.Name, lo.If(section.ShowFull, "checked").Else(""), "")
hb.writeElementOpen("label", "for", "showfull-"+section.Name)
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionshowfull"))
hb.writeElementClose("label")
// Actions
hb.writeElementOpen("div", "class", "p")
// Update
hb.writeElementOpen(
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"),
"formaction", rd.Blog.getRelativePath(settingsPath+settingsUpdateSectionPath),
)
// Delete
hb.writeElementOpen(
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"),
"formaction", rd.Blog.getRelativePath(settingsPath+settingsDeleteSectionPath),
"class", "confirm", "data-confirmmessage", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "confirmdelete"),
)
hb.writeElementOpen("script", "src", a.assetFileName("js/formconfirm.js"), "defer", "")
hb.writeElementClose("script")
hb.writeElementClose("div")
hb.writeElementClose("form")
hb.writeElementClose("details")
}
// Create new section
hb.writeElementOpen("form", "class", "fw p", "method", "post")
// Name
hb.writeElementOpen("input", "type", "text", "name", "sectionname", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionname"), "required", "")
// Title
hb.writeElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "")
// Create button
hb.writeElementOpen("div")
hb.writeElementOpen(
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create"),
"formaction", rd.Blog.getRelativePath(settingsPath+settingsCreateSectionPath),
)
hb.writeElementClose("div")
hb.writeElementClose("form")
hb.writeElementClose("main")
},
)
}

View File

@ -20,9 +20,7 @@ func Test_renderPostTax(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
p := &post{
@ -48,9 +46,7 @@ func Test_renderOldContentWarning(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
p := &post{
@ -76,10 +72,10 @@ func Test_renderInteractions(t *testing.T) {
cfg: createDefaultTestConfig(t),
}
app.cfg.Server.PublicAddress = "https://example.com"
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
app.d = app.buildRouter()
err = app.createPost(&post{
@ -145,9 +141,8 @@ func Test_renderAuthor(t *testing.T) {
}
app.cfg.User.Picture = "https://example.com/picture.jpg"
app.cfg.User.Name = "John Doe"
_ = app.initConfig()
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
buf := &bytes.Buffer{}

View File

@ -3,7 +3,6 @@ package main
import (
"net/http"
"os"
"path/filepath"
"strings"
"testing"
@ -21,23 +20,16 @@ func Test_verifyMention(t *testing.T) {
app := &goBlog{
httpClient: mockClient.Client,
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.org",
},
},
cfg: createDefaultTestConfig(t),
d: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.Path[:len(r.URL.Path)-1], http.StatusFound)
}
}),
}
app.cfg.Server.PublicAddress = "https://example.org"
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
m := &mention{
@ -71,21 +63,14 @@ func Test_verifyMentionBidgy(t *testing.T) {
app := &goBlog{
httpClient: mockClient.Client,
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.org",
},
},
cfg: createDefaultTestConfig(t),
d: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do nothing
}),
}
app.cfg.Server.PublicAddress = "https://example.org"
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
m := &mention{
@ -115,21 +100,14 @@ func Test_verifyMentionColin(t *testing.T) {
app := &goBlog{
httpClient: mockClient.Client,
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://jlelse.blog",
},
},
cfg: createDefaultConfig(),
d: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do nothing
}),
}
app.cfg.Server.PublicAddress = "https://jlelse.blog"
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
m := &mention{

View File

@ -1,7 +1,6 @@
package main
import (
"path/filepath"
"testing"
"time"
@ -11,25 +10,16 @@ import (
func Test_webmentions(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.com",
},
Blogs: map[string]*configBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Server.PublicAddress = "https://example.com"
app.cfg.Blogs = map[string]*configBlog{
"en": {
Lang: "en",
},
},
DefaultBlog: "en",
User: &configUser{},
},
}
_ = app.initDatabase(false)
defer app.db.close()
_ = app.initConfig(false)
app.initComponents(false)
_ = app.db.insertWebmention(&mention{