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_unchanged = 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_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css", "js"]
include_file = []

View file

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

View file

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

View file

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

View file

@ -9,6 +9,29 @@ import (
"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
INSERT INTO publish_targets (
site_id,
@ -41,12 +64,12 @@ func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTarg
return id, err
}
const listPublishTargetsOfRole = `-- name: ListPublishTargetsOfRole :many
SELECT id, site_id, role, target_type, url, target_ref FROM publish_targets WHERE site_id = $1 AND role = 'production'
const listTargetsOfSite = `-- name: ListTargetsOfSite :many
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) {
rows, err := q.db.Query(ctx, listPublishTargetsOfRole, siteID)
func (q *Queries) ListTargetsOfSite(ctx context.Context, siteID int64) ([]PublishTarget, error) {
rows, err := q.db.Query(ctx, listTargetsOfSite, siteID)
if err != nil {
return nil, err
}

View file

@ -1,9 +1,11 @@
package handlers
import (
"encoding/json"
"errors"
"github.com/gofiber/fiber/v2"
"lmika.dev/lmika/hugo-cms/models"
"log"
)
func GetUser(c *fiber.Ctx) models.User {
@ -21,3 +23,35 @@ func GetSite(c *fiber.Ctx) models.Site {
}
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
import (
"errors"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5"
"lmika.dev/lmika/hugo-cms/models"
"lmika.dev/lmika/hugo-cms/services/sites"
"net/http"
"time"
)
@ -19,6 +23,10 @@ func (s *Site) Create(c *fiber.Ctx) error {
return err
}
UpdatePrefCookie(c, func(prefs *models.PrefCookie) {
prefs.SiteID = 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 {
return func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) (err error) {
id, err := c.ParamsInt("siteId")
if err != nil {
return err
@ -58,7 +66,19 @@ func (s *Site) WithSite() fiber.Handler {
return err
}
user := GetUser(c)
if site.OwnerUserID != user.ID {
return c.Status(http.StatusForbidden).SendString("not permitted")
}
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()
}
}

View file

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

View file

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

View file

@ -5,6 +5,5 @@ type Site struct {
OwnerUserID int64
Name string
Title string
URL 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,
Title: site.Title,
OwnerUserID: site.OwnerUserID,
Url: site.URL,
Theme: site.Theme,
Props: []byte("{}"),
})
@ -29,10 +28,10 @@ func (db *DB) GetSite(ctx context.Context, id int64) (models.Site, error) {
}
return models.Site{
ID: site.ID,
Name: site.Name,
Title: site.Title,
URL: site.Url,
Theme: site.Theme,
ID: site.ID,
OwnerUserID: site.OwnerUserID,
Name: site.Name,
Title: site.Title,
Theme: site.Theme,
}, 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))
}
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) {
newSite := models.Site{
Name: normaliseName(name),
OwnerUserID: user.ID,
Title: name,
Theme: "bear",
URL: "https://meek-meringue-060cfc.netlify.app/",
}
_, ok := s.themes.Lookup(newSite.Theme)

View file

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

View file

@ -1,5 +1,8 @@
-- name: ListPublishTargetsOfRole :many
SELECT * FROM publish_targets WHERE site_id = $1 AND role = 'production';
-- name: ListTargetsOfSite :many
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
INSERT INTO publish_targets (

View file

@ -22,7 +22,6 @@ CREATE TABLE sites (
owner_user_id BIGINT NOT NULL,
name TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
url TEXT NOT NULL,
theme TEXT NOT NULL,
props JSON NOT NULL,
@ -50,5 +49,6 @@ CREATE TABLE publish_targets (
url 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>
User = {{.user.Email}}
<form method="post" action="/sites">
<input type="submit" value="Create Site">
</form>

View file

@ -13,7 +13,9 @@
<h1>Hugo CMS</h1>
<nav>
<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>
</nav>
</header>