pages -> controllers

This commit is contained in:
Simon Martens
2025-05-29 02:31:27 +02:00
parent 4a4505d042
commit 0e4e6d4337
22 changed files with 22 additions and 22 deletions

35
controllers/404.go Normal file
View File

@@ -0,0 +1,35 @@
package controllers
import (
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const URL_ERROR_404 = "/errors/404/"
const URL_ERROR_500 = "/errors/500/"
func init() {
rp := &ErrorPage{
StaticPage: pagemodels.StaticPage{
Name: URL_ERROR_404,
},
}
app.Register(rp)
}
type ErrorPage struct {
pagemodels.StaticPage
}
func (p *ErrorPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_ERROR_404, func(e *core.RequestEvent) error {
return engine.Response404(e, nil, nil)
})
router.GET(URL_ERROR_500, func(e *core.RequestEvent) error {
return engine.Response500(e, nil, nil)
})
return nil
}

225
controllers/almanach.go Normal file
View File

@@ -0,0 +1,225 @@
package controllers
import (
"sort"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_ALMANACH = "/almanach/{id}/"
TEMPLATE_ALMANACH = "/almanach/"
)
func init() {
rp := &AlmanachPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_REIHEN_NAME,
URL: URL_ALMANACH,
Template: TEMPLATE_ALMANACH,
Layout: templating.DEFAULT_LAYOUT_NAME,
},
}
app.Register(rp)
}
type AlmanachPage struct {
pagemodels.StaticPage
}
func (p *AlmanachPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(p.URL, p.GET(engine, app))
return nil
}
func (p *AlmanachPage) GET(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
data := make(map[string]any)
filters := NewBeitraegeFilterParameters(e)
result, err := NewAlmanachResult(app, id, filters)
if err != nil {
engine.Response404(e, err, nil)
}
data["result"] = result
data["filters"] = filters
abbrs, err := pagemodels.GetAbks(app)
if err == nil {
data["abbrs"] = abbrs
}
return engine.Response200(e, p.Template, data)
}
}
type AlmanachResult struct {
Entry *dbmodels.Entry
Places []*dbmodels.Place
Series []*dbmodels.Series
Contents []*dbmodels.Content
Agents map[string]*dbmodels.Agent // <- Key is agent id
EntriesSeries map[string]*dbmodels.REntriesSeries // <- Key is series id
EntriesAgents []*dbmodels.REntriesAgents
ContentsAgents map[string][]*dbmodels.RContentsAgents // <- Key is content id
Types []string
HasScans bool
}
func NewAlmanachResult(app core.App, id string, params BeitraegeFilterParameters) (*AlmanachResult, error) {
// INFO: what about sql.ErrNoRows?
// We don't get sql.ErrNoRows here, since dbx converts every empty slice or
// empty id to a WHERE 0=1 query, which will not error.
entry, err := dbmodels.Entries_MusenalmID(app, id)
if err != nil {
return nil, err
}
places, err := dbmodels.Places_IDs(app, datatypes.ToAny(entry.Places()))
if err != nil {
return nil, err
}
srelations, err := dbmodels.REntriesSeries_Entry(app, entry.Id)
if err != nil {
return nil, err
}
sids := []any{}
srelationsMap := map[string]*dbmodels.REntriesSeries{}
for _, r := range srelations {
sids = append(sids, r.Series())
srelationsMap[r.Series()] = r
}
series, err := dbmodels.Series_IDs(app, sids)
if err != nil {
return nil, err
}
contents, err := dbmodels.Contents_Entry(app, entry.Id)
if err != nil {
return nil, err
}
types := Types_Contents(contents)
hs := HasScans(contents)
if params.OnlyScans {
cscans := []*dbmodels.Content{}
for _, c := range contents {
if len(c.Scans()) > 0 {
cscans = append(cscans, c)
}
}
contents = cscans
}
if params.Type != "" {
cfiltered := []*dbmodels.Content{}
outer:
for _, c := range contents {
for _, t := range c.MusenalmType() {
if t == params.Type {
cfiltered = append(cfiltered, c)
continue outer
}
}
}
contents = cfiltered
}
dbmodels.Sort_Contents_Numbering(contents)
contentsagents, err := dbmodels.RContentsAgents_Contents(app, dbmodels.Ids(contents))
caids := []any{}
caMap := map[string][]*dbmodels.RContentsAgents{}
for _, r := range contentsagents {
caids = append(caids, r.Agent())
caMap[r.Content()] = append(caMap[r.Content()], r)
}
entriesagents, err := dbmodels.REntriesAgents_Entry(app, entry.Id)
if err != nil {
return nil, err
}
for _, r := range entriesagents {
caids = append(caids, r.Agent())
}
agents, err := dbmodels.Agents_IDs(app, caids)
if err != nil {
return nil, err
}
agentsMap := map[string]*dbmodels.Agent{}
for _, a := range agents {
agentsMap[a.Id] = a
}
ret := &AlmanachResult{
Entry: entry,
Places: places,
Series: series,
Contents: contents,
Agents: agentsMap,
EntriesSeries: srelationsMap,
EntriesAgents: entriesagents,
ContentsAgents: caMap,
Types: types,
HasScans: hs,
}
ret.Collections()
return ret, nil
}
func (r *AlmanachResult) Collections() {
ids := []int{}
collections := []*dbmodels.Content{}
for _, s := range r.Contents {
ids = append(ids, s.MusenalmID())
for _, t := range s.MusenalmType() {
if t == "Sammlung" {
collections = append(collections, s)
}
}
}
}
func Types_Contents(contents []*dbmodels.Content) []string {
types := map[string]bool{}
for _, c := range contents {
for _, t := range c.MusenalmType() {
types[t] = true
}
}
ret := make([]string, 0, len(types))
for t, _ := range types {
ret = append(ret, t)
}
sort.Strings(ret)
return ret
}
func HasScans(contents []*dbmodels.Content) bool {
for _, c := range contents {
if len(c.Scans()) > 0 {
return true
}
}
return false
}

View File

@@ -0,0 +1,59 @@
package controllers
import (
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_ALMANACH_EDIT = "edit/"
TEMPLATE_ALMANACH_EDIT = "/almanach/edit/"
)
func init() {
ep := &AlmanachEditPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_ALMANACH_EDIT_NAME,
URL: URL_ALMANACH_EDIT,
Template: TEMPLATE_ALMANACH_EDIT,
Layout: pagemodels.LAYOUT_LOGIN_PAGES,
},
}
app.Register(ep)
}
type AlmanachEditPage struct {
pagemodels.StaticPage
}
func (p *AlmanachEditPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
rg := router.Group(URL_ALMANACH)
rg.BindFunc(middleware.IsAdminOrEditor())
rg.GET(URL_ALMANACH_EDIT, p.GET(engine, app))
return nil
}
func (p *AlmanachEditPage) GET(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
data := make(map[string]any)
filters := NewBeitraegeFilterParameters(e)
result, err := NewAlmanachResult(app, id, filters)
if err != nil {
engine.Response404(e, err, nil)
}
data["result"] = result
data["filters"] = filters
abbrs, err := pagemodels.GetAbks(app)
if err == nil {
data["abbrs"] = abbrs
}
return engine.Response200(e, p.Template, data, p.Layout)
}
}

140
controllers/beitrag.go Normal file
View File

@@ -0,0 +1,140 @@
package controllers
import (
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_BEITRAG = "/beitrag/{id}"
TEMPLATE_BEITRAG = "/beitrag/"
)
func init() {
rp := &BeitragPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_BEITRAG_NAME,
URL: URL_BEITRAG,
Template: TEMPLATE_BEITRAG,
Layout: templating.DEFAULT_LAYOUT_NAME,
},
}
app.Register(rp)
}
type BeitragPage struct {
pagemodels.StaticPage
}
func (p *BeitragPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(p.URL, func(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
data := make(map[string]interface{})
result, err := NewBeitragResult(app, id)
if err != nil {
engine.Response404(e, err, nil)
}
data["result"] = result
abbrs, err := pagemodels.GetAbks(app)
if err == nil {
data["abbrs"] = abbrs
}
return engine.Response200(e, p.Template, data)
})
return nil
}
type BeitragResult struct {
Entry *dbmodels.Entry
Places []*dbmodels.Place
Series []*dbmodels.Series
Content *dbmodels.Content
Agents map[string]*dbmodels.Agent // <- Key is agent id
EntriesSeries map[string]*dbmodels.REntriesSeries // <- Key is series id
EntriesAgents []*dbmodels.REntriesAgents
ContentsAgents []*dbmodels.RContentsAgents // <- Key is content id
}
func NewBeitragResult(app core.App, id string) (*BeitragResult, error) {
content, err := dbmodels.Contents_MusenalmID(app, id)
if err != nil {
return nil, err
}
entry, err := dbmodels.Entries_ID(app, content.Entry())
if err != nil {
return nil, err
}
places, err := dbmodels.Places_IDs(app, datatypes.ToAny(entry.Places()))
if err != nil {
return nil, err
}
srelations, err := dbmodels.REntriesSeries_Entry(app, entry.Id)
if err != nil {
return nil, err
}
sids := []any{}
srelationsMap := make(map[string]*dbmodels.REntriesSeries)
for _, s := range srelations {
sids = append(sids, s.Series())
srelationsMap[s.Series()] = s
}
series, err := dbmodels.Series_IDs(app, sids)
if err != nil {
return nil, err
}
arelations, err := dbmodels.REntriesAgents_Entry(app, entry.Id)
if err != nil {
return nil, err
}
acrelations, err := dbmodels.RContentsAgents_Content(app, content.Id)
if err != nil {
return nil, err
}
aids := []any{}
arelationsMap := make(map[string]*dbmodels.REntriesAgents)
for _, r := range arelations {
aids = append(aids, r.Agent())
arelationsMap[r.Agent()] = r
}
for _, r := range acrelations {
aids = append(aids, r.Agent())
}
agents, err := dbmodels.Agents_IDs(app, aids)
if err != nil {
return nil, err
}
agentsMap := make(map[string]*dbmodels.Agent)
for _, a := range agents {
agentsMap[a.Id] = a
}
return &BeitragResult{
Entry: entry,
Places: places,
Series: series,
Content: content,
Agents: agentsMap,
EntriesSeries: srelationsMap,
EntriesAgents: arelations,
ContentsAgents: acrelations,
}, nil
}

10
controllers/contents.go Normal file
View File

@@ -0,0 +1,10 @@
package controllers
type ContentListFilterParams struct {
Agent string
Type string
Page int
OnlyScans bool
AlmNumber int
Entry string
}

62
controllers/index.go Normal file
View File

@@ -0,0 +1,62 @@
package controllers
import (
"time"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
"math/rand"
)
func init() {
ip := &IndexPage{
DefaultPage: pagemodels.DefaultPage[*pagemodels.IndexTexte]{
Name: pagemodels.P_INDEX_NAME,
},
}
app.Register(ip)
}
type IndexPage struct {
pagemodels.DefaultPage[*pagemodels.IndexTexte]
}
// TODO:
func (p *IndexPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET("/{$}", func(e *core.RequestEvent) error {
bilder := []*pagemodels.IndexBilder{}
err := app.RecordQuery(pagemodels.GeneratePageTableName(pagemodels.P_INDEX_NAME, pagemodels.T_INDEX_BILDER)).
All(&bilder)
if err != nil {
return engine.Response404(e, err, nil)
}
texte := []*pagemodels.IndexTexte{}
err = app.RecordQuery(pagemodels.GeneratePageTableName(pagemodels.P_INDEX_NAME)).
All(&texte)
if err != nil {
return engine.Response404(e, err, nil)
}
Shuffle(bilder)
data := map[string]interface{}{
"bilder": bilder,
"texte": texte[0],
}
return engine.Response200(e, "/", data, "blank")
})
return nil
}
func Shuffle[T any](arr []T) {
rand.Seed(time.Now().UnixNano()) // Ensure random seed
n := len(arr)
for i := n - 1; i > 0; i-- {
j := rand.Intn(i + 1) // Get a random index
arr[i], arr[j] = arr[j], arr[i] // Swap
}
}

171
controllers/login.go Normal file
View File

@@ -0,0 +1,171 @@
package controllers
import (
"fmt"
"net/http"
"time"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/security"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_LOGIN = "/login/"
TEMPLATE_LOGIN = "/login/"
)
var CSRF_CACHE *security.CSRFProtector
// TODO:
// - rate limiting
func init() {
csrf_cache, err := security.NewCSRFProtector(time.Minute*10, time.Minute)
if err != nil {
panic(err)
}
CSRF_CACHE = csrf_cache
lp := &LoginPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_LOGIN_NAME,
Layout: "blank",
Template: TEMPLATE_LOGIN,
URL: URL_LOGIN,
},
}
app.Register(lp)
}
type LoginPage struct {
pagemodels.StaticPage
}
func (p *LoginPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_LOGIN, p.GET(engine, app))
router.POST(URL_LOGIN, p.POST(engine, app))
return nil
}
func (p *LoginPage) GET(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
data := make(map[string]any)
data["record"] = p
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
if err != nil {
return engine.Response500(e, err, data)
}
data["csrf_nonce"] = nonce
data["csrf_token"] = token
Logout(e, &app)
SetRedirect(data, e)
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func Unauthorized(
engine *templating.Engine,
e *core.RequestEvent,
error error,
data map[string]any) error {
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
if err != nil {
return engine.Response500(e, err, data)
}
data["csrf_nonce"] = nonce
data["csrf_token"] = token
data["error"] = error.Error()
SetRedirect(data, e)
htm, err := engine.RenderToString(e, data, TEMPLATE_LOGIN, "blank")
if err != nil {
return engine.Response500(e, err, data)
}
return e.HTML(http.StatusUnauthorized, htm)
}
func (p *LoginPage) POST(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
data := make(map[string]any)
data["record"] = p
formdata := struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
CsrfNonce string `json:"csrf_nonce" form:"csrf_nonce"`
CsrfToken string `json:"csrf_token" form:"csrf_token"`
Persistent string `json:"persist" form:"persist"`
}{}
if err := e.BindBody(&formdata); err != nil {
return engine.Response500(e, err, data)
}
data["formdata"] = formdata
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.CsrfNonce, formdata.CsrfToken); err != nil {
return Unauthorized(engine, e, fmt.Errorf("Ungültiges CSRF-Token oder Zeit abgelaufen. Bitte versuchen Sie es erneut."), data)
}
if formdata.Username == "" || formdata.Password == "" {
return Unauthorized(engine, e, fmt.Errorf("Benuztername oder Passwort falsch. Bitte versuchen Sie es erneut."), data)
}
record, err := app.FindFirstRecordByData(dbmodels.USERS_TABLE, dbmodels.USERS_EMAIL_FIELD, formdata.Username)
if err != nil || !record.ValidatePassword(formdata.Password) {
return Unauthorized(engine, e, fmt.Errorf("Benuztername oder Passwort falsch. Bitte versuchen Sie es erneut."), data)
}
user := dbmodels.NewUser(record)
if user.Deactivated() {
return Unauthorized(engine, e, fmt.Errorf("Ihr Benutzerkonto ist deaktiviert. Bitte kontaktieren Sie den Administrator."), data)
}
duration := time.Minute * 60
if formdata.Persistent == "on" {
duration = time.Hour * 24 * 90
}
token, err := dbmodels.CreateSessionToken(app, record.Id, e.RealIP(), e.Request.UserAgent(), formdata.Persistent == "on", duration)
if err != nil {
return engine.Response500(e, err, data)
}
if formdata.Persistent == "on" {
e.SetCookie(&http.Cookie{
Name: dbmodels.SESSION_COOKIE_NAME,
Path: "/",
MaxAge: int(duration.Seconds()),
Value: token.Token(),
SameSite: http.SameSiteLaxMode,
HttpOnly: true,
Secure: true,
})
} else {
e.SetCookie(&http.Cookie{
Name: dbmodels.SESSION_COOKIE_NAME,
Path: "/",
Value: token.Token(),
SameSite: http.SameSiteLaxMode,
HttpOnly: true,
Secure: true,
})
}
SetRedirect(data, e)
return RedirectTo(e)
}
}

73
controllers/logout.go Normal file
View File

@@ -0,0 +1,73 @@
package controllers
import (
"net/http"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
func init() {
lp := &LogoutPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_LOGOUT_NAME,
Layout: "blank",
Template: "/logout/",
URL: "/logout/",
},
}
app.Register(lp)
}
type LogoutPage struct {
pagemodels.StaticPage
}
func (p *LogoutPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(p.URL, p.GET(app))
return nil
}
func (p *LogoutPage) GET(app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
Logout(e, &app)
return RedirectTo(e)
}
}
func Logout(e *core.RequestEvent, app *core.App) {
e.SetCookie(&http.Cookie{
Name: dbmodels.SESSION_COOKIE_NAME,
Path: "/",
MaxAge: -1,
})
e.Response.Header().Set("Clear-Site-Data", "\"cookies\"")
cookie, err := e.Request.Cookie(dbmodels.SESSION_COOKIE_NAME)
if err == nil && app != nil {
go func() {
app := *app
record, err := app.FindFirstRecordByData(dbmodels.SESSIONS_TABLE, dbmodels.SESSIONS_TOKEN_FIELD, cookie.Value)
if err == nil && record != nil {
app.Delete(record)
}
middleware.SESSION_CACHE.Delete(cookie.Value)
}()
}
}
func RedirectTo(e *core.RequestEvent) error {
redirect := "/reihen"
if r := e.Request.URL.Query().Get("redirectTo"); r != "" {
redirect = r
}
return e.Redirect(303, redirect)
}

242
controllers/person.go Normal file
View File

@@ -0,0 +1,242 @@
package controllers
import (
"database/sql"
"maps"
"slices"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_PERSON = "/person/{id}"
TEMPLATE_PERSON = "/person/"
)
func init() {
rp := &PersonPage{
StaticPage: pagemodels.StaticPage{
Name: URL_PERSON,
Template: TEMPLATE_PERSON,
Layout: templating.DEFAULT_LAYOUT_NAME,
URL: URL_PERSON,
},
}
app.Register(rp)
}
type PersonPage struct {
pagemodels.StaticPage
}
func (p *PersonPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_PERSON, func(e *core.RequestEvent) error {
person := e.Request.PathValue("id")
data := make(map[string]interface{})
data[PARAM_PERSON] = person
result, err := NewAgentResult(app, person)
if err != nil {
return engine.Response404(e, err, data)
}
data["result"] = result
return engine.Response200(e, p.Template, data, p.Layout)
})
return nil
}
type AgentResult struct {
Agent *dbmodels.Agent
BResult []*dbmodels.Series // Sorted
Entries map[string]*dbmodels.Entry // KEY: Entry ID
EntriesSeries map[string][]*dbmodels.REntriesSeries // KEY: Series ID
EntriesAgents map[string][]*dbmodels.REntriesAgents // KEY: Entry ID
// INFO: we could save a DB query by quering the entries table only once
CResult []*dbmodels.Entry /// Sorted
Contents map[string][]*dbmodels.Content // KEY: entry ID
ContentsAgents map[string][]*dbmodels.RContentsAgents // KEY: Content ID
Agents map[string]*dbmodels.Agent // KEY: Agent ID
}
func NewAgentResult(app core.App, id string) (*AgentResult, error) {
agent, err := dbmodels.Agents_ID(app, id)
if err != nil {
return nil, err
}
res := &AgentResult{
Agent: agent,
}
err = res.FilterEntriesByPerson(app, id, res)
if err != nil {
return nil, err
}
err = res.FilterContentsByEntry(app, id, res)
if err != nil {
return nil, err
}
return res, nil
}
func (p *AgentResult) FilterEntriesByPerson(app core.App, id string, res *AgentResult) error {
// 1. DB Hit
relations, err := dbmodels.REntriesAgents_Agent(app, id)
if err != nil && err != sql.ErrNoRows {
return err
}
if len(relations) == 0 {
return nil
}
entriesagents := make(map[string][]*dbmodels.REntriesAgents)
entryIds := []any{}
for _, r := range relations {
entryIds = append(entryIds, r.Entry())
entriesagents[r.Entry()] = append(entriesagents[r.Entry()], r)
}
res.EntriesAgents = entriesagents
// 2. DB Hit
entries, err := dbmodels.Entries_IDs(app, entryIds)
if err != nil {
return err
}
entryMap := make(map[string]*dbmodels.Entry, len(entries))
for _, e := range entries {
entryMap[e.Id] = e
}
res.Entries = entryMap
// 3. DB Hit
entriesseries, err := dbmodels.REntriesSeries_Entries(app, entryIds)
if err != nil {
return err
}
entriesseriesmap := make(map[string][]*dbmodels.REntriesSeries, len(entriesseries))
for _, r := range entriesseries {
entriesseriesmap[r.Series()] = append(entriesseriesmap[r.Series()], r)
}
for _, r := range entriesseriesmap {
dbmodels.Sort_REntriesSeries_Year(r, entryMap)
}
res.EntriesSeries = entriesseriesmap
seriesIds := []any{}
for _, s := range entriesseries {
seriesIds = append(seriesIds, s.Series())
}
// 4. DB Hit
series, err := dbmodels.Series_IDs(app, seriesIds)
if err != nil {
return err
}
res.BResult = series
return nil
}
func (p *AgentResult) FilterContentsByEntry(app core.App, id string, res *AgentResult) error {
relations, err := dbmodels.RContentsAgents_Agent(app, id)
if err != nil {
return err
}
if len(relations) == 0 {
return nil
}
contentsagents := make(map[string][]*dbmodels.RContentsAgents)
contentIds := []any{}
agentids := []any{}
for _, r := range relations {
contentIds = append(contentIds, r.Content())
agentids = append(agentids, r.Agent())
contentsagents[r.Content()] = append(contentsagents[r.Content()], r)
}
res.ContentsAgents = contentsagents
agents, err := dbmodels.Agents_IDs(app, agentids)
if err != nil {
return err
}
aMap := make(map[string]*dbmodels.Agent, len(agents))
for _, a := range agents {
aMap[a.Id] = a
}
res.Agents = aMap
contents, err := dbmodels.Contents_IDs(app, contentIds)
if err != nil {
return err
}
contentMap := make(map[string][]*dbmodels.Content, len(contents))
entrykeys := []any{}
for _, c := range contents {
contentMap[c.Entry()] = append(contentMap[c.Entry()], c)
entrykeys = append(entrykeys, c.Entry())
}
res.Contents = contentMap
for _, c := range contentMap {
dbmodels.Sort_Contents_Numbering(c)
}
entries, err := dbmodels.Entries_IDs(app, entrykeys)
if err != nil {
return err
}
dbmodels.Sort_Entries_Year_Title(entries)
res.CResult = entries
return nil
}
func (p *AgentResult) LenEntries() int {
return len(p.Entries)
}
func (p *AgentResult) LenSeries() int {
return len(p.BResult)
}
func (p *AgentResult) LenContents() int {
i := 0
for _, c := range p.Contents {
i += len(c)
}
return i
}
func (p *AgentResult) Types() []string {
types := make(map[string]bool)
// INFO: this is just a handful of entries usuallly so we're fine
for _, c := range p.Contents {
for _, c := range c {
for _, c := range c.MusenalmType() {
types[c] = true
}
}
}
return slices.Collect(maps.Keys(types))
}

180
controllers/personen.go Normal file
View File

@@ -0,0 +1,180 @@
package controllers
import (
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
// INFO: V0 of these
const (
URL_PERSONEN = "/personen/"
PARAM_FILTER = "filter"
)
func init() {
rp := &PersonenPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_REIHEN_NAME,
},
}
app.Register(rp)
}
type PersonenPage struct {
pagemodels.StaticPage
}
func (p *PersonenPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_PERSONEN, func(e *core.RequestEvent) error {
if e.Request.URL.Query().Get(PARAM_SEARCH) != "" {
return p.SearchRequest(app, engine, e)
}
return p.FilterRequest(app, engine, e)
})
return nil
}
func (p *PersonenPage) CommonData(app core.App, data map[string]interface{}) error {
return nil
}
func (p *PersonenPage) FilterRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
filter := e.Request.URL.Query().Get(PARAM_FILTER)
letter := e.Request.URL.Query().Get(PARAM_LETTER)
if letter == "" {
letter = "A"
}
if filter == "" {
filter = "noorg"
}
data := map[string]interface{}{}
var err error = nil
agents := []*dbmodels.Agent{}
if filter == "org" {
agents, err = dbmodels.AgentsForOrg(app, true, letter)
}
if filter == "noorg" {
agents, err = dbmodels.AgentsForOrg(app, false, letter)
}
if filter == "musik" {
agents, err = dbmodels.AgentsForProfession(app, "Musik", letter)
}
if filter == "text" {
agents, err = dbmodels.AgentsForProfession(app, "Text", letter)
}
if filter == "graphik" {
agents, err = dbmodels.AgentsForProfession(app, "Graphik", letter)
}
if filter == "hrsg" {
agents, err = dbmodels.AgentsForProfession(app, "Hrsg", letter)
}
if err != nil {
return engine.Response404(e, err, data)
}
dbmodels.Sort_Agents_Name(agents)
data["agents"] = agents
data["filter"] = filter
data["letter"] = letter
ids := []any{}
for _, a := range agents {
ids = append(ids, a.Id)
}
bcount, err := dbmodels.CountAgentsBaende(app, ids)
if err == nil {
data["bcount"] = bcount
}
count, err := dbmodels.CountAgentsContents(app, ids)
if err == nil {
data["ccount"] = count
}
letters, err := dbmodels.LettersForAgents(app, filter)
if err != nil {
return engine.Response404(e, err, data)
}
data["letters"] = letters
return p.Get(e, engine, data)
}
func (p *PersonenPage) SearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
search := e.Request.URL.Query().Get(PARAM_SEARCH)
data := map[string]interface{}{}
agents := []*dbmodels.Agent{}
altagents := []*dbmodels.Agent{}
a, err := dbmodels.FTS5SearchAgents(app, search)
if err != nil {
return engine.Response404(e, err, data)
}
agents = a
if len(agents) == 0 {
// INFO: Fallback to regular search, if FTS5 fails
a, aa, err := dbmodels.BasicSearchAgents(app, search)
if err != nil {
return engine.Response404(e, err, data)
}
agents = a
altagents = aa
} else {
data["FTS"] = true
}
dbmodels.Sort_Agents_Name(agents)
dbmodels.Sort_Agents_Name(altagents)
data["search"] = search
data["agents"] = agents
data["altagents"] = altagents
ids := []any{}
for _, a := range agents {
ids = append(ids, a.Id)
}
for _, a := range altagents {
ids = append(ids, a.Id)
}
bcount, err := dbmodels.CountAgentsBaende(app, ids)
if err == nil {
data["bcount"] = bcount
}
count, err := dbmodels.CountAgentsContents(app, ids)
if err == nil {
data["ccount"] = count
}
return p.Get(e, engine, data)
}
func (p *PersonenPage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
err := p.CommonData(request.App, data)
if err != nil {
return engine.Response404(request, err, data)
}
return engine.Response200(request, URL_PERSONEN, data)
}

70
controllers/reihe.go Normal file
View File

@@ -0,0 +1,70 @@
package controllers
import (
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_REIHE = "/reihe/{id}/"
TEMPLATE_REIHE = "/reihe/"
)
func init() {
rp := &ReihePage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_REIHEN_NAME,
URL: URL_REIHE,
Layout: templating.DEFAULT_LAYOUT_NAME,
Template: TEMPLATE_REIHE,
},
}
app.Register(rp)
}
type ReihePage struct {
pagemodels.StaticPage
}
// TODO: data richtig seutzen, damit die Reihe mit dem template _reihe angezeigt wird
func (p *ReihePage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_REIHE, func(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
data := make(map[string]interface{})
reihe, err := dbmodels.Series_MusenalmID(app, id)
if err != nil || reihe == nil || reihe.Id == "" {
return engine.Response404(e, err, data)
}
data["series"] = reihe
entries, relations, err := Entries_Series_IDs(app, []any{reihe.Id})
if err != nil {
return engine.Response404(e, err, data)
}
emap := make(map[string]*dbmodels.Entry)
for _, entry := range entries {
emap[entry.Id] = entry
}
rmap := make(map[string][]*dbmodels.REntriesSeries)
for _, relation := range relations {
rmap[relation.Series()] = append(rmap[relation.Series()], relation)
}
data["relations"] = rmap[reihe.Id]
data["entries"] = emap
return p.Get(e, engine, data)
})
return nil
}
func (p *ReihePage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
return engine.Response200(request, TEMPLATE_REIHE, data)
}

569
controllers/reihen.go Normal file
View File

@@ -0,0 +1,569 @@
package controllers
import (
"strconv"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_REIHEN = "/reihen/"
PARAM_LETTER = "letter"
PARAM_SEARCH = "search"
PARAM_PERSON = "agent"
PARAM_PLACE = "place"
PARAM_YEAR = "year"
PARAM_HIDDEN = "hidden"
)
func init() {
rp := &ReihenPage{
DefaultPage: pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
Name: pagemodels.P_REIHEN_NAME,
URL: URL_REIHEN,
Template: URL_REIHEN,
Layout: templating.DEFAULT_LAYOUT_NAME,
Record: &pagemodels.DefaultPageRecord{},
},
}
app.Register(rp)
}
type ReihenPage struct {
pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]
}
func (p *ReihenPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_REIHEN, func(e *core.RequestEvent) error {
search := e.Request.URL.Query().Get(PARAM_SEARCH)
if search != "" {
return p.SearchRequest(app, engine, e)
}
person := e.Request.URL.Query().Get(PARAM_PERSON)
if person != "" {
return p.PersonRequest(app, engine, e)
}
place := e.Request.URL.Query().Get(PARAM_PLACE)
if place != "" {
return p.PlaceRequest(app, engine, e)
}
year := e.Request.URL.Query().Get(PARAM_YEAR)
if year != "" {
return p.YearRequest(app, engine, e)
}
return p.LetterRequest(app, engine, e)
})
return nil
}
func (p *ReihenPage) YearRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
year := e.Request.URL.Query().Get(PARAM_YEAR)
data := make(map[string]any)
data[PARAM_HIDDEN] = e.Request.URL.Query().Get(PARAM_HIDDEN)
data[PARAM_YEAR] = year
y, err := strconv.Atoi(year)
if err != nil {
return engine.Response404(e, err, data)
}
result, err := NewSeriesResult_Year(app, y)
if err != nil {
return engine.Response404(e, err, data)
}
data["result"] = result
return p.Get(e, engine, data)
}
func (p *ReihenPage) LetterRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
letter := e.Request.URL.Query().Get(PARAM_LETTER)
data := map[string]interface{}{}
data[PARAM_HIDDEN] = e.Request.URL.Query().Get(PARAM_HIDDEN)
if letter == "" {
data["startpage"] = true
letter = "A"
}
data[PARAM_LETTER] = letter
result, err := NewSeriesListResult_Letter(app, letter)
if err != nil {
return engine.Response404(e, err, data)
}
data["result"] = result
return p.Get(e, engine, data)
}
func (p *ReihenPage) PersonRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
person := e.Request.URL.Query().Get(PARAM_PERSON)
data := map[string]interface{}{}
data[PARAM_PERSON] = person
data[PARAM_HIDDEN] = e.Request.URL.Query().Get(PARAM_HIDDEN)
result, err := NewSeriesResult_Agent(app, person)
if err != nil {
return engine.Response404(e, err, data)
}
data["result"] = result
return p.Get(e, engine, data)
}
func (p *ReihenPage) PlaceRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
place := e.Request.URL.Query().Get(PARAM_PLACE)
data := map[string]interface{}{}
data[PARAM_PLACE] = place
data[PARAM_HIDDEN] = e.Request.URL.Query().Get(PARAM_HIDDEN)
result, err := NewSeriesResult_Place(app, place)
if err != nil {
return engine.Response404(e, err, data)
}
data["result"] = result
return p.Get(e, engine, data)
}
// TODO: Suchverhalten bei gefilterten Personen, Orten und Jahren
func (p *ReihenPage) SearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
search := e.Request.URL.Query().Get(PARAM_SEARCH)
data := map[string]interface{}{}
data[PARAM_SEARCH] = search
result, err := NewSeriesResult_Search(app, search)
if err != nil {
return engine.Response404(e, err, data)
}
data["result"] = result
return p.Get(e, engine, data)
}
func (p *ReihenPage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
data["common"] = NewCommonReihenData(request.App)
record, _ := p.GetLatestData(request.App)
if record != nil {
data["record"] = pagemodels.NewReihen(record)
}
return engine.Response200(request, URL_REIHEN, data)
}
type CommonReihenData struct {
Years []int
Places []*dbmodels.Place
Letters []string
Agents []*dbmodels.Agent
}
func NewCommonReihenData(app core.App) CommonReihenData {
arels := []*core.Record{}
err := app.RecordQuery(
dbmodels.RelationTableName(dbmodels.ENTRIES_TABLE, dbmodels.AGENTS_TABLE)).
GroupBy(dbmodels.AGENTS_TABLE).
All(&arels)
if err != nil {
app.Logger().Error("Failed to fetch agents", "error", err)
}
ids := []any{}
for _, a := range arels {
ids = append(ids, a.GetString(dbmodels.AGENTS_TABLE))
}
agents, err := dbmodels.Agents_IDs(app, ids)
if err != nil {
app.Logger().Error("Failed to fetch agents", "error", err)
}
letterrecs := []core.Record{}
letters := []string{}
err = app.RecordQuery(dbmodels.SERIES_TABLE).
Select("upper(substr(" + dbmodels.SERIES_TITLE_FIELD + ", 1, 1)) AS id").
Distinct(true).
OrderBy("id").
All(&letterrecs)
if err != nil {
app.Logger().Error("Failed to fetch letters", "error", err)
}
for _, l := range letterrecs {
letters = append(letters, l.GetString("id"))
}
places := []*dbmodels.Place{}
err = app.RecordQuery(dbmodels.PLACES_TABLE).
OrderBy(dbmodels.PLACES_NAME_FIELD).
All(&places)
if err != nil {
app.Logger().Error("Failed to fetch places", "error", err)
}
dbmodels.Sort_Places_Name(places)
rec := []core.Record{}
err = app.RecordQuery(dbmodels.ENTRIES_TABLE).
Select(dbmodels.YEAR_FIELD + " AS id").
Distinct(true).
OrderBy("id").
All(&rec)
if err != nil {
app.Logger().Error("Failed to fetch years", "error", err)
}
years := []int{}
for _, r := range rec {
years = append(years, r.GetInt("id"))
}
return CommonReihenData{
Years: years,
Places: places,
Letters: letters,
Agents: agents,
}
}
type SeriesListResult struct {
Series []*dbmodels.Series
Entries map[string]*dbmodels.Entry // <-- Key is Entry.ID
EntriesSeries map[string][]*dbmodels.REntriesSeries // <-- Key is Series.ID
// INFO: Only on agent request
Agent *dbmodels.Agent
EntriesAgents map[string][]*dbmodels.REntriesAgents // <-- Key is Entry.ID
// INFO: Only on search request
IDSeries []*dbmodels.Series
AltSeries []*dbmodels.Series
// INFO: Only on place request
Place *dbmodels.Place
}
func NewSeriesListResult_Letter(app core.App, letter string) (*SeriesListResult, error) {
series := []*dbmodels.Series{}
err := app.RecordQuery(dbmodels.SERIES_TABLE).
Where(dbx.Like(dbmodels.SERIES_TITLE_FIELD, letter).Match(false, true)).
OrderBy(dbmodels.SERIES_TITLE_FIELD).
All(&series)
if err != nil {
return nil, err
}
dbmodels.Sort_Series_Title(series)
relations, err := dbmodels.REntriesSeries_Seriess(app, dbmodels.Ids(series))
if err != nil {
return nil, err
}
eids := []any{}
relationsMap := map[string][]*dbmodels.REntriesSeries{}
for _, r := range relations {
relationsMap[r.Series()] = append(relationsMap[r.Series()], r)
eids = append(eids, r.Entry())
}
entries, err := dbmodels.Entries_IDs(app, eids)
if err != nil {
return nil, err
}
entriesMap := map[string]*dbmodels.Entry{}
for _, e := range entries {
entriesMap[e.Id] = e
}
for _, r := range relationsMap {
dbmodels.Sort_REntriesSeries_Year(r, entriesMap)
}
return &SeriesListResult{
Series: series,
Entries: entriesMap,
EntriesSeries: relationsMap,
}, nil
}
func NewSeriesResult_Agent(app core.App, person string) (*SeriesListResult, error) {
agent, err := dbmodels.Agents_ID(app, person)
if err != nil {
return nil, err
}
entriesagentsrels, err := dbmodels.REntriesAgents_Agent(app, agent.Id)
if err != nil {
return nil, err
}
eids := []any{}
entriesagents := map[string][]*dbmodels.REntriesAgents{}
for _, r := range entriesagentsrels {
eids = append(eids, r.Entry())
entriesagents[r.Entry()] = append(entriesagents[r.Entry()], r)
}
entries, err := dbmodels.Entries_IDs(app, eids)
if err != nil {
return nil, err
}
entriesMap := map[string]*dbmodels.Entry{}
for _, e := range entries {
entriesMap[e.Id] = e
}
entriesseriesrels, err := dbmodels.REntriesSeries_Entries(app, eids)
if err != nil {
return nil, err
}
sids := []any{}
entriesseries := map[string][]*dbmodels.REntriesSeries{}
for _, r := range entriesseriesrels {
sids = append(sids, r.Series())
entriesseries[r.Series()] = append(entriesseries[r.Series()], r)
}
series, err := dbmodels.Series_IDs(app, sids)
if err != nil {
return nil, err
}
dbmodels.Sort_Series_Title(series)
return &SeriesListResult{
Series: series,
Entries: entriesMap,
EntriesSeries: entriesseries,
Agent: agent,
EntriesAgents: entriesagents,
}, nil
}
func NewSeriesResult_Year(app core.App, year int) (*SeriesListResult, error) {
entries := []*dbmodels.Entry{}
err := app.RecordQuery(dbmodels.ENTRIES_TABLE).
Where(dbx.HashExp{dbmodels.YEAR_FIELD: year}).
All(&entries)
if err != nil {
return nil, err
}
entriesMap := map[string]*dbmodels.Entry{}
eids := []any{}
for _, e := range entries {
eids = append(eids, e.Id)
entriesMap[e.Id] = e
}
entriesseriesrels, err := dbmodels.REntriesSeries_Entries(app, eids)
if err != nil {
return nil, err
}
sids := []any{}
entriesseries := map[string][]*dbmodels.REntriesSeries{}
for _, r := range entriesseriesrels {
sids = append(sids, r.Series())
entriesseries[r.Series()] = append(entriesseries[r.Series()], r)
}
series, err := dbmodels.Series_IDs(app, sids)
if err != nil {
return nil, err
}
dbmodels.Sort_Series_Title(series)
return &SeriesListResult{
Series: series,
Entries: entriesMap,
EntriesSeries: entriesseries,
}, nil
}
func NewSeriesResult_Place(app core.App, place string) (*SeriesListResult, error) {
p, err := dbmodels.Places_ID(app, place)
if err != nil {
return nil, err
}
entries := []*dbmodels.Entry{}
err = app.RecordQuery(dbmodels.ENTRIES_TABLE).
Where(dbx.Like(dbmodels.PLACES_TABLE, place).Match(true, true)).
All(&entries)
if err != nil {
return nil, err
}
entriesMap := map[string]*dbmodels.Entry{}
eids := []any{}
for _, e := range entries {
eids = append(eids, e.Id)
entriesMap[e.Id] = e
}
entriesseriesrels, err := dbmodels.REntriesSeries_Entries(app, eids)
if err != nil {
return nil, err
}
sids := []any{}
entriesseries := map[string][]*dbmodels.REntriesSeries{}
for _, r := range entriesseriesrels {
sids = append(sids, r.Series())
entriesseries[r.Series()] = append(entriesseries[r.Series()], r)
}
series, err := dbmodels.Series_IDs(app, sids)
if err != nil {
return nil, err
}
dbmodels.Sort_Series_Title(series)
return &SeriesListResult{
Series: series,
Entries: entriesMap,
EntriesSeries: entriesseries,
Place: p,
}, nil
}
func NewSeriesResult_Search(app core.App, search string) (*SeriesListResult, error) {
series, altseries, err := dbmodels.BasicSearchSeries(app, search)
if err != nil {
return nil, err
}
dbmodels.Sort_Series_Title(series)
dbmodels.Sort_Series_Title(altseries)
keys := []any{}
keys = append(keys, dbmodels.Ids(series)...)
keys = append(keys, dbmodels.Ids(altseries)...)
entries, relations, err := Entries_Series_IDs(app, keys)
if err != nil {
return nil, err
}
relationsMap := make(map[string][]*dbmodels.REntriesSeries)
entriesMap := make(map[string]*dbmodels.Entry)
for _, v := range relations {
relationsMap[v.Series()] = append(relationsMap[v.Series()], v)
}
for _, v := range entries {
entriesMap[v.Id] = v
}
ret := &SeriesListResult{
Series: series,
AltSeries: altseries,
Entries: entriesMap,
EntriesSeries: relationsMap,
}
if _, err := strconv.Atoi(strings.TrimSpace(search)); err == nil {
identries := []*dbmodels.Entry{}
err := app.RecordQuery(dbmodels.ENTRIES_TABLE).
Where(dbx.HashExp{dbmodels.MUSENALMID_FIELD: search}).
All(&identries)
if err != nil {
return nil, err
}
if len(identries) != 0 {
app.Logger().Info("Found entries by musenalmid", "count", len(identries))
idseries, idrelations, err := Series_Entries(app, identries)
if err != nil {
return nil, err
}
dbmodels.Sort_Series_Title(idseries)
ret.IDSeries = idseries
for _, v := range idrelations {
ret.EntriesSeries[v.Series()] = append(relationsMap[v.Series()], v)
}
for _, v := range identries {
ret.Entries[v.Id] = v
}
}
}
return ret, nil
}
func (r *SeriesListResult) Count() int {
return len(r.Series) + len(r.AltSeries) + len(r.IDSeries)
}
func Entries_Series(app core.App, series []*dbmodels.Series) ([]*dbmodels.Entry, []*dbmodels.REntriesSeries, error) {
relations, err := dbmodels.REntriesSeries_Seriess(app, dbmodels.Ids(series))
if err != nil {
return nil, nil, err
}
eids := []any{}
for _, r := range relations {
eids = append(eids, r.Entry())
}
entries, err := dbmodels.Entries_IDs(app, eids)
if err != nil {
return nil, nil, err
}
return entries, relations, nil
}
func Entries_Series_IDs(app core.App, ids []any) ([]*dbmodels.Entry, []*dbmodels.REntriesSeries, error) {
relations, err := dbmodels.REntriesSeries_Seriess(app, ids)
if err != nil {
return nil, nil, err
}
eids := []any{}
for _, r := range relations {
eids = append(eids, r.Entry())
}
entries, err := dbmodels.Entries_IDs(app, eids)
if err != nil {
return nil, nil, err
}
return entries, relations, nil
}
func Series_Entries(app core.App, entries []*dbmodels.Entry) ([]*dbmodels.Series, []*dbmodels.REntriesSeries, error) {
relations, err := dbmodels.REntriesSeries_Entries(app, dbmodels.Ids(entries))
if err != nil {
return nil, nil, err
}
sids := []any{}
for _, r := range relations {
sids = append(sids, r.Series())
}
series, err := dbmodels.Series_IDs(app, sids)
if err != nil {
return nil, nil, err
}
return series, relations, nil
}

64
controllers/static.go Normal file
View File

@@ -0,0 +1,64 @@
package controllers
import (
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
)
type HandleFunc func(e *core.RequestEvent) error
func init() {
RegisterStaticPage("/datenschutz/", pagemodels.P_DATENSCHUTZ_NAME)
RegisterTextPage("/redaktion/kontakt/", pagemodels.P_KONTAKT_NAME)
RegisterTextPage("/redaktion/danksagungen/", pagemodels.P_DANK_NAME)
RegisterTextPage("/redaktion/literatur/", pagemodels.P_LIT_NAME)
RegisterTextPage("/redaktion/einleitung/", pagemodels.P_EINFUEHRUNG_NAME)
RegisterTextPage("/redaktion/benutzerhinweise/", pagemodels.P_DOK_NAME)
RegisterTextPage("/redaktion/lesekabinett/", pagemodels.P_KABINETT_NAME)
}
func RegisterStaticPage(url, name string, layout ...string) {
layoutName := templating.DEFAULT_LAYOUT_NAME
if len(layout) > 0 {
layoutName = layout[0]
}
app.Register(&pagemodels.StaticPage{
Name: name,
Layout: layoutName,
Template: url,
URL: url,
})
}
// TODO: mocve textpage to defaultpage with T = TextPageRecord
func RegisterTextPage(url, name string, layout ...string) {
layoutName := templating.DEFAULT_LAYOUT_NAME
if len(layout) > 0 {
layoutName = layout[0]
}
app.Register(&pagemodels.TextPage{
Name: name,
Layout: layoutName,
Template: url,
URL: url,
})
}
func RegisterDefaultPage(url string, name string, layout ...string) {
layoutName := templating.DEFAULT_LAYOUT_NAME
if len(layout) > 0 {
layoutName = layout[0]
}
app.Register(&pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
Name: name,
Layout: layoutName,
Template: url,
URL: url,
Record: &pagemodels.DefaultPageRecord{},
})
}

143
controllers/suche.go Normal file
View File

@@ -0,0 +1,143 @@
package controllers
import (
"fmt"
"net/http"
"slices"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
type SuchePage struct {
pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]
}
const (
URL_SUCHE = "/suche/{type}"
URL_SUCHE_ALT = "/suche/{$}"
DEFAULT_SUCHE = "/suche/baende"
PARAM_EXTENDED = "extended"
TEMPLATE_SUCHE = "/suche/"
PARAM_QUERY = "q"
PARAM_PLACEHOLDER = "p"
)
var availableTypes = []string{"baende", "beitraege"}
func init() {
rp := &SuchePage{
DefaultPage: pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
Record: &pagemodels.DefaultPageRecord{},
Name: pagemodels.P_SUCHE_NAME,
Template: TEMPLATE_SUCHE,
Layout: templating.DEFAULT_LAYOUT_NAME,
URL: URL_SUCHE,
},
}
app.Register(rp)
}
func (p *SuchePage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_SUCHE_ALT, func(e *core.RequestEvent) error {
return e.Redirect(http.StatusPermanentRedirect, DEFAULT_SUCHE)
})
router.GET(URL_SUCHE, func(e *core.RequestEvent) error {
paras, err := NewParameters(e)
if err != nil {
return engine.Response404(e, err, nil)
}
allparas, _ := NewSearchParameters(e, *paras)
if allparas.IsBaendeSearch() {
return p.SearchBaendeRequest(app, engine, e, *allparas)
}
if allparas.IsBeitraegeSearch() {
return p.SearchBeitraegeRequest(app, engine, e, *allparas)
}
data := make(map[string]any)
data["parameters"] = allparas
data["types"] = dbmodels.MUSENALM_TYPE_VALUES
return engine.Response200(e, p.Template+paras.Collection+"/", data, p.Layout)
})
return nil
}
func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
return engine.Response404(e, nil, nil)
}
func (p *SuchePage) SearchBeitraegeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, params SearchParameters) error {
data := make(map[string]any)
filterparams := NewBeitraegeFilterParameters(e)
result, err := NewSearchBeitraege(app, params, filterparams)
if err != nil {
return engine.Response404(e, err, nil)
}
data["filters"] = filterparams
data["parameters"] = params
data["result"] = result
data["types"] = dbmodels.MUSENALM_TYPE_VALUES
return engine.Response200(e, p.Template+params.Collection+"/", data, p.Layout)
}
func (p *SuchePage) SearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, params SearchParameters) error {
data := make(map[string]any)
result, err := NewSearchBaende(app, params)
if err != nil {
return engine.Response404(e, err, nil)
}
data["parameters"] = params
data["result"] = result
return engine.Response200(e, p.Template+params.Collection+"/", data, p.Layout)
}
var ErrInvalidCollectionType = fmt.Errorf("Invalid collection type")
type Parameters struct {
Extended bool
Collection string
Query string
Placeholder string
}
func NewParameters(e *core.RequestEvent) (*Parameters, error) {
collection := e.Request.PathValue("type")
if !slices.Contains(availableTypes, collection) {
return nil, ErrInvalidCollectionType
}
extended := false
if e.Request.URL.Query().Get(PARAM_EXTENDED) == "true" {
extended = true
}
query := e.Request.URL.Query().Get(PARAM_QUERY)
placeholder := query
if query == "" {
placeholder = e.Request.URL.Query().Get(PARAM_PLACEHOLDER)
}
return &Parameters{
Collection: collection,
Extended: extended,
Query: query,
Placeholder: placeholder,
}, nil
}
func (p *Parameters) NormalizeQuery() dbmodels.Query {
return dbmodels.NormalizeQuery(p.Query)
}

230
controllers/suche_baende.go Normal file
View File

@@ -0,0 +1,230 @@
package controllers
import (
"database/sql"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/pocketbase/pocketbase/core"
)
const (
DEFAULT_PAGESIZE_BAENDE = 40
)
type BaendeFilterParameters struct {
Agent string
Year string
Place string
State string
}
func NewBaendeFilterParameters(ev *core.RequestEvent) BaendeFilterParameters {
agent := ev.Request.URL.Query().Get("agent")
year := ev.Request.URL.Query().Get("year")
place := ev.Request.URL.Query().Get("place")
state := ev.Request.URL.Query().Get("state")
return BaendeFilterParameters{
Agent: agent,
Year: year,
Place: place,
State: state,
}
}
type SearchResultBaende struct {
Queries []dbmodels.FTS5QueryRequest
// these are the sorted IDs for hits
Hits []string
Series map[string]*dbmodels.Series // <- Key: Series ID
Entries map[string]*dbmodels.Entry // <- Key: Entry ID
Places map[string]*dbmodels.Place // <- All places, Key: Place IDs
Agents map[string]*dbmodels.Agent // <- Key: Agent IDs
// INFO: this is as they say doppelt gemoppelt bc of a logic error i made while tired
EntriesSeries map[string][]*dbmodels.REntriesSeries // <- Key: Entry ID
SeriesEntries map[string][]*dbmodels.REntriesSeries // <- Key: Series ID
EntriesAgents map[string][]*dbmodels.REntriesAgents // <- Key: Entry ID
Pages []int
}
func EmptyResultBaende() *SearchResultBaende {
return &SearchResultBaende{
Hits: []string{},
Series: make(map[string]*dbmodels.Series),
Entries: make(map[string]*dbmodels.Entry),
Places: make(map[string]*dbmodels.Place),
Agents: make(map[string]*dbmodels.Agent),
EntriesSeries: make(map[string][]*dbmodels.REntriesSeries),
SeriesEntries: make(map[string][]*dbmodels.REntriesSeries),
EntriesAgents: make(map[string][]*dbmodels.REntriesAgents),
}
}
func NewSearchBaende(app core.App, params SearchParameters) (*SearchResultBaende, error) {
entries := []*dbmodels.Entry{}
queries := params.FieldSetBaende()
if params.AlmString != "" {
e, err := dbmodels.Entries_MusenalmID(app, params.AlmString)
if err != nil && err == sql.ErrNoRows {
return EmptyResultBaende(), nil
} else if err != nil {
return nil, err
}
entries = append(entries, e)
} else {
if len(queries) == 0 {
return nil, ErrNoQuery
}
ids, err := dbmodels.FTS5Search(app, dbmodels.ENTRIES_TABLE, queries...)
if err != nil {
return nil, err
} else if len(ids) == 0 {
return EmptyResultBaende(), nil
}
resultids := []any{}
for _, id := range ids {
resultids = append(resultids, id.ID)
}
e, err := dbmodels.Entries_IDs(app, resultids)
if err != nil {
return nil, err
}
entries = e
}
resultids := []any{}
for _, entry := range entries {
resultids = append(resultids, entry.Id)
}
entriesmap := make(map[string]*dbmodels.Entry)
for _, entry := range entries {
entriesmap[entry.Id] = entry
}
series, relations, err := Series_Entries(app, entries)
if err != nil {
return nil, err
}
seriesmap := make(map[string]*dbmodels.Series)
for _, s := range series {
seriesmap[s.Id] = s
}
relationsmap := make(map[string][]*dbmodels.REntriesSeries)
invrelationsmap := make(map[string][]*dbmodels.REntriesSeries)
for _, r := range relations {
invrelationsmap[r.Series()] = append(invrelationsmap[r.Series()], r)
relationsmap[r.Entry()] = append(relationsmap[r.Entry()], r)
}
agents, arelations, err := Agents_Entries_IDs(app, resultids)
if err != nil {
return nil, err
}
agentsmap := make(map[string]*dbmodels.Agent)
for _, a := range agents {
agentsmap[a.Id] = a
}
relationsagentsmap := make(map[string][]*dbmodels.REntriesAgents)
for _, r := range arelations {
relationsagentsmap[r.Entry()] = append(relationsagentsmap[r.Entry()], r)
}
placesids := []any{}
for _, entry := range entries {
for _, place := range entry.Places() {
placesids = append(placesids, place)
}
}
places, err := dbmodels.Places_IDs(app, placesids)
if err != nil {
return nil, err
}
placesmap := make(map[string]*dbmodels.Place)
for _, place := range places {
placesmap[place.Id] = place
}
hits := []string{}
pages := []int{}
if params.Sort == "series" {
dbmodels.Sort_Series_Title(series)
for _, s := range series {
hits = append(hits, s.Id)
}
pages = PagesMap(hits, invrelationsmap, DEFAULT_PAGESIZE_BAENDE)
} else {
dbmodels.Sort_Entries_Year_Title(entries)
for _, e := range entries {
hits = append(hits, e.Id)
}
pages = PagesArray(hits, DEFAULT_PAGESIZE_BAENDE)
}
if params.Page < 1 || params.Page > len(pages) {
params.Page = 1
}
if params.Page == len(pages) {
hits = hits[pages[params.Page-1]:]
} else {
hits = hits[pages[params.Page-1]:pages[params.Page]]
}
return &SearchResultBaende{
Hits: hits,
Series: seriesmap,
Entries: entriesmap,
Places: placesmap,
Agents: agentsmap,
EntriesSeries: relationsmap,
SeriesEntries: invrelationsmap,
EntriesAgents: relationsagentsmap,
Pages: pages,
}, nil
}
func (r SearchResultBaende) PagesCount() int {
return len(r.Pages) - 1
}
func (r SearchResultBaende) Count() int {
return len(r.Entries)
}
func (r SearchResultBaende) SeriesCount() int {
return len(r.Series)
}
func Agents_Entries_IDs(app core.App, ids []any) ([]*dbmodels.Agent, []*dbmodels.REntriesAgents, error) {
relations, err := dbmodels.REntriesAgents_Entries(app, ids)
if err != nil {
return nil, nil, err
}
agentids := []any{}
for _, r := range relations {
agentids = append(agentids, r.Agent())
}
agents, err := dbmodels.Agents_IDs(app, agentids)
if err != nil {
return nil, nil, err
}
return agents, relations, nil
}

View File

@@ -0,0 +1,341 @@
package controllers
import (
"database/sql"
"maps"
"slices"
"sort"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
"github.com/pocketbase/pocketbase/core"
)
const (
DEFAULT_PAGESIZE = 80
FILTER_PARAM_BEIAEGE_AGENT = "agentfilter"
FILTER_PARAM_BEIAEGE_TYPE = "typefilter"
FILTER_PARAM_BEIAEGE_ONLYSCANS = "onlyscans"
FILTER_PARAM_BEIAEGE_YEAR = "yearfilter"
)
type BeitraegeFilterParameters struct {
Agent string
Type string
Year string
OnlyScans bool
}
func NewBeitraegeFilterParameters(ev *core.RequestEvent) BeitraegeFilterParameters {
agent := ev.Request.URL.Query().Get(FILTER_PARAM_BEIAEGE_AGENT)
typ := ev.Request.URL.Query().Get(FILTER_PARAM_BEIAEGE_TYPE)
year := ev.Request.URL.Query().Get(FILTER_PARAM_BEIAEGE_YEAR)
onlyscans := ev.Request.URL.Query().Get(FILTER_PARAM_BEIAEGE_ONLYSCANS) == "on"
return BeitraegeFilterParameters{
Agent: agent,
Type: typ,
Year: year,
OnlyScans: onlyscans,
}
}
func (p *BeitraegeFilterParameters) FieldSetBeitraege() []dbmodels.FTS5QueryRequest {
ret := []dbmodels.FTS5QueryRequest{}
if p.Agent != "" {
q := "\"" + p.Agent + "\""
que := dbmodels.NormalizeQuery(q)
req := dbmodels.IntoQueryRequests([]string{dbmodels.AGENTS_TABLE}, que)
ret = append(ret, req...)
}
if p.Type != "" {
q := "\"" + p.Type + "\""
que := dbmodels.NormalizeQuery(q)
req := dbmodels.IntoQueryRequests([]string{dbmodels.MUSENALM_INHALTE_TYPE_FIELD}, que)
ret = append(ret, req...)
}
if p.Year != "" {
q := "\"" + p.Year + "\""
que := dbmodels.NormalizeQuery(q)
req := dbmodels.IntoQueryRequests([]string{dbmodels.ENTRIES_TABLE}, que)
ret = append(ret, req...)
}
return ret
}
func (p BeitraegeFilterParameters) ToQueryParams() string {
r := ""
if p.Agent != "" {
r += "&" + FILTER_PARAM_BEIAEGE_AGENT + "=" + p.Agent
}
if p.Type != "" {
r += "&" + FILTER_PARAM_BEIAEGE_TYPE + "=" + p.Type
}
if p.Year != "" {
r += "&" + FILTER_PARAM_BEIAEGE_YEAR + "=" + p.Year
}
if p.OnlyScans {
r += "&" + FILTER_PARAM_BEIAEGE_ONLYSCANS + "=on"
}
return r
}
func (p BeitraegeFilterParameters) ToQueryParamsWOScans() string {
r := ""
if p.Agent != "" {
r += "&" + FILTER_PARAM_BEIAEGE_AGENT + "=" + p.Agent
}
if p.Type != "" {
r += "&" + FILTER_PARAM_BEIAEGE_TYPE + "=" + p.Type
}
if p.Year != "" {
r += "&" + FILTER_PARAM_BEIAEGE_YEAR + "=" + p.Year
}
return r
}
type SearchResultBeitraege struct {
Queries []dbmodels.FTS5QueryRequest
// these are the sorted IDs for hits
Hits []string
Entries map[string]*dbmodels.Entry // <- Key: Entry ID
Agents map[string]*dbmodels.Agent // <- Key: Agent IDs
Contents map[string][]*dbmodels.Content // <- Key: Entry ID, or year
ContentsAgents map[string][]*dbmodels.RContentsAgents // <- Key: Content ID
Pages []int
AgentsList []*dbmodels.Agent
TypesList []string
YearList []int
}
func EmptyResultBeitraege() *SearchResultBeitraege {
return &SearchResultBeitraege{
Hits: []string{},
Entries: make(map[string]*dbmodels.Entry),
Agents: make(map[string]*dbmodels.Agent),
Contents: make(map[string][]*dbmodels.Content),
ContentsAgents: make(map[string][]*dbmodels.RContentsAgents),
}
}
func NewSearchBeitraege(app core.App, params SearchParameters, filters BeitraegeFilterParameters) (*SearchResultBeitraege, error) {
contents := []*dbmodels.Content{}
queries := params.FieldSetBeitraege()
fqueries := filters.FieldSetBeitraege()
queries = append(queries, fqueries...)
if params.AlmString != "" {
e, err := dbmodels.Contents_MusenalmID(app, params.AlmString)
if err != nil && err == sql.ErrNoRows {
return EmptyResultBeitraege(), nil
} else if err != nil {
return nil, err
}
contents = append(contents, e)
} else {
if len(queries) == 0 {
return nil, ErrNoQuery
}
hits, err := dbmodels.FTS5Search(app, dbmodels.CONTENTS_TABLE, queries...)
if err != nil {
return nil, err
} else if len(hits) == 0 {
return EmptyResultBeitraege(), nil
}
ids := []any{}
for _, hit := range hits {
ids = append(ids, hit.ID)
}
cs, err := dbmodels.Contents_IDs(app, ids)
if err != nil {
return nil, err
}
if filters.OnlyScans {
scans := []*dbmodels.Content{}
for _, c := range cs {
if len(c.Scans()) > 0 {
scans = append(scans, c)
}
}
cs = scans
}
contents = append(contents, cs...)
}
resultids := []any{}
uniqueresultentryids := map[string]bool{}
types := make(map[string]bool)
for _, content := range contents {
resultids = append(resultids, content.Id)
uniqueresultentryids[content.Entry()] = true
for _, typ := range content.MusenalmType() {
types[typ] = true
}
}
resultentryids := []any{}
for entryid, _ := range uniqueresultentryids {
resultentryids = append(resultentryids, entryid)
}
entries, err := dbmodels.Entries_IDs(app, datatypes.ToAny(resultentryids))
if err != nil {
return nil, err
}
if params.Sort == "year" {
dbmodels.Sort_Entries_Year_Title(entries)
} else {
dbmodels.Sort_Entries_Title_Year(entries)
}
arels, err := dbmodels.RContentsAgents_Contents(app, resultids)
if err != nil {
return nil, err
}
uniqueaids := map[string]bool{}
for _, a := range arels {
uniqueaids[a.Agent()] = true
}
aids := []any{}
for aid, _ := range uniqueaids {
aids = append(aids, aid)
}
agents, err := dbmodels.Agents_IDs(app, aids)
if err != nil {
return nil, err
}
contentsmap := make(map[string][]*dbmodels.Content)
for _, c := range contents {
contentsmap[c.Entry()] = append(contentsmap[c.Entry()], c)
}
for _, c := range contentsmap {
dbmodels.Sort_Contents_Numbering(c)
}
contentsagents := make(map[string][]*dbmodels.RContentsAgents)
for _, a := range arels {
contentsagents[a.Content()] = append(contentsagents[a.Content()], a)
}
agentsmap := make(map[string]*dbmodels.Agent)
for _, a := range agents {
agentsmap[a.Id] = a
}
entriesmap := make(map[string]*dbmodels.Entry)
years := make(map[int]bool)
for _, e := range entries {
entriesmap[e.Id] = e
years[e.Year()] = true
}
hits := []string{}
for _, e := range entries {
hits = append(hits, e.Id)
}
pages := PagesMap(hits, contentsmap, DEFAULT_PAGESIZE)
if params.Page < 1 || params.Page > len(pages) {
params.Page = 1
}
if params.Page == len(pages) {
hits = hits[pages[params.Page-1]:]
} else {
hits = hits[pages[params.Page-1]:pages[params.Page]]
}
tL := slices.Collect(maps.Keys(types))
sort.Strings(tL)
yL := slices.Collect(maps.Keys(years))
sort.Ints(yL)
dbmodels.Sort_Agents_Name(agents)
return &SearchResultBeitraege{
Queries: queries,
Hits: hits,
Entries: entriesmap,
Agents: agentsmap,
Contents: contentsmap,
ContentsAgents: contentsagents,
Pages: pages,
AgentsList: agents,
TypesList: tL,
YearList: yL,
}, nil
}
func (p *SearchResultBeitraege) CountEntries() int {
return len(p.Entries)
}
func (p *SearchResultBeitraege) Count() int {
cnt := 0
for _, c := range p.Contents {
cnt += len(c)
}
return cnt
}
func (p *SearchResultBeitraege) PagesCount() int {
return len(p.Pages) - 1
}
func PagesMap[T any](hits []string, hitmap map[string][]*T, pagesize int) []int {
ret := []int{0}
m := 0
for i, hit := range hits {
m += len(hitmap[hit])
if m >= pagesize {
ret = append(ret, i)
m = 0
}
}
if m > 0 {
ret = append(ret, len(hits))
}
return ret
}
func PagesArray[T any](hits []T, pagesize int) []int {
ret := []int{0}
m := 0
for i := range hits {
m++
if m >= pagesize {
ret = append(ret, i)
m = 0
}
}
if m > 0 {
ret = append(ret, len(hits))
}
return ret
}

View File

@@ -0,0 +1,486 @@
package controllers
import (
"fmt"
"strconv"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/pocketbase/pocketbase/core"
)
var ErrNoQuery = fmt.Errorf("No query")
const (
SEARCH_PARAM_ALM_NR = "alm"
SEARCH_PARAM_TITLE = "title"
SEARCH_PARAM_PERSONS = "persons"
SEARCH_PARAM_ANNOTATIONS = "annotations"
SEARCH_PARAM_YEAR = "year"
BAENDE_PARAM_PLACES = "places"
BAENDE_PARAM_SERIES = "series"
BAENDE_PARAM_REFS = "references"
BEITRAEGE_PARAM_ENTRY = "entry"
BEITRAEGE_PARAM_INCIPT = "incipit"
BEITRAEGE_PARAM_TYPE = "type"
)
type SearchParameters struct {
Parameters
Sort string // year, entry,
Annotations bool
Persons bool
Title bool
Series bool
Places bool
Refs bool
Year bool
Entry bool
Incipit bool
AnnotationsString string
PersonsString string
TitleString string
AlmString string
SeriesString string
PlacesString string
RefsString string
YearString string
EntryString string
IncipitString string
TypeString string
Page int
}
func NewSearchParameters(e *core.RequestEvent, p Parameters) (*SearchParameters, error) {
almstring := e.Request.URL.Query().Get(SEARCH_PARAM_ALM_NR + "string")
title := e.Request.URL.Query().Get(SEARCH_PARAM_TITLE) == "on"
titlestring := e.Request.URL.Query().Get(SEARCH_PARAM_TITLE + "string")
persons := e.Request.URL.Query().Get(SEARCH_PARAM_PERSONS) == "on"
personsstring := e.Request.URL.Query().Get(SEARCH_PARAM_PERSONS + "string")
annotations := e.Request.URL.Query().Get(SEARCH_PARAM_ANNOTATIONS) == "on"
annotationsstring := e.Request.URL.Query().Get(SEARCH_PARAM_ANNOTATIONS + "string")
year := e.Request.URL.Query().Get(SEARCH_PARAM_YEAR) == "on"
yearstring := e.Request.URL.Query().Get(SEARCH_PARAM_YEAR + "string")
typestring := e.Request.URL.Query().Get(BEITRAEGE_PARAM_TYPE + "string")
series := e.Request.URL.Query().Get(BAENDE_PARAM_SERIES) == "on"
seriesstring := e.Request.URL.Query().Get(BAENDE_PARAM_SERIES + "string")
places := e.Request.URL.Query().Get(BAENDE_PARAM_PLACES) == "on"
placesstring := e.Request.URL.Query().Get(BAENDE_PARAM_PLACES + "string")
refs := e.Request.URL.Query().Get(BAENDE_PARAM_REFS) == "on"
refss := e.Request.URL.Query().Get(BAENDE_PARAM_REFS + "string")
if refss != "" {
refss = "\"" + refss + "\""
}
incipit := e.Request.URL.Query().Get(BEITRAEGE_PARAM_INCIPT) == "on"
incipitstring := e.Request.URL.Query().Get(BEITRAEGE_PARAM_INCIPT + "string")
entry := e.Request.URL.Query().Get(BEITRAEGE_PARAM_ENTRY) == "on"
entrystring := e.Request.URL.Query().Get(BEITRAEGE_PARAM_ENTRY + "string")
sort := e.Request.URL.Query().Get("sort")
if sort == "" {
sort = "year"
}
page := e.Request.URL.Query().Get("page")
if page == "" {
page = "1"
}
pageint, err := strconv.Atoi(page)
if err != nil {
return nil, err
}
return &SearchParameters{
Parameters: p,
Sort: sort,
Page: pageint,
// INFO: Common parameters
Title: title,
Persons: persons,
Annotations: annotations,
Year: year,
// INFO: Baende parameters
Places: places,
Refs: refs,
Series: series,
// INFO: Beitraege parameters
Entry: entry,
Incipit: incipit,
// INFO: Expanded search
AlmString: almstring,
TitleString: titlestring,
SeriesString: seriesstring,
PersonsString: personsstring,
PlacesString: placesstring,
RefsString: refss,
AnnotationsString: annotationsstring,
YearString: yearstring,
EntryString: entrystring,
IncipitString: incipitstring,
TypeString: typestring,
}, nil
}
func (p SearchParameters) AllSearchTermsBaende() string {
res := []string{}
res = append(res, p.includedParams(p.Query)...)
res = append(res, p.includedParams(p.AnnotationsString)...)
res = append(res, p.includedParams(p.PersonsString)...)
res = append(res, p.includedParams(p.TitleString)...)
res = append(res, p.includedParams(p.SeriesString)...)
res = append(res, p.includedParams(p.PlacesString)...)
res = append(res, p.includedParams(p.RefsString)...)
res = append(res, p.includedParams(p.YearString)...)
res = append(res, p.AlmString)
return strings.Join(res, " ")
}
func (p SearchParameters) AllSearchTermsBeitraege() string {
res := []string{}
res = append(res, p.includedParams(p.Query)...)
res = append(res, p.includedParams(p.AnnotationsString)...)
res = append(res, p.includedParams(p.PersonsString)...)
res = append(res, p.includedParams(p.TitleString)...)
res = append(res, p.includedParams(p.YearString)...)
res = append(res, p.includedParams(p.EntryString)...)
res = append(res, p.includedParams(p.IncipitString)...)
res = append(res, p.includedParams(p.TypeString)...)
return strings.Join(res, " ")
}
func (p SearchParameters) includedParams(q string) []string {
res := []string{}
que := dbmodels.NormalizeQuery(q)
for _, qq := range que.Include {
res = append(res, qq)
}
for _, qq := range que.UnsafeI {
res = append(res, qq)
}
return res
}
func (p SearchParameters) SortToQueryParams() string {
return fmt.Sprintf("&sort=%s", p.Sort)
}
func (p SearchParameters) ToQueryParamsBeitraege() string {
q := "?"
if p.Extended {
q += "extended=true"
}
if p.Query != "" {
q += fmt.Sprintf("q=%s", p.Query)
if p.Title {
q += "&title=on"
}
if p.Persons {
q += "&persons=on"
}
if p.Annotations {
q += "&annotations=on"
}
if p.Year {
q += "&year=on"
}
if p.Entry {
q += "&entry=on"
}
if p.Incipit {
q += "&incipit=on"
}
}
if p.YearString != "" {
q += fmt.Sprintf("&yearstring=%s", p.YearString)
}
if p.AnnotationsString != "" {
q += fmt.Sprintf("&annotationsstring=%s", p.AnnotationsString)
}
if p.PersonsString != "" {
q += fmt.Sprintf("&personsstring=%s", p.PersonsString)
}
if p.TitleString != "" {
q += fmt.Sprintf("&titlestring=%s", p.TitleString)
}
if p.EntryString != "" {
q += fmt.Sprintf("&entrystring=%s", p.EntryString)
}
if p.IncipitString != "" {
q += fmt.Sprintf("&incipitstring=%s", p.IncipitString)
}
if p.TypeString != "" {
q += fmt.Sprintf("&typestring=%s", p.TypeString)
}
return q
}
func (p SearchParameters) ToQueryParamsBaende() string {
q := "?"
// TODO: use variables, url escape
if p.Extended {
q += "extended=true"
}
if p.Query != "" {
q += fmt.Sprintf("q=%s", p.Query)
if p.Title {
q += "&title=on"
}
if p.Persons {
q += "&persons=on"
}
if p.Annotations {
q += "&annotations=on"
}
if p.Series {
q += "&series=on"
}
if p.Places {
q += "&places=on"
}
if p.Refs {
q += "&references=on"
}
if p.Year {
q += "&year=on"
}
}
if p.YearString != "" {
q += fmt.Sprintf("&yearstring=%s", p.YearString)
}
if p.AnnotationsString != "" {
q += fmt.Sprintf("&annotationsstring=%s", p.AnnotationsString)
}
if p.PersonsString != "" {
q += fmt.Sprintf("&personsstring=%s", p.PersonsString)
}
if p.TitleString != "" {
q += fmt.Sprintf("&titlestring=%s", p.TitleString)
}
if p.SeriesString != "" {
q += fmt.Sprintf("&seriesstring=%s", p.SeriesString)
}
if p.PlacesString != "" {
q += fmt.Sprintf("&placesstring=%s", p.PlacesString)
}
if p.RefsString != "" {
q += fmt.Sprintf("&refsstring=%s", p.RefsString)
}
return q
}
func (p SearchParameters) IsBeitraegeSearch() bool {
return p.Collection == "beitraege" && (p.Query != "" || p.TypeString != "" || p.AlmString != "" || p.AnnotationsString != "" || p.PersonsString != "" || p.TitleString != "" || p.YearString != "" || p.EntryString != "" || p.IncipitString != "")
}
func (p SearchParameters) IsBaendeSearch() bool {
return p.Collection == "baende" && (p.Query != "" || p.AlmString != "" || p.AnnotationsString != "" || p.PersonsString != "" || p.TitleString != "" || p.SeriesString != "" || p.PlacesString != "" || p.RefsString != "" || p.YearString != "")
}
func (p SearchParameters) FieldSetBeitraege() []dbmodels.FTS5QueryRequest {
ret := []dbmodels.FTS5QueryRequest{}
if p.Query != "" {
fields := []string{dbmodels.ID_FIELD}
if p.Title {
fields = append(fields, dbmodels.TITLE_STMT_FIELD, dbmodels.SUBTITLE_STMT_FIELD, dbmodels.VARIANT_TITLE_FIELD, dbmodels.PARALLEL_TITLE_FIELD)
}
if p.Persons {
fields = append(fields, dbmodels.RESPONSIBILITY_STMT_FIELD, dbmodels.AGENTS_TABLE)
}
if p.Annotations {
fields = append(fields, dbmodels.ANNOTATION_FIELD)
}
if p.Year {
fields = append(fields, dbmodels.YEAR_FIELD)
}
if p.Entry {
fields = append(fields, dbmodels.ENTRIES_TABLE)
}
if p.Incipit {
fields = append(fields, dbmodels.INCIPIT_STMT_FIELD)
}
que := p.NormalizeQuery()
req := dbmodels.IntoQueryRequests(fields, que)
ret = append(ret, req...)
}
if p.AnnotationsString != "" {
que := dbmodels.NormalizeQuery(p.AnnotationsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.ANNOTATION_FIELD}, que)
ret = append(ret, req...)
}
if p.PersonsString != "" {
que := dbmodels.NormalizeQuery(p.PersonsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.AGENTS_TABLE, dbmodels.RESPONSIBILITY_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.TitleString != "" {
que := dbmodels.NormalizeQuery(p.TitleString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.TITLE_STMT_FIELD, dbmodels.SUBTITLE_STMT_FIELD, dbmodels.VARIANT_TITLE_FIELD, dbmodels.PARALLEL_TITLE_FIELD}, que)
ret = append(ret, req...)
}
if p.YearString != "" {
que := dbmodels.NormalizeQuery(p.YearString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.YEAR_FIELD}, que)
ret = append(ret, req...)
}
if p.EntryString != "" {
que := dbmodels.NormalizeQuery(p.EntryString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.ENTRIES_TABLE}, que)
ret = append(ret, req...)
}
if p.IncipitString != "" {
que := dbmodels.NormalizeQuery(p.IncipitString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.INCIPIT_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.TypeString != "" {
que := dbmodels.NormalizeQuery(p.TypeString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.MUSENALM_INHALTE_TYPE_FIELD}, que)
ret = append(ret, req...)
}
return ret
}
func (p SearchParameters) FieldSetBaende() []dbmodels.FTS5QueryRequest {
ret := []dbmodels.FTS5QueryRequest{}
if p.Query != "" {
fields := []string{dbmodels.ID_FIELD}
if p.Title {
// INFO: Preferred Title is not here to avoid hitting the Reihentitel
fields = append(fields,
dbmodels.TITLE_STMT_FIELD,
dbmodels.SUBTITLE_STMT_FIELD,
dbmodels.INCIPIT_STMT_FIELD,
dbmodels.VARIANT_TITLE_FIELD,
dbmodels.PARALLEL_TITLE_FIELD,
)
}
if p.Series {
fields = append(fields, dbmodels.SERIES_TABLE)
}
if p.Persons {
fields = append(fields, dbmodels.RESPONSIBILITY_STMT_FIELD, dbmodels.AGENTS_TABLE)
}
if p.Places {
fields = append(fields, dbmodels.PLACES_TABLE, dbmodels.PLACE_STMT_FIELD)
}
if p.Refs {
fields = append(fields, dbmodels.REFERENCES_FIELD)
}
if p.Annotations {
fields = append(fields, dbmodels.ANNOTATION_FIELD)
}
if p.Year {
fields = append(fields, dbmodels.YEAR_FIELD)
}
que := p.NormalizeQuery()
req := dbmodels.IntoQueryRequests(fields, que)
ret = append(ret, req...)
}
if p.AnnotationsString != "" {
que := dbmodels.NormalizeQuery(p.AnnotationsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.ANNOTATION_FIELD}, que)
ret = append(ret, req...)
}
if p.PersonsString != "" {
que := dbmodels.NormalizeQuery(p.PersonsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.AGENTS_TABLE, dbmodels.RESPONSIBILITY_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.TitleString != "" {
que := dbmodels.NormalizeQuery(p.TitleString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.TITLE_STMT_FIELD, dbmodels.SUBTITLE_STMT_FIELD, dbmodels.INCIPIT_STMT_FIELD, dbmodels.VARIANT_TITLE_FIELD, dbmodels.PARALLEL_TITLE_FIELD}, que)
ret = append(ret, req...)
}
if p.SeriesString != "" {
que := dbmodels.NormalizeQuery(p.SeriesString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.SERIES_TABLE}, que)
ret = append(ret, req...)
}
if p.PlacesString != "" {
que := dbmodels.NormalizeQuery(p.PlacesString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.PLACES_TABLE, dbmodels.PLACE_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.RefsString != "" {
que := dbmodels.NormalizeQuery(p.RefsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.REFERENCES_FIELD}, que)
ret = append(ret, req...)
}
if p.YearString != "" {
que := dbmodels.NormalizeQuery(p.YearString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.YEAR_FIELD}, que)
ret = append(ret, req...)
}
return ret
}
func (p SearchParameters) IsExtendedSearch() bool {
return p.AnnotationsString != "" || p.PersonsString != "" || p.TitleString != "" || p.AlmString != "" || p.SeriesString != "" || p.PlacesString != "" || p.RefsString != "" || p.YearString != "" || p.EntryString != "" || p.IncipitString != ""
}
func (p SearchParameters) NormalizeQuery() dbmodels.Query {
return dbmodels.NormalizeQuery(p.Query)
}
func (p SearchParameters) Prev() int {
if p.Page > 1 {
return p.Page - 1
}
return 1
}
func (p SearchParameters) Next() int {
return p.Page + 1
}

143
controllers/user_create.go Normal file
View File

@@ -0,0 +1,143 @@
package controllers
import (
"fmt"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_USER_CREATE = "/user/new/"
PATH_VALUE_ROLE = "role"
TEMPLATE_USER_CREATE = "/user/new/"
)
func init() {
ucp := &UserCreatePage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_USER_CREATE_NAME,
Layout: "blank",
Template: TEMPLATE_USER_CREATE,
URL: URL_USER_CREATE,
},
}
app.Register(ucp)
}
type UserCreatePage struct {
pagemodels.StaticPage
}
func (p *UserCreatePage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
rg := router.Group(URL_USER_CREATE)
rg.BindFunc(middleware.HasToken())
rg.GET("{"+PATH_VALUE_ROLE+"}", p.GET(engine, app))
rg.POST("{"+PATH_VALUE_ROLE+"}", p.POST(engine, app))
return nil
}
func (p *UserCreatePage) GET(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
role := e.Request.PathValue("role")
if role != "User" && role != "Editor" && role != "Admin" {
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
}
// TODO: check access token
data := make(map[string]any)
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
if err != nil {
return engine.Response500(e, err, data)
}
data["role"] = role
data["csrf_nonce"] = nonce
data["csrf_token"] = token
SetRedirect(data, e)
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func InvalidSignupResponse(engine *templating.Engine, e *core.RequestEvent, error string) error {
data := make(map[string]any)
data["error"] = error
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
if err != nil {
return engine.Response500(e, err, data)
}
data["csrf_nonce"] = nonce
data["csrf_token"] = token
SetRedirect(data, e)
str, err := engine.RenderToString(e, data, TEMPLATE_USER_CREATE, "blank")
if err != nil {
return engine.Response500(e, err, data)
}
return e.HTML(400, str)
}
func (p *UserCreatePage) POST(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
role := e.Request.PathValue("role")
if role != "User" && role != "Editor" && role != "Admin" {
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
}
// TODO: check access token
data := make(map[string]any)
data["role"] = role
formdata := struct {
Username string `form:"username"`
Password string `form:"password"`
PasswordRepeat string `form:"password_repeat"`
Name string `form:"name"`
CsrfNonce string `form:"csrf_nonce"`
CsrfToken string `form:"csrf_token"`
}{}
if err := e.BindBody(&formdata); err != nil {
return engine.Response500(e, err, data)
}
data["formdata"] = formdata
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.CsrfNonce, formdata.CsrfToken); err != nil {
return InvalidSignupResponse(engine, e, "CSRF-Token ungültig.")
}
if formdata.Username == "" || formdata.Password == "" || formdata.Name == "" {
return InvalidSignupResponse(engine, e, "Bitte alle Felder ausfüllen.")
}
if formdata.Password != formdata.PasswordRepeat {
return InvalidSignupResponse(engine, e, "Passwörter stimmen nicht überein.")
}
_, err := app.FindAuthRecordByEmail(dbmodels.USERS_TABLE, formdata.Username)
if err == nil {
return InvalidSignupResponse(engine, e, "Benutzer existiert bereits.")
}
user, err := dbmodels.CreateUser(app, formdata.Username, formdata.Password, formdata.Name, role)
if err != nil {
return InvalidSignupResponse(engine, e, fmt.Sprintf("Fehler beim Erstellen des Benutzers: %s", err.Error()))
}
SetRedirect(data, e)
data["user"] = user
return engine.Response200(e, p.Template, data, p.Layout)
}
}

327
controllers/user_edit.go Normal file
View File

@@ -0,0 +1,327 @@
package controllers
import (
"fmt"
"log/slog"
"time"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_USER = "/user/{uid}/"
URL_USER_EDIT = "edit/"
URL_USER_LOGOUT = "logout/"
URL_USER_DEACTIVATE = "deactivate/"
URL_USER_ACTIVATE = "activate/"
UID_PATH_VALUE = "uid"
TEMPLATE_USER_EDIT = "/user/edit/"
)
func init() {
ump := &UserEditPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_USER_EDIT_NAME,
Layout: pagemodels.LAYOUT_LOGIN_PAGES,
Template: TEMPLATE_USER_EDIT,
URL: URL_USER_EDIT,
},
}
app.Register(ump)
}
type UserEditPage struct {
pagemodels.StaticPage
}
func (p *UserEditPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
rg := router.Group(URL_USER)
rg.BindFunc(middleware.IsAdminOrUser())
rg.GET(URL_USER_EDIT, p.GET(engine, app))
rg.POST(URL_USER_EDIT, p.POST(engine, app))
rg.POST(URL_USER_DEACTIVATE, p.POSTDeactivate(engine, app))
rg.POST(URL_USER_ACTIVATE, p.POSTActivate(engine, app))
return nil
}
func (p *UserEditPage) GET(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
data := make(map[string]any)
err := p.getData(app, data, e)
if err != nil {
return engine.Response500(e, err, data)
}
return engine.Response200(e, TEMPLATE_USER_EDIT, data, p.Layout)
}
}
func (p *UserEditPage) getData(app core.App, data map[string]any, e *core.RequestEvent) error {
uid := e.Request.PathValue(UID_PATH_VALUE)
u, err := app.FindRecordById(dbmodels.USERS_TABLE, uid)
if err != nil {
return fmt.Errorf("Konnte Nutzer nicht finden: %w", err)
}
user := dbmodels.NewUser(u)
fu := user.Fixed()
data["user"] = &fu
data["db_user"] = user
nonce, token, err := CSRF_CACHE.GenerateTokenBundleWithExpiration(2 * time.Hour)
if err != nil {
return fmt.Errorf("Konnte CSRF-Token nicht generieren: %w", err)
}
data["csrf_token"] = token
data["csrf_nonce"] = nonce
SetRedirect(data, e)
return nil
}
func DeleteSessionsForUser(app core.App, uid string) error {
defer middleware.SESSION_CACHE.DeleteSessionByUserID(uid)
records := []*core.Record{}
err := app.RecordQuery(dbmodels.SESSIONS_TABLE).
Where(dbx.HashExp{dbmodels.SESSIONS_USER_FIELD: uid}).
All(&records)
if err != nil {
return err
}
err = app.RunInTransaction(func(tx core.App) error {
for _, r := range records {
session := dbmodels.NewSession(r)
session.SetStatus(dbmodels.TOKEN_STATUS_VALUES[3])
if err := tx.Save(r); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func (p *UserEditPage) InvalidDataResponse(engine *templating.Engine, e *core.RequestEvent, error string, user dbmodels.FixedUser) error {
data := make(map[string]any)
err := p.getData(e.App, data, e)
if err != nil {
return engine.Response500(e, err, data)
}
str, err := engine.RenderToString(e, data, p.Template, p.Layout)
if err != nil {
return engine.Response500(e, err, data)
}
return e.HTML(400, str)
}
func (p *UserEditPage) POSTDeactivate(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
data := make(map[string]any)
err := p.getData(app, data, e)
if err != nil {
return engine.Response500(e, err, data)
}
formdata := struct {
CSRF string `form:"csrf_token"`
Nonce string `form:"csrf_nonce"`
}{}
user := data["db_user"].(*dbmodels.User)
if err := e.BindBody(&formdata); err != nil {
return p.InvalidDataResponse(engine, e, "Formulardaten ungültig.", user.Fixed())
}
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
return p.InvalidDataResponse(engine, e, err.Error(), user.Fixed())
}
user.SetDeactivated(true)
if err := app.Save(user); err != nil {
return p.InvalidDataResponse(engine, e, "Konnte Nutzer nicht deaktivieren: "+err.Error(), user.Fixed())
}
DeleteSessionsForUser(app, user.Id)
req := templating.NewRequest(e)
if req.User() != nil && req.User().Id == user.Id {
return e.Redirect(303, "/login/")
}
fu := user.Fixed()
data["user"] = &fu
data["success"] = "Nutzer " + fu.Name + "(" + fu.Email + ") wurde deaktiviert."
e.Response.Header().Add("HX-Push-Url", "false")
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func (p *UserEditPage) POSTActivate(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
data := make(map[string]any)
err := p.getData(app, data, e)
if err != nil {
return engine.Response500(e, err, data)
}
user := data["db_user"].(*dbmodels.User)
formdata := struct {
CSRF string `form:"csrf_token"`
Nonce string `form:"csrf_nonce"`
}{}
if err := e.BindBody(&formdata); err != nil {
return p.InvalidDataResponse(engine, e, "Formulardaten ungültig.", user.Fixed())
}
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
return p.InvalidDataResponse(engine, e, err.Error(), user.Fixed())
}
user.SetDeactivated(false)
if err := app.Save(user); err != nil {
return p.InvalidDataResponse(engine, e, "Konnte Nutzer nicht aktivieren: "+err.Error(), user.Fixed())
}
fu := user.Fixed()
data["user"] = &fu
data["success"] = "Nutzer " + fu.Name + "(" + fu.Email + ") wurde aktiviert."
e.Response.Header().Add("HX-Push-Url", "false")
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func (p *UserEditPage) POST(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
data := make(map[string]any)
uid := e.Request.PathValue(UID_PATH_VALUE)
req := templating.NewRequest(e)
user := req.User()
u, err := app.FindRecordById(dbmodels.USERS_TABLE, uid)
if err != nil {
return engine.Response404(e, err, nil)
}
user_proxy := dbmodels.NewUser(u)
fu := user_proxy.Fixed()
formdata := struct {
Email string `form:"username"`
Name string `form:"name"`
Role string `form:"role"`
CsrfNonce string `form:"csrf_nonce"`
CsrfToken string `form:"csrf_token"`
Password string `form:"password"`
PasswordRepeat string `form:"password_repeat"`
OldPassword string `form:"old_password"`
Logout string `form:"logout"`
}{}
if err := e.BindBody(&formdata); err != nil {
return p.InvalidDataResponse(engine, e, err.Error(), fu)
}
if formdata.CsrfNonce != "" && formdata.CsrfToken != "" {
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.CsrfNonce, formdata.CsrfToken); err != nil {
return p.InvalidDataResponse(engine, e, "CSRF ungültig oder abgelaufen", fu)
}
} else {
return p.InvalidDataResponse(engine, e, "CSRF ungültig oder abgelaufen", fu)
}
if formdata.Email == "" || formdata.Name == "" {
return p.InvalidDataResponse(engine, e, "Bitte alle Felder ausfüllen", fu)
}
// INFO: at this point email and name changes are allowed
user_proxy.SetEmail(formdata.Email)
user_proxy.SetName(formdata.Name)
rolechanged := false
if formdata.Role != "" && formdata.Role != user_proxy.Role() {
if user.Role == "Admin" &&
(formdata.Role == "User" || formdata.Role == "Editor" || formdata.Role == "Admin") {
user_proxy.SetRole(formdata.Role)
rolechanged = true
} else {
return p.InvalidDataResponse(engine, e, "Rolle nicht erlaubt", fu)
}
}
passwordchanged := false
if formdata.Password != "" || formdata.PasswordRepeat != "" || formdata.OldPassword != "" {
if user.Role != "Admin" && formdata.OldPassword == "" {
return p.InvalidDataResponse(engine, e, "Altes Passwort erforderlich", fu)
} else if user.Role != "Admin" && !user_proxy.ValidatePassword(formdata.OldPassword) {
return p.InvalidDataResponse(engine, e, "Altes Passwort falsch", fu)
}
if formdata.Password != formdata.PasswordRepeat {
return p.InvalidDataResponse(engine, e, "Passwörter stimmen nicht überein", fu)
}
user_proxy.SetPassword(formdata.Password)
passwordchanged = true
}
if err := app.Save(user_proxy); err != nil {
return p.InvalidDataResponse(engine, e, err.Error(), fu)
}
slog.Info("UserEditPage: User edited", "user_id", user_proxy.Id, "role_changed", rolechanged, "password_changed", passwordchanged, "formdata", formdata)
if rolechanged || (passwordchanged && formdata.Logout == "on") {
slog.Error("UserEditPage: Deleting sessions for user", "user_id", user_proxy.Id, "role_changed", rolechanged, "password_changed", passwordchanged)
if err := DeleteSessionsForUser(app, user_proxy.Id); err != nil {
return p.InvalidDataResponse(engine, e, "Fehler beim Löschen der Sitzungen: "+err.Error(), fu)
}
if user_proxy.Id == user.Id {
// INFO: user changed his own role, so we log him out
return e.Redirect(303, "/login/")
}
}
go middleware.SESSION_CACHE.DeleteSessionByUserID(user_proxy.Id)
fu = user_proxy.Fixed()
data["user"] = &fu
if user_proxy.Id == user.Id {
e.Set("user", &fu)
}
data["success"] = "Benutzer erfolgreich bearbeitet"
nonce, token, err := CSRF_CACHE.GenerateTokenBundleWithExpiration(2 * time.Hour)
if err != nil {
return engine.Response500(e, err, nil)
}
data["csrf_token"] = token
data["csrf_nonce"] = nonce
return engine.Response200(e, TEMPLATE_USER_EDIT, data, p.Layout)
}
}

View File

@@ -0,0 +1,266 @@
package controllers
import (
"fmt"
"time"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
URL_USER_MANAGEMENT = "/user/management"
TEMPLATE_USER_MANAGEMENT = "/user/management/"
URL_DEACTIVATE_USER = "/deactivate/"
URL_ACTIVATE_USER = "/activate/"
URL_LOGOUT_USER = "/logout/"
)
type SessionCount struct {
Count int `json:"count" db:"count"`
UserId string `json:"user" db:"user"`
}
func init() {
ump := &UserManagementPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_USER_MGMT_NAME,
Layout: "blankfooter",
Template: TEMPLATE_USER_MANAGEMENT,
URL: URL_USER_MANAGEMENT,
},
}
app.Register(ump)
}
type UserManagementPage struct {
pagemodels.StaticPage
}
func (p *UserManagementPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
rg := router.Group(URL_USER_MANAGEMENT)
rg.BindFunc(middleware.IsAdmin())
rg.GET("", p.GET(engine, app))
rg.POST(URL_DEACTIVATE_USER, p.POSTDeactivate(engine, app))
rg.POST(URL_ACTIVATE_USER, p.POSTActivate(engine, app))
rg.POST(URL_LOGOUT_USER, p.POSTLogout(engine, app))
return nil
}
func GetSessionsCounts(app core.App) ([]*SessionCount, error) {
query := app.RecordQuery(dbmodels.SESSIONS_TABLE).
Select("COUNT(*) AS count", dbmodels.SESSIONS_USER_FIELD).
GroupBy(dbmodels.SESSIONS_USER_FIELD).
OrderBy("count DESC")
var counts []*SessionCount
err := query.All(&counts)
if err != nil {
return nil, fmt.Errorf("failed to get session counts: %w", err)
}
return counts, nil
}
func (p *UserManagementPage) GET(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
data := make(map[string]any)
p.getData(app, data)
SetRedirect(data, e)
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func (p *UserManagementPage) getData(app core.App, data map[string]any) error {
records := []*core.Record{}
err := app.RecordQuery(dbmodels.USERS_TABLE).OrderBy(dbmodels.USERS_NAME_FIELD).All(&records)
if err != nil {
return fmt.Errorf("Konnte keine Nutzer laden: %w", err)
}
users := make([]*dbmodels.User, 0, len(records))
for _, record := range records {
users = append(users, dbmodels.NewUser(record))
}
sessionCounts, err := GetSessionsCounts(app)
if err != nil {
return fmt.Errorf("Konnte keine Sitzungsanzahlen laden: %w", err)
}
scmap := make(map[string]int)
for _, sc := range sessionCounts {
scmap[sc.UserId] = sc.Count
}
data["users"] = users
data["len"] = len(users)
data["session_counts"] = scmap
csrfNonce, csrfToken, err := CSRF_CACHE.GenerateTokenBundleWithExpiration(2 * time.Hour)
if err != nil {
return fmt.Errorf("Konnte kein CSRF-Token generieren.")
}
data["csrf_nonce"] = csrfNonce
data["csrf_token"] = csrfToken
return nil
}
func (p *UserManagementPage) ErrorResponse(engine *templating.Engine, e *core.RequestEvent, err error) error {
data := make(map[string]any)
data["error"] = err.Error()
err = p.getData(e.App, data)
if err != nil {
engine.Response500(e, fmt.Errorf("Nutzerdaten konnten nicht geladen werden: %w", err), data)
}
str, err := engine.RenderToString(e, data, p.Template, p.Layout)
if err != nil {
engine.Response500(e, fmt.Errorf("Konnte Fehlerseite nicht rendern: %w", err), data)
}
e.Response.Header().Add("HX-Push-Url", "false")
return e.HTML(400, str)
}
func (p *UserManagementPage) POSTDeactivate(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
formdata := struct {
User string `form:"uid"`
CSRF string `form:"csrf_token"`
Nonce string `form:"csrf_nonce"`
}{}
if err := e.BindBody(&formdata); err != nil {
return p.ErrorResponse(engine, e, fmt.Errorf("Formulardaten ungültig: %w", err))
}
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
return p.ErrorResponse(engine, e, err)
}
user, err := app.FindRecordById(dbmodels.USERS_TABLE, formdata.User)
if err != nil {
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht finden."))
}
u := dbmodels.NewUser(user)
u.SetDeactivated(true)
if err := app.Save(u); err != nil {
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht deaktivieren: %w", err))
}
DeleteSessionsForUser(app, u.Id)
data := make(map[string]any)
data["success"] = "Nutzer " + u.Name() + "(" + u.Email() + ") wurde deaktiviert."
p.getData(app, data)
req := templating.NewRequest(e)
if req.User() != nil && req.User().Id == u.Id {
return e.Redirect(303, "/login/")
}
e.Response.Header().Add("HX-Push-Url", "false")
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func (p *UserManagementPage) POSTActivate(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
formdata := struct {
User string `form:"uid"`
CSRF string `form:"csrf_token"`
Nonce string `form:"csrf_nonce"`
}{}
if err := e.BindBody(&formdata); err != nil {
return p.ErrorResponse(engine, e, fmt.Errorf("Formulardaten ungültig: %w", err))
}
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
return p.ErrorResponse(engine, e, err)
}
user, err := app.FindRecordById(dbmodels.USERS_TABLE, formdata.User)
if err != nil {
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht finden."))
}
u := dbmodels.NewUser(user)
u.SetDeactivated(false)
if err := app.Save(u); err != nil {
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht aktivieren: %w", err))
}
go DeleteSessionsForUser(app, u.Id)
data := make(map[string]any)
data["success"] = "Nutzer " + u.Name() + "(" + u.Email() + ") wurde aktiviert."
p.getData(app, data)
req := templating.NewRequest(e)
if req.User() != nil && req.User().Id == u.Id {
return e.Redirect(303, "/login/")
}
e.Response.Header().Add("HX-Push-Url", "false")
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func (p *UserManagementPage) POSTLogout(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
formdata := struct {
User string `form:"uid"`
CSRF string `form:"csrf_token"`
Nonce string `form:"csrf_nonce"`
}{}
if err := e.BindBody(&formdata); err != nil {
return p.ErrorResponse(engine, e, fmt.Errorf("Formulardaten ungültig: %w", err))
}
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
return p.ErrorResponse(engine, e, err)
}
user, err := app.FindRecordById(dbmodels.USERS_TABLE, formdata.User)
if err != nil {
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht finden."))
}
u := dbmodels.NewUser(user)
DeleteSessionsForUser(app, u.Id)
data := make(map[string]any)
data["success"] = "Nutzer " + u.Name() + "(" + u.Email() + ") wurde überall ausgeloggt."
p.getData(app, data)
req := templating.NewRequest(e)
if req.User() != nil && req.User().Id == u.Id {
return e.Redirect(301, "/login/")
}
// TODO: is there a better way to do this?
// This destroys the URL FullPath thing, bc fullURL is set to /user/management/logout/
// Same above
e.Response.Header().Add("HX-Push-Url", "false")
return engine.Response200(e, p.Template, data, p.Layout)
}
}

View File

@@ -0,0 +1,125 @@
package controllers
import (
"fmt"
"time"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
"github.com/pocketbase/pocketbase/tools/types"
)
const (
URL_USER_MANAGEMENT_ACCESS = "/user/management/access/"
TEMPLATE_USER_MANAGEMENT_ACCESS = "/user/management/access/"
)
func init() {
ump := &UserManagementAccessPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_USER_MGMT_ACCESS_NAME,
Layout: "blankfooter",
Template: TEMPLATE_USER_MANAGEMENT_ACCESS,
URL: URL_USER_MANAGEMENT_ACCESS,
},
}
app.Register(ump)
}
type UserManagementAccessPage struct {
pagemodels.StaticPage
}
func (p *UserManagementAccessPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
rg := router.Group(URL_USER_MANAGEMENT_ACCESS)
rg.BindFunc(middleware.IsAdmin())
rg.GET("{"+PATH_VALUE_ROLE+"}", p.GET(engine, app))
rg.POST("{"+PATH_VALUE_ROLE+"}", p.POST(engine, app))
return nil
}
func (p *UserManagementAccessPage) GET(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
role := e.Request.PathValue("role")
if role != "User" && role != "Editor" && role != "Admin" {
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
}
path_access := URL_USER_CREATE + role
record, err := app.FindFirstRecordByData(dbmodels.ACCESS_TOKENS_TABLE, dbmodels.ACCESS_TOKENS_URL_FIELD, path_access)
var access_token *dbmodels.AccessToken
if err != nil {
token, err := dbmodels.CreateAccessToken(app, "", path_access, 7*24*time.Hour)
if err != nil {
return engine.Response500(e, err, nil)
}
access_token = token
} else {
access_token = dbmodels.NewAccessToken(record)
access_token.SetExpires(types.NowDateTime().Add(7 * 24 * time.Hour))
if err := app.Save(access_token); err != nil {
return engine.Response500(e, err, nil)
}
}
// TODO: check if access token exists, if not generate
data := make(map[string]any)
data["role"] = role
data["access_url"] = "https://musenalm.de" + path_access + "?token=" + access_token.Token()
data["relative_url"] = path_access + "?token=" + access_token.Token()
data["validUntil"] = access_token.Expires().Time().Local().Format("02.01.2006 15:04")
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
if err != nil {
return engine.Response500(e, err, data)
}
data["csrf_nonce"] = nonce
data["csrf_token"] = token
SetRedirect(data, e)
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func (p *UserManagementAccessPage) POST(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
role := e.Request.PathValue("role")
if role != "User" && role != "Editor" && role != "Admin" {
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
}
path_access := URL_USER_CREATE + role
record, err := app.FindFirstRecordByData(dbmodels.ACCESS_TOKENS_TABLE, dbmodels.ACCESS_TOKENS_URL_FIELD, path_access)
if err == nil {
go app.Delete(record)
}
token, err := dbmodels.CreateAccessToken(app, "", path_access, 7*24*time.Hour)
if err != nil {
return engine.Response500(e, err, nil)
}
data := make(map[string]any)
data["role"] = role
data["access_url"] = "https://musenalm.de" + path_access + "?token=" + token.Token()
data["relative_url"] = path_access + "?token=" + token.Token()
data["validUntil"] = token.Expires().Time().Format("02.01.2006 15:04")
SetRedirect(data, e)
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func SetRedirect(data map[string]any, e *core.RequestEvent) {
redirect_url := e.Request.URL.Query().Get("redirectTo")
if redirect_url != "" {
data["redirect_url"] = redirect_url
}
}