Fixed site ownership

This commit is contained in:
Leon Mika 2025-02-01 10:56:59 +11:00
parent cb54057305
commit 50f7e9632e
20 changed files with 192 additions and 78 deletions

View file

@ -12,7 +12,7 @@ exclude_file = []
exclude_regex = ["_test.go", "build/.*"] exclude_regex = ["_test.go", "build/.*"]
exclude_unchanged = false exclude_unchanged = false
follow_symlink = false follow_symlink = false
full_bin = "export $(cat .env | xargs) ; cd build ; ./hugo-cms" full_bin = "export $(cat .env | xargs) ; make init-db ; cd build ; ./hugo-cms"
include_dir = [] include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css", "js"] include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css", "js"]
include_file = [] include_file = []

View file

@ -1,7 +1,7 @@
.Phony: clean .Phony: clean
clean: clean:
-docker-compose down -v -docker-compose down -v
-rm -r build -rm -rf build
.Phony: prep .Phony: prep
prep: prep:
@ -14,5 +14,4 @@ compile: prep
.Phony: init-db .Phony: init-db
init-db: init-db:
export $(cat .env | xargs) go run . -user test@example.com -password test123
./build/hugo-cms -user test@example.com -password test123

View file

@ -160,7 +160,6 @@ type Site struct {
OwnerUserID int64 OwnerUserID int64
Name string Name string
Title string Title string
Url string
Theme string Theme string
Props []byte Props []byte
} }

View file

@ -10,7 +10,7 @@ import (
) )
const getSiteWithID = `-- name: GetSiteWithID :one const getSiteWithID = `-- name: GetSiteWithID :one
SELECT id, owner_user_id, name, title, url, theme, props FROM sites WHERE id = $1 LIMIT 1 SELECT id, owner_user_id, name, title, theme, props FROM sites WHERE id = $1 LIMIT 1
` `
func (q *Queries) GetSiteWithID(ctx context.Context, id int64) (Site, error) { func (q *Queries) GetSiteWithID(ctx context.Context, id int64) (Site, error) {
@ -21,7 +21,6 @@ func (q *Queries) GetSiteWithID(ctx context.Context, id int64) (Site, error) {
&i.OwnerUserID, &i.OwnerUserID,
&i.Name, &i.Name,
&i.Title, &i.Title,
&i.Url,
&i.Theme, &i.Theme,
&i.Props, &i.Props,
) )
@ -29,7 +28,7 @@ func (q *Queries) GetSiteWithID(ctx context.Context, id int64) (Site, error) {
} }
const listSites = `-- name: ListSites :one const listSites = `-- name: ListSites :one
SELECT id, owner_user_id, name, title, url, theme, props FROM sites SELECT id, owner_user_id, name, title, theme, props FROM sites
` `
func (q *Queries) ListSites(ctx context.Context) (Site, error) { func (q *Queries) ListSites(ctx context.Context) (Site, error) {
@ -40,7 +39,6 @@ func (q *Queries) ListSites(ctx context.Context) (Site, error) {
&i.OwnerUserID, &i.OwnerUserID,
&i.Name, &i.Name,
&i.Title, &i.Title,
&i.Url,
&i.Theme, &i.Theme,
&i.Props, &i.Props,
) )
@ -52,10 +50,9 @@ INSERT INTO sites (
name, name,
owner_user_id, owner_user_id,
title, title,
url,
theme, theme,
props props
) VALUES ($1, $2, $3, $4, $5, $6) ) VALUES ($1, $2, $3, $4, $5)
RETURNING id RETURNING id
` `
@ -63,7 +60,6 @@ type NewSiteParams struct {
Name string Name string
OwnerUserID int64 OwnerUserID int64
Title string Title string
Url string
Theme string Theme string
Props []byte Props []byte
} }
@ -73,7 +69,6 @@ func (q *Queries) NewSite(ctx context.Context, arg NewSiteParams) (int64, error)
arg.Name, arg.Name,
arg.OwnerUserID, arg.OwnerUserID,
arg.Title, arg.Title,
arg.Url,
arg.Theme, arg.Theme,
arg.Props, arg.Props,
) )

View file

@ -9,6 +9,29 @@ import (
"context" "context"
) )
const getTargetOfSiteRole = `-- name: GetTargetOfSiteRole :one
SELECT id, site_id, role, target_type, url, target_ref FROM publish_targets WHERE site_id = $1 AND role = $2 LIMIT 1
`
type GetTargetOfSiteRoleParams struct {
SiteID int64
Role TargetRole
}
func (q *Queries) GetTargetOfSiteRole(ctx context.Context, arg GetTargetOfSiteRoleParams) (PublishTarget, error) {
row := q.db.QueryRow(ctx, getTargetOfSiteRole, arg.SiteID, arg.Role)
var i PublishTarget
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Role,
&i.TargetType,
&i.Url,
&i.TargetRef,
)
return i, err
}
const insertPublishTarget = `-- name: InsertPublishTarget :one const insertPublishTarget = `-- name: InsertPublishTarget :one
INSERT INTO publish_targets ( INSERT INTO publish_targets (
site_id, site_id,
@ -41,12 +64,12 @@ func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTarg
return id, err return id, err
} }
const listPublishTargetsOfRole = `-- name: ListPublishTargetsOfRole :many const listTargetsOfSite = `-- name: ListTargetsOfSite :many
SELECT id, site_id, role, target_type, url, target_ref FROM publish_targets WHERE site_id = $1 AND role = 'production' SELECT id, site_id, role, target_type, url, target_ref FROM publish_targets WHERE site_id = $1
` `
func (q *Queries) ListPublishTargetsOfRole(ctx context.Context, siteID int64) ([]PublishTarget, error) { func (q *Queries) ListTargetsOfSite(ctx context.Context, siteID int64) ([]PublishTarget, error) {
rows, err := q.db.Query(ctx, listPublishTargetsOfRole, siteID) rows, err := q.db.Query(ctx, listTargetsOfSite, siteID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1,9 +1,11 @@
package handlers package handlers
import ( import (
"encoding/json"
"errors" "errors"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"lmika.dev/lmika/hugo-cms/models" "lmika.dev/lmika/hugo-cms/models"
"log"
) )
func GetUser(c *fiber.Ctx) models.User { func GetUser(c *fiber.Ctx) models.User {
@ -21,3 +23,35 @@ func GetSite(c *fiber.Ctx) models.Site {
} }
return s return s
} }
func UpdatePrefCookie(c *fiber.Ctx, update func(prefs *models.PrefCookie)) {
cookie := GetPrefCookie(c)
update(&cookie)
setPrefCookie(c, cookie)
}
func GetPrefCookie(c *fiber.Ctx) models.PrefCookie {
prefCookieValue := c.Cookies(models.PrefCookieName)
if prefCookieValue == "" {
return models.PrefCookie{}
}
var prefCookie models.PrefCookie
err := json.Unmarshal([]byte(prefCookieValue), &prefCookie)
if err != nil {
return models.PrefCookie{}
}
return prefCookie
}
func setPrefCookie(c *fiber.Ctx, prefCookie models.PrefCookie) {
if prefJson, err := json.Marshal(prefCookie); err == nil {
c.Cookie(&fiber.Cookie{
Name: models.PrefCookieName,
Value: string(prefJson),
})
} else {
log.Printf("unable to save pref cookie: %v", err)
}
}

19
handlers/index.go Normal file
View file

@ -0,0 +1,19 @@
package handlers
import (
"fmt"
"github.com/gofiber/fiber/v2"
"net/http"
)
type IndexHandler struct {
}
func (h IndexHandler) Index(c *fiber.Ctx) error {
prefs := GetPrefCookie(c)
if prefs.SiteID > 0 {
return c.Redirect(fmt.Sprintf("/sites/%v/posts", prefs.SiteID), http.StatusFound)
}
return c.Render("index", fiber.Map{}, "layouts/main")
}

View file

@ -1,9 +1,13 @@
package handlers package handlers
import ( import (
"errors"
"fmt" "fmt"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5"
"lmika.dev/lmika/hugo-cms/models"
"lmika.dev/lmika/hugo-cms/services/sites" "lmika.dev/lmika/hugo-cms/services/sites"
"net/http"
"time" "time"
) )
@ -19,6 +23,10 @@ func (s *Site) Create(c *fiber.Ctx) error {
return err return err
} }
UpdatePrefCookie(c, func(prefs *models.PrefCookie) {
prefs.SiteID = site.ID
})
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID)) return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
} }
@ -47,7 +55,7 @@ func (s *Site) Rebuild(c *fiber.Ctx) error {
} }
func (s *Site) WithSite() fiber.Handler { func (s *Site) WithSite() fiber.Handler {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) (err error) {
id, err := c.ParamsInt("siteId") id, err := c.ParamsInt("siteId")
if err != nil { if err != nil {
return err return err
@ -58,7 +66,19 @@ func (s *Site) WithSite() fiber.Handler {
return err return err
} }
user := GetUser(c)
if site.OwnerUserID != user.ID {
return c.Status(http.StatusForbidden).SendString("not permitted")
}
c.Locals("site", site) c.Locals("site", site)
if prodTarget, err := s.Site.GetProdTargetOfSite(c.UserContext(), int(site.ID)); err == nil {
c.Locals("prodTarget", prodTarget)
} else if !errors.Is(err, pgx.ErrNoRows) {
return err
}
return c.Next() return c.Next()
} }
} }

View file

@ -78,6 +78,7 @@ func main() {
siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService) siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService)
postService := posts.New(dbp, siteBuilderService, jobService) postService := posts.New(dbp, siteBuilderService, jobService)
indexHandlers := handlers.IndexHandler{}
siteHandlers := handlers.Site{Site: siteService} siteHandlers := handlers.Site{Site: siteService}
postHandlers := handlers.Post{Post: postService} postHandlers := handlers.Post{Post: postService}
authHandlers := handlers.AuthHandler{UserService: userService} authHandlers := handlers.AuthHandler{UserService: userService}
@ -115,9 +116,7 @@ func main() {
app.Post("/auth/login", authHandlers.Login) app.Post("/auth/login", authHandlers.Login)
app.Use(authHandlers.RequireAuth) app.Use(authHandlers.RequireAuth)
app.Get("/", func(c *fiber.Ctx) error { app.Get("/", indexHandlers.Index)
return c.Render("index", fiber.Map{}, "layouts/main")
})
app.Post("/sites", siteHandlers.Create) app.Post("/sites", siteHandlers.Create)
app.Get("/sites/:siteId", siteHandlers.Show) app.Get("/sites/:siteId", siteHandlers.Show)

View file

@ -2,8 +2,13 @@ package models
const ( const (
AuthCookieName = "hugocrm_auth" AuthCookieName = "hugocrm_auth"
PrefCookieName = "hugocrm_pref"
) )
type AuthCookie struct { type AuthCookie struct {
UserID int64 `json:"uid"` UserID int64 `json:"uid"`
} }
type PrefCookie struct {
SiteID int64 `json:"siteId"`
}

View file

@ -5,6 +5,5 @@ type Site struct {
OwnerUserID int64 OwnerUserID int64
Name string Name string
Title string Title string
URL string
Theme string Theme string
} }

View file

@ -1,42 +0,0 @@
package db
import (
"context"
"lmika.dev/lmika/hugo-cms/gen/sqlc/dbq"
"lmika.dev/lmika/hugo-cms/models"
"lmika.dev/pkg/modash/moslice"
)
func (db *DB) InsertPublishTarget(ctx context.Context, target *models.PublishTarget) error {
id, err := db.q.InsertPublishTarget(ctx, dbq.InsertPublishTargetParams{
SiteID: target.SiteID,
Role: dbq.TargetRole(target.Role),
TargetType: dbq.TargetType(target.Type),
Url: target.URL,
TargetRef: target.TargetRef,
})
if err != nil {
return err
}
target.ID = id
return nil
}
func (db *DB) GetPublishTargets(ctx context.Context, siteID int64) ([]models.PublishTarget, error) {
res, err := db.q.ListPublishTargetsOfRole(ctx, siteID)
if err != nil {
return nil, err
}
return moslice.Map(res, func(m dbq.PublishTarget) models.PublishTarget {
return models.PublishTarget{
ID: m.ID,
SiteID: m.SiteID,
Role: models.TargetRole(m.Role),
Type: models.TargetType(m.TargetType),
URL: m.Url,
TargetRef: m.TargetRef,
}
}), nil
}

View file

@ -11,7 +11,6 @@ func (db *DB) InsertSite(ctx context.Context, site *models.Site) error {
Name: site.Name, Name: site.Name,
Title: site.Title, Title: site.Title,
OwnerUserID: site.OwnerUserID, OwnerUserID: site.OwnerUserID,
Url: site.URL,
Theme: site.Theme, Theme: site.Theme,
Props: []byte("{}"), Props: []byte("{}"),
}) })
@ -29,10 +28,10 @@ func (db *DB) GetSite(ctx context.Context, id int64) (models.Site, error) {
} }
return models.Site{ return models.Site{
ID: site.ID, ID: site.ID,
Name: site.Name, OwnerUserID: site.OwnerUserID,
Title: site.Title, Name: site.Name,
URL: site.Url, Title: site.Title,
Theme: site.Theme, Theme: site.Theme,
}, nil }, nil
} }

56
providers/db/targets.go Normal file
View file

@ -0,0 +1,56 @@
package db
import (
"context"
"lmika.dev/lmika/hugo-cms/gen/sqlc/dbq"
"lmika.dev/lmika/hugo-cms/models"
"lmika.dev/pkg/modash/moslice"
)
func (db *DB) InsertPublishTarget(ctx context.Context, target *models.PublishTarget) error {
id, err := db.q.InsertPublishTarget(ctx, dbq.InsertPublishTargetParams{
SiteID: target.SiteID,
Role: dbq.TargetRole(target.Role),
TargetType: dbq.TargetType(target.Type),
Url: target.URL,
TargetRef: target.TargetRef,
})
if err != nil {
return err
}
target.ID = id
return nil
}
func (db *DB) GetPublishTargets(ctx context.Context, siteID int64) ([]models.PublishTarget, error) {
res, err := db.q.ListTargetsOfSite(ctx, siteID)
if err != nil {
return nil, err
}
return moslice.Map(res, dbTargetToTarget), nil
}
func (db *DB) GetPublishTargetBySiteRole(ctx context.Context, siteID int64, role models.TargetRole) (models.PublishTarget, error) {
target, err := db.q.GetTargetOfSiteRole(ctx, dbq.GetTargetOfSiteRoleParams{
SiteID: siteID,
Role: dbq.TargetRole(role),
})
if err != nil {
return models.PublishTarget{}, err
}
return dbTargetToTarget(target), nil
}
func dbTargetToTarget(m dbq.PublishTarget) models.PublishTarget {
return models.PublishTarget{
ID: m.ID,
SiteID: m.SiteID,
Role: models.TargetRole(m.Role),
Type: models.TargetType(m.TargetType),
URL: m.Url,
TargetRef: m.TargetRef,
}
}

View file

@ -41,13 +41,16 @@ func (s *Service) GetSite(ctx context.Context, id int) (models.Site, error) {
return s.db.GetSite(ctx, int64(id)) return s.db.GetSite(ctx, int64(id))
} }
func (s *Service) GetProdTargetOfSite(ctx context.Context, siteID int) (models.PublishTarget, error) {
return s.db.GetPublishTargetBySiteRole(ctx, int64(siteID), models.TargetRoleProduction)
}
func (s *Service) CreateSite(ctx context.Context, user models.User, name string) (models.Site, error) { func (s *Service) CreateSite(ctx context.Context, user models.User, name string) (models.Site, error) {
newSite := models.Site{ newSite := models.Site{
Name: normaliseName(name), Name: normaliseName(name),
OwnerUserID: user.ID, OwnerUserID: user.ID,
Title: name, Title: name,
Theme: "bear", Theme: "bear",
URL: "https://meek-meringue-060cfc.netlify.app/",
} }
_, ok := s.themes.Lookup(newSite.Theme) _, ok := s.themes.Lookup(newSite.Theme)

View file

@ -9,8 +9,7 @@ INSERT INTO sites (
name, name,
owner_user_id, owner_user_id,
title, title,
url,
theme, theme,
props props
) VALUES ($1, $2, $3, $4, $5, $6) ) VALUES ($1, $2, $3, $4, $5)
RETURNING id; RETURNING id;

View file

@ -1,5 +1,8 @@
-- name: ListPublishTargetsOfRole :many -- name: ListTargetsOfSite :many
SELECT * FROM publish_targets WHERE site_id = $1 AND role = 'production'; SELECT * FROM publish_targets WHERE site_id = $1;
-- name: GetTargetOfSiteRole :one
SELECT * FROM publish_targets WHERE site_id = $1 AND role = $2 LIMIT 1;
-- name: InsertPublishTarget :one -- name: InsertPublishTarget :one
INSERT INTO publish_targets ( INSERT INTO publish_targets (

View file

@ -22,7 +22,6 @@ CREATE TABLE sites (
owner_user_id BIGINT NOT NULL, owner_user_id BIGINT NOT NULL,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
title TEXT NOT NULL, title TEXT NOT NULL,
url TEXT NOT NULL,
theme TEXT NOT NULL, theme TEXT NOT NULL,
props JSON NOT NULL, props JSON NOT NULL,
@ -50,5 +49,6 @@ CREATE TABLE publish_targets (
url TEXT NOT NULL, url TEXT NOT NULL,
target_ref TEXT NOT NULL, target_ref TEXT NOT NULL,
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE,
UNIQUE (site_id, role)
); );

View file

@ -1,5 +1,7 @@
<h1>Thing</h1> <h1>Thing</h1>
User = {{.user.Email}}
<form method="post" action="/sites"> <form method="post" action="/sites">
<input type="submit" value="Create Site"> <input type="submit" value="Create Site">
</form> </form>

View file

@ -13,7 +13,9 @@
<h1>Hugo CMS</h1> <h1>Hugo CMS</h1>
<nav> <nav>
<span>{{.site.Name}}</span> <span>{{.site.Name}}</span>
<a href="{{.site.URL}}">Visit</a> {{ if .prodTarget }}
<a href="{{.prodTarget.URL}}" target="_blank">Visit</a>
{{ end }}
<a href="#">User</a> <a href="#">User</a>
</nav> </nav>
</header> </header>