diff --git a/.air.toml b/.air.toml index 850b4a9..0747c04 100644 --- a/.air.toml +++ b/.air.toml @@ -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 = [] diff --git a/Makefile b/Makefile index 7e99026..99bbcd1 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file + go run . -user test@example.com -password test123 \ No newline at end of file diff --git a/gen/sqlc/dbq/models.go b/gen/sqlc/dbq/models.go index b2b3d00..d4052a2 100644 --- a/gen/sqlc/dbq/models.go +++ b/gen/sqlc/dbq/models.go @@ -160,7 +160,6 @@ type Site struct { OwnerUserID int64 Name string Title string - Url string Theme string Props []byte } diff --git a/gen/sqlc/dbq/sites.sql.go b/gen/sqlc/dbq/sites.sql.go index 660c72c..3ba7978 100644 --- a/gen/sqlc/dbq/sites.sql.go +++ b/gen/sqlc/dbq/sites.sql.go @@ -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, ) diff --git a/gen/sqlc/dbq/targets.sql.go b/gen/sqlc/dbq/targets.sql.go index 020d793..6470e95 100644 --- a/gen/sqlc/dbq/targets.sql.go +++ b/gen/sqlc/dbq/targets.sql.go @@ -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 } diff --git a/handlers/ctx.go b/handlers/ctx.go index 86c269a..9a18d63 100644 --- a/handlers/ctx.go +++ b/handlers/ctx.go @@ -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) + } +} diff --git a/handlers/index.go b/handlers/index.go new file mode 100644 index 0000000..dfd4aad --- /dev/null +++ b/handlers/index.go @@ -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") +} diff --git a/handlers/site.go b/handlers/site.go index 94496bc..cded58a 100644 --- a/handlers/site.go +++ b/handlers/site.go @@ -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() } } diff --git a/main.go b/main.go index 15bf513..f6194b2 100644 --- a/main.go +++ b/main.go @@ -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) diff --git a/models/cookie.go b/models/cookie.go index 156f96b..918a7b4 100644 --- a/models/cookie.go +++ b/models/cookie.go @@ -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"` +} diff --git a/models/sites.go b/models/sites.go index 7a45321..3da9fc6 100644 --- a/models/sites.go +++ b/models/sites.go @@ -5,6 +5,5 @@ type Site struct { OwnerUserID int64 Name string Title string - URL string Theme string } diff --git a/providers/db/publish.go b/providers/db/publish.go deleted file mode 100644 index 51f035f..0000000 --- a/providers/db/publish.go +++ /dev/null @@ -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 -} diff --git a/providers/db/sites.go b/providers/db/sites.go index 9733150..fac59fa 100644 --- a/providers/db/sites.go +++ b/providers/db/sites.go @@ -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 } diff --git a/providers/db/targets.go b/providers/db/targets.go new file mode 100644 index 0000000..6f004cd --- /dev/null +++ b/providers/db/targets.go @@ -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, + } +} diff --git a/services/sites/service.go b/services/sites/service.go index 71b549c..53a0492 100644 --- a/services/sites/service.go +++ b/services/sites/service.go @@ -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) diff --git a/sql/queries/sites.sql b/sql/queries/sites.sql index ce8a7fe..c71346c 100644 --- a/sql/queries/sites.sql +++ b/sql/queries/sites.sql @@ -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; \ No newline at end of file diff --git a/sql/queries/targets.sql b/sql/queries/targets.sql index ba75c7e..011cc4d 100644 --- a/sql/queries/targets.sql +++ b/sql/queries/targets.sql @@ -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 ( diff --git a/sql/schema/1_init.up.sql b/sql/schema/1_init.up.sql index b4f0c42..38492d1 100644 --- a/sql/schema/1_init.up.sql +++ b/sql/schema/1_init.up.sql @@ -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) ); \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 0c4d297..614290c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,5 +1,7 @@