diff --git a/cmds/pubtargets.go b/cmds/pubtargets.go new file mode 100644 index 0000000..bb74e3b --- /dev/null +++ b/cmds/pubtargets.go @@ -0,0 +1,118 @@ +package cmds + +import ( + "context" + "fmt" + "log" + "os" + "text/tabwriter" + + "github.com/spf13/cobra" + "lmika.dev/lmika/weiro/config" + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/services" +) + +func PubTargetsAdd() *cobra.Command { + var ( + siteGUID string + targetType string + targetRef string + targetKey string + baseURL string + enabled bool + ) + + cmd := &cobra.Command{ + Use: "add", + Short: "Add a publication target", + Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.LoadConfig() + if err != nil { + log.Fatal(err) + } + + svcs, err := services.New(cfg) + if err != nil { + log.Fatal(err) + } + defer svcs.Close() + + ctx := context.Background() + site, err := svcs.DB.SelectSiteByGUID(ctx, siteGUID) + if err != nil { + log.Fatal(err) + } + + target := &models.SitePublishTarget{ + SiteID: site.ID, + GUID: models.NewNanoID(), + Enabled: enabled, + BaseURL: baseURL, + TargetType: targetType, + TargetRef: targetRef, + TargetKey: targetKey, + } + + if err := svcs.DB.SavePublishTarget(ctx, target); err != nil { + log.Fatal(err) + } + + fmt.Printf("Added publish target %s\n", target.GUID) + }, + } + + cmd.Flags().StringVarP(&siteGUID, "site", "s", "", "Site GUID") + cmd.Flags().StringVarP(&targetType, "type", "t", "", "Target type (localfs, netlify)") + cmd.Flags().StringVarP(&targetRef, "ref", "r", "", "Target reference") + cmd.Flags().StringVarP(&targetKey, "key", "k", "", "Target key") + cmd.Flags().StringVarP(&baseURL, "url", "u", "", "Base URL") + cmd.Flags().BoolVar(&enabled, "enabled", true, "Enable target") + + cmd.MarkFlagRequired("site") + cmd.MarkFlagRequired("type") + cmd.MarkFlagRequired("ref") + cmd.MarkFlagRequired("url") + + return cmd +} + +func PubTargets() *cobra.Command { + cmd := &cobra.Command{ + Use: "pubtargets ", + Short: "Manage publication targets", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.LoadConfig() + if err != nil { + log.Fatal(err) + } + + svcs, err := services.New(cfg) + if err != nil { + log.Fatal(err) + } + defer svcs.Close() + + ctx := context.Background() + site, err := svcs.DB.SelectSiteByGUID(ctx, args[0]) + if err != nil { + log.Fatal(err) + } + + targets, err := svcs.DB.SelectPublishTargetsOfSite(ctx, site.ID) + if err != nil { + log.Fatal(err) + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "GUID\tTARGET_TYPE\tENABLED\tTARGET_REF") + for _, target := range targets { + fmt.Fprintf(w, "%s\t%s\t%v\t%s\n", target.GUID, target.TargetType, target.Enabled, target.TargetRef) + } + w.Flush() + }, + } + cmd.AddCommand(PubTargetsAdd()) + return cmd +} diff --git a/cmds/server.go b/cmds/server.go new file mode 100644 index 0000000..ccf11f7 --- /dev/null +++ b/cmds/server.go @@ -0,0 +1,139 @@ +package cmds + +import ( + "context" + "html" + "html/template" + "log" + "path/filepath" + "strings" + "time" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/extractors" + "github.com/gofiber/fiber/v3/middleware/session" + "github.com/gofiber/fiber/v3/middleware/static" + "github.com/gofiber/storage/sqlite3/v2" + fiber_html "github.com/gofiber/template/html/v3" + "github.com/gofiber/utils/v2" + "github.com/spf13/cobra" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "lmika.dev/lmika/weiro/config" + "lmika.dev/lmika/weiro/handlers" + "lmika.dev/lmika/weiro/handlers/middleware" + "lmika.dev/lmika/weiro/services" +) + +func Root() *cobra.Command { + cmd := &cobra.Command{ + Use: "weiro", + Short: "Weiro is a simple blogging platform", + Long: `Weiro is a simple blogging platform. + +Starting weiro without any arguments will start the server. +`, + Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.LoadConfig() + if err != nil { + log.Fatal(err) + } + + svcs, err := services.New(cfg) + if err != nil { + log.Fatal(err) + } + defer svcs.Close() + + svcs.PublisherQueue.Start(context.Background()) + + fiberTemplate := fiber_html.New("./views", ".html") + fiberTemplate.Funcmap["sub"] = func(x, y int) int { return x - y } + fiberTemplate.Funcmap["markdown"] = func() func(s string) template.HTML { + mdParser := goldmark.New( + goldmark.WithExtensions(extension.GFM), + ) + return func(s string) template.HTML { + var sb strings.Builder + if err := mdParser.Convert([]byte(s), &sb); err != nil { + return template.HTML("Markdown error: " + html.EscapeString(err.Error())) + } + return template.HTML(sb.String()) + } + }() + + // Initialize custom config + store := sqlite3.New(sqlite3.Config{ + Database: filepath.Join(cfg.DataDir, "./fiber.db"), + Table: "fiber_storage", + Reset: false, + GCInterval: 10 * time.Second, + MaxOpenConns: 100, + MaxIdleConns: 100, + ConnMaxLifetime: 1 * time.Second, + }) + + app := fiber.New(fiber.Config{ + Views: fiberTemplate, + ViewsLayout: "layouts/main", + PassLocalsToViews: true, + }) + app.Use(session.New(session.Config{ + // Storage + Storage: store, + + // Security + CookieSecure: cfg.IsProd(), + CookieSameSite: "Lax", + + // Session Management + IdleTimeout: 24 * time.Hour, // Inactivity timeout + AbsoluteTimeout: 7 * 24 * time.Hour, // Maximum session duration + + // Cookie Settings + CookiePath: "/", + CookieDomain: cfg.SiteDomain, + CookieSessionOnly: false, // Persist across browser restarts + + // Session ID + Extractor: extractors.FromCookie("__wro-session_id"), + KeyGenerator: utils.SecureToken, + + // Error Handling + ErrorHandler: func(c fiber.Ctx, err error) { + log.Printf("Session error: %v", err) + }, + })) + + ih := handlers.IndexHandler{SiteService: svcs.Sites} + lh := handlers.LoginHandler{Config: cfg, AuthService: svcs.Auth} + ph := handlers.PostsHandler{PostService: svcs.Posts} + + app.Get("/login", lh.Login) + app.Post("/login", lh.DoLogin) + app.Post("/logout", lh.Logout) + + siteGroup := app.Group("/sites/:siteID", middleware.RequireUser(svcs.Auth), middleware.RequiresSite(svcs.Sites)) + + siteGroup.Get("/posts", ph.Index) + siteGroup.Get("/posts/new", ph.New) + siteGroup.Get("/posts/:postID", ph.Edit) + siteGroup.Post("/posts", ph.Update) + siteGroup.Patch("/posts/:postID", ph.Patch) + siteGroup.Delete("/posts/:postID", ph.Delete) + + app.Get("/", middleware.OptionalUser(svcs.Auth), ih.Index) + app.Get("/first-run", ih.FirstRun) + app.Post("/first-run", ih.FirstRunSubmit) + + app.Get("/static/*", static.New("./static")) + + if err := app.Listen(":3000"); err != nil { + log.Println(err) + } + }, + } + cmd.AddCommand(Sites()) + cmd.AddCommand(PubTargets()) + return cmd +} diff --git a/cmds/sites.go b/cmds/sites.go new file mode 100644 index 0000000..8e7dfea --- /dev/null +++ b/cmds/sites.go @@ -0,0 +1,46 @@ +package cmds + +import ( + "context" + "fmt" + "log" + "os" + "text/tabwriter" + + "github.com/spf13/cobra" + "lmika.dev/lmika/weiro/config" + "lmika.dev/lmika/weiro/services" +) + +func Sites() *cobra.Command { + cmd := &cobra.Command{ + Use: "sites", + Short: "Manage sites", + Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.LoadConfig() + if err != nil { + log.Fatal(err) + } + + svcs, err := services.New(cfg) + if err != nil { + log.Fatal(err) + } + defer svcs.Close() + + ctx := context.Background() + sites, err := svcs.Sites.ListAllSitesWithOwners(ctx) + if err != nil { + log.Fatal(err) + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "GUID\tOWNER\tNAME") + for _, site := range sites { + fmt.Fprintf(w, "%s\t%s\t%s\n", site.GUID, site.Username, site.Title) + } + w.Flush() + }, + } + return cmd +} diff --git a/config/config.go b/config/config.go index 448b73b..38cde39 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "path/filepath" "github.com/Netflix/go-env" ) @@ -24,3 +25,7 @@ func LoadConfig() (Config, error) { func (c Config) IsProd() bool { return c.Env != "dev" } + +func (c Config) DBName() string { + return filepath.Join(c.DataDir, "weiro.db") +} diff --git a/handlers/middleware/site.go b/handlers/middleware/site.go index 0cc2b7a..54211bc 100644 --- a/handlers/middleware/site.go +++ b/handlers/middleware/site.go @@ -3,12 +3,14 @@ package middleware import ( "strconv" + "emperror.dev/errors" "github.com/gofiber/fiber/v3" "lmika.dev/lmika/weiro/models" "lmika.dev/lmika/weiro/providers/db" + "lmika.dev/lmika/weiro/services/sites" ) -func RequiresSite(db *db.Provider) func(c fiber.Ctx) error { +func RequiresSite(sites *sites.Service) func(c fiber.Ctx) error { return func(c fiber.Ctx) error { siteIDStr := c.Params("siteID") if siteIDStr == "" { @@ -20,18 +22,15 @@ func RequiresSite(db *db.Provider) func(c fiber.Ctx) error { return fiber.ErrBadRequest } - user, ok := models.GetUser(c.Context()) - if !ok { - return fiber.ErrUnauthorized - } - - site, err := db.SelectSiteByID(c.Context(), siteID) + site, err := sites.GetSiteByID(c.Context(), siteID) if err != nil { - return fiber.ErrNotFound - } - - if site.OwnerID != user.ID { - return fiber.ErrForbidden + if errors.Is(err, models.UserRequiredError) { + return fiber.ErrForbidden + } else if errors.Is(err, models.PermissionError) || db.ErrorIsNoRows(err) { + return fiber.ErrNotFound + } else if errors.Is(err, models.NotFoundError) || db.ErrorIsNoRows(err) { + return err + } } c.Locals("site", site) diff --git a/main.go b/main.go index 040ba74..823bff5 100644 --- a/main.go +++ b/main.go @@ -1,176 +1,14 @@ package main import ( - "context" - "flag" - "html" - "html/template" - "log" - "path/filepath" - "strings" - "time" + "os" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/extractors" - "github.com/gofiber/fiber/v3/middleware/session" - "github.com/gofiber/fiber/v3/middleware/static" - "github.com/gofiber/storage/sqlite3/v2" - fiber_html "github.com/gofiber/template/html/v3" - "github.com/gofiber/utils/v2" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/extension" - "lmika.dev/lmika/weiro/config" - "lmika.dev/lmika/weiro/handlers" - "lmika.dev/lmika/weiro/handlers/middleware" - "lmika.dev/lmika/weiro/providers/db" - "lmika.dev/lmika/weiro/services/auth" - "lmika.dev/lmika/weiro/services/posts" - "lmika.dev/lmika/weiro/services/publisher" - "lmika.dev/lmika/weiro/services/sites" + "lmika.dev/lmika/weiro/cmds" _ "modernc.org/sqlite" ) func main() { - flagUser := flag.String("user", "", "select user to perform operation on") - flagPasswd := flag.String("passwd", "", "change password for user") - flag.Parse() - - cfg, err := config.LoadConfig() - if err != nil { - log.Fatal(err) + if err := cmds.Root().Execute(); err != nil { + os.Exit(1) } - - dbp, err := db.New(filepath.Join(cfg.DataDir, "weiro.db")) - if err != nil { - log.Fatal(err) - } - defer dbp.Close() - - authSvc := auth.New(dbp) - publisherSvc := publisher.New(dbp) - publisherQueue := publisher.NewQueue(publisherSvc) - postService := posts.New(dbp, publisherQueue) - siteService := sites.New(dbp) - - // CLI tools - if *flagPasswd != "" && *flagUser != "" { - user, err := authSvc.SetPassword(context.Background(), *flagUser, *flagPasswd) - if err != nil { - log.Fatal(err) - } - log.Printf("Password changed for user %s\n", user.Username) - return - } - - publisherQueue.Start(context.Background()) - - fiberTemplate := fiber_html.New("./views", ".html") - fiberTemplate.Funcmap["sub"] = func(x, y int) int { return x - y } - fiberTemplate.Funcmap["markdown"] = func() func(s string) template.HTML { - mdParser := goldmark.New( - goldmark.WithExtensions(extension.GFM), - ) - return func(s string) template.HTML { - var sb strings.Builder - if err := mdParser.Convert([]byte(s), &sb); err != nil { - return template.HTML("Markdown error: " + html.EscapeString(err.Error())) - } - return template.HTML(sb.String()) - } - }() - - // Initialize custom config - store := sqlite3.New(sqlite3.Config{ - Database: filepath.Join(cfg.DataDir, "./fiber.db"), - Table: "fiber_storage", - Reset: false, - GCInterval: 10 * time.Second, - MaxOpenConns: 100, - MaxIdleConns: 100, - ConnMaxLifetime: 1 * time.Second, - }) - - app := fiber.New(fiber.Config{ - Views: fiberTemplate, - ViewsLayout: "layouts/main", - PassLocalsToViews: true, - }) - app.Use(session.New(session.Config{ - // Storage - Storage: store, - - // Security - CookieSecure: cfg.IsProd(), - CookieSameSite: "Lax", - - // Session Management - IdleTimeout: 24 * time.Hour, // Inactivity timeout - AbsoluteTimeout: 7 * 24 * time.Hour, // Maximum session duration - - // Cookie Settings - CookiePath: "/", - CookieDomain: cfg.SiteDomain, - CookieSessionOnly: false, // Persist across browser restarts - - // Session ID - Extractor: extractors.FromCookie("__wro-session_id"), - KeyGenerator: utils.SecureToken, - - // Error Handling - ErrorHandler: func(c fiber.Ctx, err error) { - log.Printf("Session error: %v", err) - }, - })) - - ih := handlers.IndexHandler{SiteService: siteService} - lh := handlers.LoginHandler{Config: cfg, AuthService: authSvc} - ph := handlers.PostsHandler{PostService: postService} - - app.Get("/login", lh.Login) - app.Post("/login", lh.DoLogin) - app.Post("/logout", lh.Logout) - - siteGroup := app.Group("/sites/:siteID", middleware.RequireUser(authSvc), middleware.RequiresSite(dbp)) - - siteGroup.Get("/posts", ph.Index) - siteGroup.Get("/posts/new", ph.New) - siteGroup.Get("/posts/:postID", ph.Edit) - siteGroup.Post("/posts", ph.Update) - siteGroup.Patch("/posts/:postID", ph.Patch) - siteGroup.Delete("/posts/:postID", ph.Delete) - - app.Get("/", middleware.OptionalUser(authSvc), ih.Index) - app.Get("/first-run", ih.FirstRun) - app.Post("/first-run", ih.FirstRunSubmit) - - app.Get("/static/*", static.New("./static")) - - // TEMP - // - /* - dbp.SaveUser(context.Background(), &models.User{Username: "testuser"}) - - ctx := models.WithUser(context.Background(), models.User{ID: 1}) - site, err := importer.New(dbp).Import(ctx, "_test-site") - if err != nil { - log.Fatal(err) - } - - target := models.SitePublishTarget{ - SiteID: site.ID, - BaseURL: "https://jolly-boba-9e2486.netlify.app", - TargetType: "netlify", - TargetRef: "55c878a7-189e-42cf-aa02-5c60908143f3", - TargetKey: os.Getenv("NETLIFY_AUTH_TOKEN"), - } - - if err := dbp.SavePublishTarget(ctx, &target); err != nil { - log.Fatal(err) - } - */ - //if err := publisherSvc.Publish(ctx, site.ID); err != nil { - // log.Fatal(err) - //} - - log.Fatal(app.Listen(":3000")) } diff --git a/models/sites.go b/models/sites.go index c56c0f4..42b8a3c 100644 --- a/models/sites.go +++ b/models/sites.go @@ -10,6 +10,17 @@ const ( PublishTargetTypeNetlify PublishTargetType = 2 ) +func ParsePublishTargetType(s string) (PublishTargetType, error) { + switch s { + case "localfs": + return PublishTargetTypeLocalFS, nil + case "netlify": + return PublishTargetTypeNetlify, nil + default: + return PublishTargetTypeNone, nil + } +} + type Site struct { ID int64 OwnerID int64 @@ -23,6 +34,7 @@ type Site struct { type SitePublishTarget struct { ID int64 SiteID int64 + GUID string Enabled bool BaseURL string diff --git a/providers/db/gen/sqlgen/models.go b/providers/db/gen/sqlgen/models.go index f991778..5317ef2 100644 --- a/providers/db/gen/sqlgen/models.go +++ b/providers/db/gen/sqlgen/models.go @@ -21,6 +21,7 @@ type Post struct { type PublishTarget struct { ID int64 SiteID int64 + Guid string TargetType string Enabled int64 BaseUrl string diff --git a/providers/db/gen/sqlgen/pubtargets.sql.go b/providers/db/gen/sqlgen/pubtargets.sql.go index 49ca66a..cd5cfa6 100644 --- a/providers/db/gen/sqlgen/pubtargets.sql.go +++ b/providers/db/gen/sqlgen/pubtargets.sql.go @@ -12,17 +12,19 @@ import ( const insertPublishTarget = `-- name: InsertPublishTarget :one INSERT INTO publish_targets ( site_id, + guid, target_type, enabled, base_url, target_ref, target_key -) VALUES (?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id ` type InsertPublishTargetParams struct { SiteID int64 + Guid string TargetType string Enabled int64 BaseUrl string @@ -33,6 +35,7 @@ type InsertPublishTargetParams struct { func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTargetParams) (int64, error) { row := q.db.QueryRowContext(ctx, insertPublishTarget, arg.SiteID, + arg.Guid, arg.TargetType, arg.Enabled, arg.BaseUrl, @@ -45,7 +48,7 @@ func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTarg } const selectPublishTargetsOfSite = `-- name: SelectPublishTargetsOfSite :many -SELECT id, site_id, target_type, enabled, base_url, target_ref, target_key FROM publish_targets WHERE site_id = ? +SELECT id, site_id, guid, target_type, enabled, base_url, target_ref, target_key FROM publish_targets WHERE site_id = ? ` func (q *Queries) SelectPublishTargetsOfSite(ctx context.Context, siteID int64) ([]PublishTarget, error) { @@ -60,6 +63,7 @@ func (q *Queries) SelectPublishTargetsOfSite(ctx context.Context, siteID int64) if err := rows.Scan( &i.ID, &i.SiteID, + &i.Guid, &i.TargetType, &i.Enabled, &i.BaseUrl, diff --git a/providers/db/gen/sqlgen/sites.sql.go b/providers/db/gen/sqlgen/sites.sql.go index 90b7000..fd3d3c6 100644 --- a/providers/db/gen/sqlgen/sites.sql.go +++ b/providers/db/gen/sqlgen/sites.sql.go @@ -53,6 +53,68 @@ func (q *Queries) InsertSite(ctx context.Context, arg InsertSiteParams) (int64, return id, err } +const selectAllSitesWithOwners = `-- name: SelectAllSitesWithOwners :many +SELECT s.id, s.guid, s.title, s.owner_id, u.username +FROM sites s +JOIN users u ON s.owner_id = u.id +ORDER BY s.title ASC +` + +type SelectAllSitesWithOwnersRow struct { + ID int64 + Guid string + Title string + OwnerID int64 + Username string +} + +func (q *Queries) SelectAllSitesWithOwners(ctx context.Context) ([]SelectAllSitesWithOwnersRow, error) { + rows, err := q.db.QueryContext(ctx, selectAllSitesWithOwners) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SelectAllSitesWithOwnersRow + for rows.Next() { + var i SelectAllSitesWithOwnersRow + if err := rows.Scan( + &i.ID, + &i.Guid, + &i.Title, + &i.OwnerID, + &i.Username, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const selectSiteByGUID = `-- name: SelectSiteByGUID :one +SELECT id, owner_id, guid, title, tagline, created_at FROM sites WHERE guid = ? +` + +func (q *Queries) SelectSiteByGUID(ctx context.Context, guid string) (Site, error) { + row := q.db.QueryRowContext(ctx, selectSiteByGUID, guid) + var i Site + err := row.Scan( + &i.ID, + &i.OwnerID, + &i.Guid, + &i.Title, + &i.Tagline, + &i.CreatedAt, + ) + return i, err +} + const selectSiteByID = `-- name: SelectSiteByID :one SELECT id, owner_id, guid, title, tagline, created_at FROM sites WHERE id = ? ` diff --git a/providers/db/provider_test.go b/providers/db/provider_test.go index f4788cd..4781d61 100644 --- a/providers/db/provider_test.go +++ b/providers/db/provider_test.go @@ -158,7 +158,7 @@ func TestProvider_Posts(t *testing.T) { require.NoError(t, err) assert.NotZero(t, post.ID) - posts, err := p.SelectPostsOfSite(ctx, site.ID) + posts, err := p.SelectPostsOfSite(ctx, site.ID, false) require.NoError(t, err) require.Len(t, posts, 1) assert.Equal(t, post.ID, posts[0].ID) @@ -205,7 +205,7 @@ func TestProvider_Posts(t *testing.T) { require.NoError(t, p.SavePost(ctx, post1)) require.NoError(t, p.SavePost(ctx, post2)) - posts, err := p.SelectPostsOfSite(ctx, site2.ID) + posts, err := p.SelectPostsOfSite(ctx, site2.ID, false) require.NoError(t, err) require.Len(t, posts, 2) assert.Equal(t, "New Post", posts[0].Title) @@ -220,7 +220,7 @@ func TestProvider_Posts(t *testing.T) { } require.NoError(t, p.SaveSite(ctx, emptySite)) - posts, err := p.SelectPostsOfSite(ctx, emptySite.ID) + posts, err := p.SelectPostsOfSite(ctx, emptySite.ID, false) require.NoError(t, err) assert.Empty(t, posts) }) @@ -248,6 +248,7 @@ func TestProvider_PublishTargets(t *testing.T) { target := &models.SitePublishTarget{ SiteID: site.ID, TargetType: "netlify", + GUID: "target-001", BaseURL: "https://example.netlify.app", TargetRef: "netlify-site-123", TargetKey: "secret-key", diff --git a/providers/db/pubtargets.go b/providers/db/pubtargets.go index 74b0b26..dcfb25f 100644 --- a/providers/db/pubtargets.go +++ b/providers/db/pubtargets.go @@ -18,6 +18,7 @@ func (db *Provider) SelectPublishTargetsOfSite(ctx context.Context, siteID int64 targets[i] = models.SitePublishTarget{ ID: row.ID, SiteID: row.SiteID, + GUID: row.Guid, Enabled: row.Enabled != 0, TargetType: row.TargetType, BaseURL: row.BaseUrl, @@ -38,6 +39,7 @@ func (db *Provider) SavePublishTarget(ctx context.Context, target *models.SitePu newID, err := db.queries.InsertPublishTarget(ctx, sqlgen.InsertPublishTargetParams{ SiteID: target.SiteID, TargetType: target.TargetType, + Guid: target.GUID, Enabled: enabled, BaseUrl: target.BaseURL, TargetRef: target.TargetRef, diff --git a/providers/db/sites.go b/providers/db/sites.go index 51fc17f..f878e45 100644 --- a/providers/db/sites.go +++ b/providers/db/sites.go @@ -17,6 +17,15 @@ func (db *Provider) SelectSiteByID(ctx context.Context, id int64) (models.Site, return dbSiteToSite(row), nil } +func (db *Provider) SelectSiteByGUID(ctx context.Context, guid string) (models.Site, error) { + row, err := db.queries.SelectSiteByGUID(ctx, guid) + if err != nil { + return models.Site{}, err + } + + return dbSiteToSite(row), nil +} + func (db *Provider) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([]models.Site, error) { rows, err := db.queries.SelectSitesOwnedByUser(ctx, ownerID) if err != nil { @@ -58,6 +67,33 @@ func (db *Provider) HasUsersAndSites(ctx context.Context) (bool, error) { return nullBool.Valid && nullBool.Bool, nil } +type SiteWithOwner struct { + ID int64 + GUID string + Title string + OwnerID int64 + Username string +} + +func (db *Provider) SelectAllSitesWithOwners(ctx context.Context) ([]SiteWithOwner, error) { + rows, err := db.queries.SelectAllSitesWithOwners(ctx) + if err != nil { + return nil, err + } + + sites := make([]SiteWithOwner, len(rows)) + for i, row := range rows { + sites[i] = SiteWithOwner{ + ID: row.ID, + GUID: row.Guid, + Title: row.Title, + OwnerID: row.OwnerID, + Username: row.Username, + } + } + return sites, nil +} + func dbSiteToSite(row sqlgen.Site) models.Site { return models.Site{ ID: row.ID, diff --git a/services/services.go b/services/services.go new file mode 100644 index 0000000..edf52cd --- /dev/null +++ b/services/services.go @@ -0,0 +1,47 @@ +package services + +import ( + "path/filepath" + + "lmika.dev/lmika/weiro/config" + "lmika.dev/lmika/weiro/providers/db" + "lmika.dev/lmika/weiro/services/auth" + "lmika.dev/lmika/weiro/services/posts" + "lmika.dev/lmika/weiro/services/publisher" + "lmika.dev/lmika/weiro/services/sites" +) + +type Services struct { + DB *db.Provider + Auth *auth.Service + Publisher *publisher.Publisher + PublisherQueue *publisher.Queue + Posts *posts.Service + Sites *sites.Service +} + +func New(cfg config.Config) (*Services, error) { + dbp, err := db.New(filepath.Join(cfg.DataDir, "weiro.db")) + if err != nil { + return nil, err + } + + authSvc := auth.New(dbp) + publisherSvc := publisher.New(dbp) + publisherQueue := publisher.NewQueue(publisherSvc) + postService := posts.New(dbp, publisherQueue) + siteService := sites.New(dbp) + + return &Services{ + DB: dbp, + Auth: authSvc, + Publisher: publisherSvc, + PublisherQueue: publisherQueue, + Posts: postService, + Sites: siteService, + }, nil +} + +func (s *Services) Close() error { + return s.DB.Close() +} diff --git a/services/sites/services.go b/services/sites/services.go index d3711c9..22e3916 100644 --- a/services/sites/services.go +++ b/services/sites/services.go @@ -6,6 +6,7 @@ import ( "emperror.dev/errors" "github.com/go-ozzo/ozzo-validation/v4" + "github.com/gofiber/fiber/v3" "lmika.dev/lmika/weiro/models" "lmika.dev/lmika/weiro/providers/db" ) @@ -90,6 +91,7 @@ func (s *Service) FirstRun(ctx context.Context, req FirstRunRequest) (newUser mo target := models.SitePublishTarget{ SiteID: newSite.ID, Enabled: true, + GUID: models.NewNanoID(), BaseURL: req.SiteURL, TargetType: "netlify", TargetRef: req.NetlifySiteID, @@ -102,3 +104,25 @@ func (s *Service) FirstRun(ctx context.Context, req FirstRunRequest) (newUser mo return newUser, newSite, nil } + +func (s *Service) GetSiteByID(ctx context.Context, siteID int64) (models.Site, error) { + user, ok := models.GetUser(ctx) + if !ok { + return models.Site{}, models.UserRequiredError + } + + site, err := s.db.SelectSiteByID(ctx, siteID) + if err != nil { + return models.Site{}, err + } + + if site.OwnerID != user.ID { + return models.Site{}, fiber.ErrForbidden + } + + return site, nil +} + +func (s *Service) ListAllSitesWithOwners(ctx context.Context) ([]db.SiteWithOwner, error) { + return s.db.SelectAllSitesWithOwners(ctx) +} diff --git a/sql/queries/pubtargets.sql b/sql/queries/pubtargets.sql index e77ef8b..f3fdabd 100644 --- a/sql/queries/pubtargets.sql +++ b/sql/queries/pubtargets.sql @@ -4,10 +4,11 @@ SELECT * FROM publish_targets WHERE site_id = ?; -- name: InsertPublishTarget :one INSERT INTO publish_targets ( site_id, + guid, target_type, enabled, base_url, target_ref, target_key -) VALUES (?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id; \ No newline at end of file diff --git a/sql/queries/sites.sql b/sql/queries/sites.sql index 682aaaf..92e7ccb 100644 --- a/sql/queries/sites.sql +++ b/sql/queries/sites.sql @@ -4,6 +4,9 @@ SELECT * FROM sites WHERE owner_id = ? ORDER BY title ASC; -- name: SelectSiteByID :one SELECT * FROM sites WHERE id = ?; +-- name: SelectSiteByGUID :one +SELECT * FROM sites WHERE guid = ?; + -- name: InsertSite :one INSERT INTO sites ( owner_id, @@ -15,4 +18,10 @@ INSERT INTO sites ( RETURNING id; -- name: HasUsersAndSites :one -SELECT (SELECT COUNT(*) FROM users) > 0 AND (SELECT COUNT(*) FROM sites) > 0 AS has_users_and_sites; \ No newline at end of file +SELECT (SELECT COUNT(*) FROM users) > 0 AND (SELECT COUNT(*) FROM sites) > 0 AS has_users_and_sites; + +-- name: SelectAllSitesWithOwners :many +SELECT s.id, s.guid, s.title, s.owner_id, u.username +FROM sites s +JOIN users u ON s.owner_id = u.id +ORDER BY s.title ASC; \ No newline at end of file diff --git a/sql/schema/01_init.up.sql b/sql/schema/01_init.up.sql index 3bfce69..cc76a5b 100644 --- a/sql/schema/01_init.up.sql +++ b/sql/schema/01_init.up.sql @@ -22,6 +22,7 @@ CREATE UNIQUE INDEX idx_site_guid ON sites (guid); CREATE TABLE publish_targets ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_id INTEGER NOT NULL, + guid TEXT NOT NULL, target_type TEXT NOT NULL, enabled INT NOT NULL, base_url TEXT NOT NULL, @@ -29,6 +30,7 @@ CREATE TABLE publish_targets ( target_key TEXT NOT NULL ); CREATE INDEX idx_publish_targets_site ON publish_targets (site_id); +CREATE UNIQUE INDEX idx_publish_targets_guid ON publish_targets (guid); CREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT,