Added user authentication
This commit is contained in:
parent
d7e7af5a10
commit
cb54057305
|
@ -3,9 +3,7 @@ testdata_dir = "testdata"
|
|||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = [
|
||||
"-no-auth"
|
||||
]
|
||||
args_bin = []
|
||||
bin = "./build/hugo-cms"
|
||||
cmd = "make compile"
|
||||
delay = 1000
|
||||
|
|
7
Makefile
7
Makefile
|
@ -10,4 +10,9 @@ prep:
|
|||
|
||||
.Phony: compile
|
||||
compile: prep
|
||||
go build -o ./build/hugo-cms
|
||||
go build -o ./build/hugo-cms
|
||||
|
||||
.Phony: init-db
|
||||
init-db:
|
||||
export $(cat .env | xargs)
|
||||
./build/hugo-cms -user test@example.com -password test123
|
|
@ -40,6 +40,11 @@ body.role-site main {
|
|||
}
|
||||
|
||||
|
||||
div.post {
|
||||
border-bottom: solid thin grey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
form.post-form {
|
||||
display: flex;
|
||||
|
|
|
@ -6,11 +6,13 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
DatabaseURL string `env:"DATABASE_URL"`
|
||||
NetlifyAuthToken string `env:"NETLIFY_AUTH_TOKEN"`
|
||||
DataDir string `env:"DATA_DIR"`
|
||||
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
|
||||
DataScratchDir string `env:"DATA_SCRATCH_DIR,default=scratch"`
|
||||
DatabaseURL string `env:"DATABASE_URL"`
|
||||
NetlifyAuthToken string `env:"NETLIFY_AUTH_TOKEN"`
|
||||
DataDir string `env:"DATA_DIR"`
|
||||
EncryptedCookieKey string `env:"ENCRYPTED_COOKIE_KEY"`
|
||||
|
||||
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
|
||||
DataScratchDir string `env:"DATA_SCRATCH_DIR,default=scratch"`
|
||||
}
|
||||
|
||||
func Load() (cfg Config, err error) {
|
||||
|
|
|
@ -156,10 +156,17 @@ type PublishTarget struct {
|
|||
}
|
||||
|
||||
type Site struct {
|
||||
ID int64
|
||||
Name string
|
||||
Title string
|
||||
Url string
|
||||
Theme string
|
||||
Props []byte
|
||||
ID int64
|
||||
OwnerUserID int64
|
||||
Name string
|
||||
Title string
|
||||
Url string
|
||||
Theme string
|
||||
Props []byte
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
|
|
@ -11,8 +11,17 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const deletePost = `-- name: DeletePost :exec
|
||||
DELETE FROM posts WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeletePost(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, deletePost, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getPostWithID = `-- name: GetPostWithID :one
|
||||
SELECT id, site_id, title, body, state, props, post_date, created_at FROM post WHERE id = $1 LIMIT 1
|
||||
SELECT id, site_id, title, body, state, props, post_date, created_at FROM posts WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetPostWithID(ctx context.Context, id int64) (Post, error) {
|
||||
|
@ -32,7 +41,7 @@ func (q *Queries) GetPostWithID(ctx context.Context, id int64) (Post, error) {
|
|||
}
|
||||
|
||||
const insertPost = `-- name: InsertPost :one
|
||||
INSERT INTO post (
|
||||
INSERT INTO posts (
|
||||
site_id,
|
||||
title,
|
||||
body,
|
||||
|
@ -70,7 +79,7 @@ func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64,
|
|||
}
|
||||
|
||||
const listPosts = `-- name: ListPosts :many
|
||||
SELECT id, site_id, title, body, state, props, post_date, created_at FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25
|
||||
SELECT id, site_id, title, body, state, props, post_date, created_at FROM posts WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25
|
||||
`
|
||||
|
||||
func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
||||
|
@ -104,7 +113,7 @@ func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
|||
|
||||
const listPublishablePosts = `-- name: ListPublishablePosts :many
|
||||
SELECT id, site_id, title, body, state, props, post_date, created_at
|
||||
FROM post
|
||||
FROM posts
|
||||
WHERE id > $1 AND site_id = $2 AND state = 'published' AND post_date <= $3
|
||||
ORDER BY id LIMIT 100
|
||||
`
|
||||
|
@ -145,7 +154,7 @@ func (q *Queries) ListPublishablePosts(ctx context.Context, arg ListPublishableP
|
|||
}
|
||||
|
||||
const updatePost = `-- name: UpdatePost :exec
|
||||
UPDATE post SET
|
||||
UPDATE posts SET
|
||||
site_id = $2,
|
||||
title = $3,
|
||||
body = $4,
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
const getSiteWithID = `-- name: GetSiteWithID :one
|
||||
SELECT id, name, title, url, theme, props FROM site WHERE id = $1 LIMIT 1
|
||||
SELECT id, owner_user_id, name, title, url, theme, props FROM sites WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSiteWithID(ctx context.Context, id int64) (Site, error) {
|
||||
|
@ -18,6 +18,7 @@ func (q *Queries) GetSiteWithID(ctx context.Context, id int64) (Site, error) {
|
|||
var i Site
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.OwnerUserID,
|
||||
&i.Name,
|
||||
&i.Title,
|
||||
&i.Url,
|
||||
|
@ -28,7 +29,7 @@ func (q *Queries) GetSiteWithID(ctx context.Context, id int64) (Site, error) {
|
|||
}
|
||||
|
||||
const listSites = `-- name: ListSites :one
|
||||
SELECT id, name, title, url, theme, props FROM site
|
||||
SELECT id, owner_user_id, name, title, url, theme, props FROM sites
|
||||
`
|
||||
|
||||
func (q *Queries) ListSites(ctx context.Context) (Site, error) {
|
||||
|
@ -36,6 +37,7 @@ func (q *Queries) ListSites(ctx context.Context) (Site, error) {
|
|||
var i Site
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.OwnerUserID,
|
||||
&i.Name,
|
||||
&i.Title,
|
||||
&i.Url,
|
||||
|
@ -46,27 +48,30 @@ func (q *Queries) ListSites(ctx context.Context) (Site, error) {
|
|||
}
|
||||
|
||||
const newSite = `-- name: NewSite :one
|
||||
INSERT INTO site (
|
||||
INSERT INTO sites (
|
||||
name,
|
||||
owner_user_id,
|
||||
title,
|
||||
url,
|
||||
theme,
|
||||
props
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type NewSiteParams struct {
|
||||
Name string
|
||||
Title string
|
||||
Url string
|
||||
Theme string
|
||||
Props []byte
|
||||
Name string
|
||||
OwnerUserID int64
|
||||
Title string
|
||||
Url string
|
||||
Theme string
|
||||
Props []byte
|
||||
}
|
||||
|
||||
func (q *Queries) NewSite(ctx context.Context, arg NewSiteParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, newSite,
|
||||
arg.Name,
|
||||
arg.OwnerUserID,
|
||||
arg.Title,
|
||||
arg.Url,
|
||||
arg.Theme,
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
const insertPublishTarget = `-- name: InsertPublishTarget :one
|
||||
INSERT INTO publish_target (
|
||||
INSERT INTO publish_targets (
|
||||
site_id,
|
||||
role,
|
||||
target_type,
|
||||
|
@ -42,7 +42,7 @@ func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTarg
|
|||
}
|
||||
|
||||
const listPublishTargetsOfRole = `-- name: ListPublishTargetsOfRole :many
|
||||
SELECT id, site_id, role, target_type, url, target_ref FROM publish_target WHERE site_id = $1 AND role = 'production'
|
||||
SELECT id, site_id, role, target_type, url, target_ref FROM publish_targets WHERE site_id = $1 AND role = 'production'
|
||||
`
|
||||
|
||||
func (q *Queries) ListPublishTargetsOfRole(ctx context.Context, siteID int64) ([]PublishTarget, error) {
|
||||
|
|
53
gen/sqlc/dbq/users.sql.go
Normal file
53
gen/sqlc/dbq/users.sql.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: users.sql
|
||||
|
||||
package dbq
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const addUser = `-- name: AddUser :one
|
||||
INSERT INTO users (
|
||||
email,
|
||||
password
|
||||
) VALUES ($1, $2)
|
||||
ON CONFLICT (email) DO UPDATE SET password = $2
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type AddUserParams struct {
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, addUser, arg.Email, arg.Password)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, email, password FROM users WHERE email = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByEmail, email)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Email, &i.Password)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, email, password FROM users WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByID, id)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Email, &i.Password)
|
||||
return i, err
|
||||
}
|
4
go.mod
4
go.mod
|
@ -10,7 +10,9 @@ require (
|
|||
require (
|
||||
github.com/Netflix/go-env v0.1.2 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.52.6 // indirect
|
||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4 // indirect
|
||||
github.com/gofiber/schema v1.2.0 // indirect
|
||||
|
@ -18,6 +20,7 @@ require (
|
|||
github.com/gofiber/template/html/v2 v2.1.3 // indirect
|
||||
github.com/gofiber/utils v1.1.0 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
|
@ -40,6 +43,7 @@ require (
|
|||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -2,10 +2,14 @@ github.com/Netflix/go-env v0.1.2 h1:0DRoLR9lECQ9Zqvkswuebm3jJ/2enaDX6Ei8/Z+EnK0=
|
|||
github.com/Netflix/go-env v0.1.2/go.mod h1:WlIhYi++8FlKNJtrop1mjXYAJMzv1f43K4MqCoh0yGE=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
|
||||
|
@ -20,6 +24,8 @@ github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
|||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ=
|
||||
github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
@ -76,6 +82,8 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
|||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
69
handlers/auth.go
Normal file
69
handlers/auth.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"lmika.dev/lmika/hugo-cms/models"
|
||||
"lmika.dev/lmika/hugo-cms/services/users"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
UserService *users.Service
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ShowLogin(c *fiber.Ctx) error {
|
||||
return c.Render("auth/login", fiber.Map{}, "layouts/login")
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
Email string `form:"email"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New("invalid email or password")
|
||||
}
|
||||
|
||||
user, err := h.UserService.VerifyLogin(c.UserContext(), req.Email, req.Password)
|
||||
if err != nil {
|
||||
return errors.New("invalid email or password")
|
||||
}
|
||||
|
||||
bts, err := json.Marshal(models.AuthCookie{UserID: user.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: models.AuthCookieName,
|
||||
Value: string(bts),
|
||||
})
|
||||
return c.Redirect("/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) RequireAuth(c *fiber.Ctx) error {
|
||||
user, err := h.readAuthCookie(c)
|
||||
if err != nil {
|
||||
return c.Redirect("/auth/login", http.StatusFound)
|
||||
}
|
||||
|
||||
c.Locals("user", user)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (h *AuthHandler) readAuthCookie(c *fiber.Ctx) (user models.User, err error) {
|
||||
authData := c.Cookies(models.AuthCookieName)
|
||||
if authData == "" {
|
||||
return models.User{}, errors.New("no auth cookie")
|
||||
}
|
||||
|
||||
var ac models.AuthCookie
|
||||
if err := json.Unmarshal([]byte(authData), &ac); err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
return h.UserService.GetUserByID(c.UserContext(), ac.UserID)
|
||||
}
|
|
@ -1,14 +1,23 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"lmika.dev/lmika/hugo-cms/models"
|
||||
)
|
||||
|
||||
type siteKeyType struct{}
|
||||
|
||||
var siteKey siteKeyType
|
||||
func GetUser(c *fiber.Ctx) models.User {
|
||||
u, ok := c.Locals("user").(models.User)
|
||||
if !ok {
|
||||
panic(errors.New("user not found in context"))
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func GetSite(c *fiber.Ctx) models.Site {
|
||||
return c.UserContext().Value(siteKey).(models.Site)
|
||||
s, ok := c.Locals("site").(models.Site)
|
||||
if !ok {
|
||||
panic(errors.New("no site in context"))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
37
handlers/mimeselectors.go
Normal file
37
handlers/mimeselectors.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package handlers
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
|
||||
type mimeTypeHandler interface {
|
||||
CanHandle(c *fiber.Ctx) bool
|
||||
Handle(c *fiber.Ctx) error
|
||||
}
|
||||
|
||||
type HTMX func(c *fiber.Ctx) error
|
||||
|
||||
func (h HTMX) CanHandle(c *fiber.Ctx) bool {
|
||||
return c.Get("Hx-request") == "true"
|
||||
}
|
||||
|
||||
func (h HTMX) Handle(c *fiber.Ctx) error {
|
||||
return h(c)
|
||||
}
|
||||
|
||||
type Otherwise func(c *fiber.Ctx) error
|
||||
|
||||
func (h Otherwise) CanHandle(c *fiber.Ctx) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h Otherwise) Handle(c *fiber.Ctx) error {
|
||||
return h(c)
|
||||
}
|
||||
|
||||
func Select(c *fiber.Ctx, mimeTypes ...mimeTypeHandler) error {
|
||||
for _, mt := range mimeTypes {
|
||||
if mt.CanHandle(c) {
|
||||
return mt.Handle(c)
|
||||
}
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("cant handle response")
|
||||
}
|
186
handlers/post.go
186
handlers/post.go
|
@ -5,121 +5,125 @@ import (
|
|||
"github.com/gofiber/fiber/v2"
|
||||
"lmika.dev/lmika/hugo-cms/models"
|
||||
"lmika.dev/lmika/hugo-cms/services/posts"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
Post *posts.Service
|
||||
}
|
||||
|
||||
func (h *Post) Posts() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
func (h *Post) Posts(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
|
||||
posts, err := h.Post.ListPostOfSite(c.UserContext(), site)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("posts/index", fiber.Map{
|
||||
"site": site,
|
||||
"posts": posts,
|
||||
}, "layouts/site")
|
||||
posts, err := h.Post.ListPostOfSite(c.UserContext(), site)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("posts/index", fiber.Map{
|
||||
"posts": posts,
|
||||
}, "layouts/site")
|
||||
}
|
||||
|
||||
func (h *Post) New() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
|
||||
return c.Render("posts/new", fiber.Map{
|
||||
"site": site,
|
||||
"post": models.Post{},
|
||||
}, "layouts/site")
|
||||
}
|
||||
func (h *Post) New(c *fiber.Ctx) error {
|
||||
return c.Render("posts/new", fiber.Map{
|
||||
"post": models.Post{},
|
||||
}, "layouts/site")
|
||||
}
|
||||
|
||||
func (h *Post) Create() fiber.Handler {
|
||||
type Req struct {
|
||||
func (h *Post) Create(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
|
||||
var req struct {
|
||||
Title string `json:"title" form:"title"`
|
||||
Body string `json:"body" form:"body"`
|
||||
}
|
||||
|
||||
return func(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := h.Post.Create(c.UserContext(), site, posts.NewPost{
|
||||
Title: req.Title,
|
||||
Body: req.Body,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := h.Post.Create(c.UserContext(), site, posts.NewPost{
|
||||
Title: req.Title,
|
||||
Body: req.Body,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
}
|
||||
|
||||
func (h *Post) Edit() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
func (h *Post) Edit(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
|
||||
postID, err := c.ParamsInt("postId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
post, err := h.Post.GetPost(c.UserContext(), postID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if post.SiteID != site.ID {
|
||||
return fmt.Errorf("post id %v not equal to site id %v", postID, site.ID)
|
||||
}
|
||||
|
||||
return c.Render("posts/new", fiber.Map{
|
||||
"site": site,
|
||||
"post": post,
|
||||
}, "layouts/site")
|
||||
postID, err := c.ParamsInt("postId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
post, err := h.Post.GetPost(c.UserContext(), postID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if post.SiteID != site.ID {
|
||||
return fmt.Errorf("post id %v not equal to site id %v", postID, site.ID)
|
||||
}
|
||||
|
||||
return c.Render("posts/new", fiber.Map{
|
||||
"post": post,
|
||||
}, "layouts/site")
|
||||
}
|
||||
|
||||
func (h *Post) Update() fiber.Handler {
|
||||
type Req struct {
|
||||
func (h *Post) Update(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
|
||||
postID, err := c.ParamsInt("postId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Title string `json:"title" form:"title"`
|
||||
Body string `json:"body" form:"body"`
|
||||
}
|
||||
|
||||
return func(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
|
||||
postID, err := c.ParamsInt("postId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
post, err := h.Post.GetPost(c.UserContext(), postID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if post.SiteID != site.ID {
|
||||
return fmt.Errorf("post id %v not equal to site id %v", postID, site.ID)
|
||||
}
|
||||
|
||||
post.Title = req.Title
|
||||
post.Body = req.Body
|
||||
|
||||
if err := h.Post.Save(c.UserContext(), site, &post); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
post, err := h.Post.GetPost(c.UserContext(), postID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if post.SiteID != site.ID {
|
||||
return fmt.Errorf("post id %v not equal to site id %v", postID, site.ID)
|
||||
}
|
||||
|
||||
post.Title = req.Title
|
||||
post.Body = req.Body
|
||||
|
||||
if err := h.Post.Save(c.UserContext(), site, &post); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
}
|
||||
|
||||
func (h *Post) Delete(c *fiber.Ctx) error {
|
||||
site := GetSite(c)
|
||||
|
||||
postID, err := c.ParamsInt("postId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.Post.DeletePost(c.UserContext(), site, postID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Select(c,
|
||||
HTMX(func(c *fiber.Ctx) error {
|
||||
return c.Status(http.StatusOK).SendString("")
|
||||
}),
|
||||
Otherwise(func(c *fiber.Ctx) error {
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID), http.StatusSeeOther)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"lmika.dev/lmika/hugo-cms/services/sites"
|
||||
|
@ -12,43 +11,39 @@ type Site struct {
|
|||
Site *sites.Service
|
||||
}
|
||||
|
||||
func (s *Site) Create() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
site, err := s.Site.CreateSite(c.UserContext(), "New Site "+time.Now().Format("2006-01-02 15:04:05"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (s *Site) Create(c *fiber.Ctx) error {
|
||||
user := GetUser(c)
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
site, err := s.Site.CreateSite(c.UserContext(), user, "New Site "+time.Now().Format("2006-01-02 15:04:05"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
}
|
||||
|
||||
func (s *Site) Show() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
id, err := c.ParamsInt("siteId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site, err := s.Site.GetSite(c.UserContext(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("sites/index", fiber.Map{
|
||||
"site": site,
|
||||
}, "layouts/main")
|
||||
func (s *Site) Show(c *fiber.Ctx) error {
|
||||
id, err := c.ParamsInt("siteId")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site, err := s.Site.GetSite(c.UserContext(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("sites/index", fiber.Map{
|
||||
"site": site,
|
||||
}, "layouts/main")
|
||||
}
|
||||
|
||||
func (s *Site) Rebuild() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if err := s.Site.Rebuild(c.UserContext(), GetSite(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", GetSite(c).ID))
|
||||
func (s *Site) Rebuild(c *fiber.Ctx) error {
|
||||
if err := s.Site.Rebuild(c.UserContext(), GetSite(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", GetSite(c).ID))
|
||||
}
|
||||
|
||||
func (s *Site) WithSite() fiber.Handler {
|
||||
|
@ -63,7 +58,7 @@ func (s *Site) WithSite() fiber.Handler {
|
|||
return err
|
||||
}
|
||||
|
||||
c.SetUserContext(context.WithValue(c.UserContext(), siteKey, site))
|
||||
c.Locals("site", site)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
|
66
main.go
66
main.go
|
@ -3,7 +3,10 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/encryptcookie"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/template/html/v2"
|
||||
"github.com/yuin/goldmark"
|
||||
|
@ -20,12 +23,23 @@ import (
|
|||
"lmika.dev/lmika/hugo-cms/services/posts"
|
||||
"lmika.dev/lmika/hugo-cms/services/sitebuilder"
|
||||
"lmika.dev/lmika/hugo-cms/services/sites"
|
||||
"lmika.dev/lmika/hugo-cms/services/users"
|
||||
"lmika.dev/lmika/hugo-cms/templates"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flagGenKey := flag.Bool("gen-key", false, "Generate a new key")
|
||||
flagUser := flag.String("user", "", "add new user")
|
||||
flagPassword := flag.String("password", "", "add new password")
|
||||
flag.Parse()
|
||||
|
||||
if *flagGenKey {
|
||||
fmt.Println(encryptcookie.GenerateKey())
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -37,6 +51,19 @@ func main() {
|
|||
}
|
||||
defer dbp.Close()
|
||||
|
||||
userService := users.NewService(dbp)
|
||||
|
||||
if *flagUser != "" {
|
||||
if _, err := userService.AddUser(context.Background(), users.NewUser{
|
||||
Email: *flagUser,
|
||||
Password: *flagPassword,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("User added")
|
||||
return
|
||||
}
|
||||
|
||||
hugoProvider, err := hugo.New(cfg.StagingDir(), cfg.ScratchDir())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -53,6 +80,7 @@ func main() {
|
|||
|
||||
siteHandlers := handlers.Site{Site: siteService}
|
||||
postHandlers := handlers.Post{Post: postService}
|
||||
authHandlers := handlers.AuthHandler{UserService: userService}
|
||||
|
||||
log.Println("Connected to database")
|
||||
if err := dbp.Migrate(context.Background()); err != nil {
|
||||
|
@ -70,33 +98,41 @@ func main() {
|
|||
return template.HTML(buf.String()), nil
|
||||
}
|
||||
|
||||
if cfg.EncryptedCookieKey == "" {
|
||||
log.Println("No encrypt cookie key defined. Generating random key")
|
||||
cfg.EncryptedCookieKey = encryptcookie.GenerateKey()
|
||||
}
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
Views: tmplEngine,
|
||||
Views: tmplEngine,
|
||||
PassLocalsToViews: true,
|
||||
})
|
||||
app.Use(encryptcookie.New(encryptcookie.Config{Key: cfg.EncryptedCookieKey}))
|
||||
|
||||
app.Use("/assets", filesystem.New(filesystem.Config{Root: http.FS(assets.FS)}))
|
||||
|
||||
app.Get("/auth/login", authHandlers.ShowLogin)
|
||||
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.Post("/sites", siteHandlers.Create())
|
||||
app.Get("/sites/:siteId", siteHandlers.Show())
|
||||
app.Post("/sites", siteHandlers.Create)
|
||||
app.Get("/sites/:siteId", siteHandlers.Show)
|
||||
|
||||
app.Route("/sites/:siteId", func(r fiber.Router) {
|
||||
r.Use(siteHandlers.WithSite())
|
||||
r.Post("/rebuild", siteHandlers.Rebuild())
|
||||
r.Post("/rebuild", siteHandlers.Rebuild)
|
||||
|
||||
r.Get("/posts", postHandlers.Posts())
|
||||
r.Get("/posts/:postId", postHandlers.Edit())
|
||||
r.Get("/posts/new", postHandlers.New())
|
||||
r.Post("/posts", postHandlers.Create())
|
||||
r.Post("/posts/:postId", postHandlers.Update())
|
||||
r.Get("/posts", postHandlers.Posts)
|
||||
r.Get("/posts/new", postHandlers.New)
|
||||
r.Post("/posts", postHandlers.Create)
|
||||
r.Get("/posts/:postId", postHandlers.Edit)
|
||||
r.Post("/posts/:postId", postHandlers.Update)
|
||||
r.Delete("/posts/:postId", postHandlers.Delete)
|
||||
})
|
||||
|
||||
app.Use("/assets", filesystem.New(filesystem.Config{
|
||||
Root: http.FS(assets.FS),
|
||||
}))
|
||||
app.Static("/assets", "./assets")
|
||||
|
||||
jobService.Start()
|
||||
defer jobService.Stop()
|
||||
|
||||
|
|
9
models/cookie.go
Normal file
9
models/cookie.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
const (
|
||||
AuthCookieName = "hugocrm_auth"
|
||||
)
|
||||
|
||||
type AuthCookie struct {
|
||||
UserID int64 `json:"uid"`
|
||||
}
|
|
@ -12,6 +12,7 @@ const (
|
|||
type Post struct {
|
||||
ID int64
|
||||
SiteID int64
|
||||
OwnerID int64
|
||||
Title string
|
||||
Body string
|
||||
State PostState
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package models
|
||||
|
||||
type Site struct {
|
||||
ID int64
|
||||
Name string
|
||||
Title string
|
||||
URL string
|
||||
Theme string
|
||||
ID int64
|
||||
OwnerUserID int64
|
||||
Name string
|
||||
Title string
|
||||
URL string
|
||||
Theme string
|
||||
}
|
||||
|
|
7
models/user.go
Normal file
7
models/user.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package models
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
Email string
|
||||
PasswordHash string
|
||||
}
|
|
@ -27,6 +27,10 @@ func (db *DB) GetPost(ctx context.Context, postID int64) (models.Post, error) {
|
|||
return dbPostToPost(res), nil
|
||||
}
|
||||
|
||||
func (db *DB) DeletePost(ctx context.Context, postID int64) error {
|
||||
return db.q.DeletePost(ctx, postID)
|
||||
}
|
||||
|
||||
func (db *DB) ListPublishablePosts(ctx context.Context, fromID, siteID int64, now time.Time) ([]models.Post, error) {
|
||||
res, err := db.q.ListPublishablePosts(ctx, dbq.ListPublishablePostsParams{
|
||||
ID: fromID,
|
||||
|
|
|
@ -8,11 +8,12 @@ import (
|
|||
|
||||
func (db *DB) InsertSite(ctx context.Context, site *models.Site) error {
|
||||
id, err := db.q.NewSite(ctx, dbq.NewSiteParams{
|
||||
Name: site.Name,
|
||||
Title: site.Title,
|
||||
Url: site.URL,
|
||||
Theme: site.Theme,
|
||||
Props: []byte("{}"),
|
||||
Name: site.Name,
|
||||
Title: site.Title,
|
||||
OwnerUserID: site.OwnerUserID,
|
||||
Url: site.URL,
|
||||
Theme: site.Theme,
|
||||
Props: []byte("{}"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -31,6 +32,7 @@ func (db *DB) GetSite(ctx context.Context, id int64) (models.Site, error) {
|
|||
ID: site.ID,
|
||||
Name: site.Name,
|
||||
Title: site.Title,
|
||||
URL: site.Url,
|
||||
Theme: site.Theme,
|
||||
}, nil
|
||||
}
|
||||
|
|
45
providers/db/user.go
Normal file
45
providers/db/user.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"lmika.dev/lmika/hugo-cms/gen/sqlc/dbq"
|
||||
"lmika.dev/lmika/hugo-cms/models"
|
||||
)
|
||||
|
||||
func (db *DB) AddUser(ctx context.Context, user *models.User) error {
|
||||
id, err := db.q.AddUser(ctx, dbq.AddUserParams{
|
||||
Email: user.Email,
|
||||
Password: user.PasswordHash,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetUserByID(ctx context.Context, id int64) (models.User, error) {
|
||||
res, err := db.q.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
return dbUserToUser(res), nil
|
||||
}
|
||||
|
||||
func (db *DB) GetUserByEmail(ctx context.Context, email string) (models.User, error) {
|
||||
res, err := db.q.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
return dbUserToUser(res), nil
|
||||
}
|
||||
|
||||
func dbUserToUser(u dbq.User) models.User {
|
||||
return models.User{
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
PasswordHash: u.Password,
|
||||
}
|
||||
}
|
|
@ -40,6 +40,19 @@ func (s *Service) GetPost(ctx context.Context, id int) (models.Post, error) {
|
|||
return post, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeletePost(ctx context.Context, site models.Site, id int) error {
|
||||
post, err := s.db.GetPost(ctx, int64(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.db.DeletePost(ctx, int64(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.jobs.Queue(ctx, s.sb.DeletePost(site, post))
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, site models.Site, req NewPost) (models.Post, error) {
|
||||
post := models.Post{
|
||||
SiteID: site.ID,
|
||||
|
|
|
@ -47,13 +47,41 @@ func (s *Service) WriteAllPosts(site models.Site) models.Job {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) DeletePost(site models.Site, post models.Post) models.Job {
|
||||
return models.Jobs(
|
||||
models.Job{
|
||||
Do: func(ctx context.Context) error {
|
||||
themeMeta, ok := s.themes.Lookup(site.Theme)
|
||||
if !ok {
|
||||
return errors.New("theme not found")
|
||||
}
|
||||
|
||||
postFilename := s.postFilename(site, themeMeta, post)
|
||||
|
||||
if _, err := os.Stat(postFilename); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if os.Remove(postFilename) != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
s.Publish(site),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) writePost(site models.Site, post models.Post) error {
|
||||
themeMeta, ok := s.themes.Lookup(site.Theme)
|
||||
if !ok {
|
||||
return errors.New("theme not found")
|
||||
}
|
||||
|
||||
postFilename := filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.PostDir, post.CreatedAt.Format("2006-01-02-150405.md"))
|
||||
postFilename := s.postFilename(site, themeMeta, post)
|
||||
|
||||
log.Printf(" .. post %v", postFilename)
|
||||
|
||||
|
@ -96,3 +124,7 @@ func (s *Service) writePost(site models.Site, post models.Post) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) postFilename(site models.Site, themeMeta models.ThemeMeta, post models.Post) string {
|
||||
return filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.PostDir, post.CreatedAt.Format("2006-01-02-150405.md"))
|
||||
}
|
||||
|
|
|
@ -41,11 +41,13 @@ func (s *Service) GetSite(ctx context.Context, id int) (models.Site, error) {
|
|||
return s.db.GetSite(ctx, int64(id))
|
||||
}
|
||||
|
||||
func (s *Service) CreateSite(ctx context.Context, name string) (models.Site, error) {
|
||||
func (s *Service) CreateSite(ctx context.Context, user models.User, name string) (models.Site, error) {
|
||||
newSite := models.Site{
|
||||
Name: normaliseName(name),
|
||||
Title: name,
|
||||
Theme: "bear",
|
||||
Name: normaliseName(name),
|
||||
OwnerUserID: user.ID,
|
||||
Title: name,
|
||||
Theme: "bear",
|
||||
URL: "https://meek-meringue-060cfc.netlify.app/",
|
||||
}
|
||||
|
||||
_, ok := s.themes.Lookup(newSite.Theme)
|
||||
|
@ -62,7 +64,7 @@ func (s *Service) CreateSite(ctx context.Context, name string) (models.Site, err
|
|||
SiteID: newSite.ID,
|
||||
Role: models.TargetRoleProduction,
|
||||
Type: models.TargetTypeNetlify,
|
||||
URL: "https://meek-meringue-060cfc.netlify.app",
|
||||
URL: "https://meek-meringue-060cfc.netlify.app/",
|
||||
TargetRef: "e628dc6e-e6e1-45a9-847a-982adef940a8",
|
||||
}); err != nil {
|
||||
return models.Site{}, err
|
||||
|
|
76
services/users/service.go
Normal file
76
services/users/service.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"lmika.dev/lmika/hugo-cms/models"
|
||||
"lmika.dev/lmika/hugo-cms/providers/db"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
dbp *db.DB
|
||||
}
|
||||
|
||||
func NewService(dbp *db.DB) *Service {
|
||||
return &Service{
|
||||
dbp: dbp,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) AddUser(ctx context.Context, newUser NewUser) (models.User, error) {
|
||||
passwd, err := bcrypt.GenerateFromPassword([]byte(newUser.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Email: newUser.Email,
|
||||
PasswordHash: base64.StdEncoding.EncodeToString(passwd),
|
||||
}
|
||||
if err := s.dbp.AddUser(ctx, &user); err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUserByID(ctx context.Context, id int64) (models.User, error) {
|
||||
return s.dbp.GetUserByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) VerifyLogin(ctx context.Context, email string, password string) (models.User, error) {
|
||||
user, err := s.dbp.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
log.Println("User not found")
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
pwdHash, err := base64.StdEncoding.DecodeString(user.PasswordHash)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(pwdHash, []byte(password))
|
||||
if err != nil {
|
||||
log.Println("Password incorrect")
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUserByEmail(ctx context.Context, email string) (models.User, error) {
|
||||
user, err := s.dbp.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
type NewUser struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
-- name: ListPosts :many
|
||||
SELECT * FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25;
|
||||
SELECT * FROM posts WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25;
|
||||
|
||||
-- name: GetPostWithID :one
|
||||
SELECT * FROM post WHERE id = $1 LIMIT 1;
|
||||
SELECT * FROM posts WHERE id = $1 LIMIT 1;
|
||||
|
||||
-- name: ListPublishablePosts :many
|
||||
SELECT *
|
||||
FROM post
|
||||
FROM posts
|
||||
WHERE id > $1 AND site_id = $2 AND state = 'published' AND post_date <= $3
|
||||
ORDER BY id LIMIT 100;
|
||||
|
||||
-- name: InsertPost :one
|
||||
INSERT INTO post (
|
||||
INSERT INTO posts (
|
||||
site_id,
|
||||
title,
|
||||
body,
|
||||
|
@ -23,7 +23,7 @@ INSERT INTO post (
|
|||
RETURNING id;
|
||||
|
||||
-- name: UpdatePost :exec
|
||||
UPDATE post SET
|
||||
UPDATE posts SET
|
||||
site_id = $2,
|
||||
title = $3,
|
||||
body = $4,
|
||||
|
@ -31,4 +31,7 @@ UPDATE post SET
|
|||
props = $6,
|
||||
post_date = $7
|
||||
-- updated_at = $7
|
||||
WHERE id = $1;
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: DeletePost :exec
|
||||
DELETE FROM posts WHERE id = $1;
|
|
@ -1,15 +1,16 @@
|
|||
-- name: ListSites :one
|
||||
SELECT * FROM site;
|
||||
SELECT * FROM sites;
|
||||
|
||||
-- name: GetSiteWithID :one
|
||||
SELECT * FROM site WHERE id = $1 LIMIT 1;
|
||||
SELECT * FROM sites WHERE id = $1 LIMIT 1;
|
||||
|
||||
-- name: NewSite :one
|
||||
INSERT INTO site (
|
||||
INSERT INTO sites (
|
||||
name,
|
||||
owner_user_id,
|
||||
title,
|
||||
url,
|
||||
theme,
|
||||
props
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id;
|
|
@ -1,8 +1,8 @@
|
|||
-- name: ListPublishTargetsOfRole :many
|
||||
SELECT * FROM publish_target WHERE site_id = $1 AND role = 'production';
|
||||
SELECT * FROM publish_targets WHERE site_id = $1 AND role = 'production';
|
||||
|
||||
-- name: InsertPublishTarget :one
|
||||
INSERT INTO publish_target (
|
||||
INSERT INTO publish_targets (
|
||||
site_id,
|
||||
role,
|
||||
target_type,
|
||||
|
|
13
sql/queries/users.sql
Normal file
13
sql/queries/users.sql
Normal file
|
@ -0,0 +1,13 @@
|
|||
-- name: AddUser :one
|
||||
INSERT INTO users (
|
||||
email,
|
||||
password
|
||||
) VALUES ($1, $2)
|
||||
ON CONFLICT (email) DO UPDATE SET password = $2
|
||||
RETURNING id;
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT * FROM users WHERE id = $1 LIMIT 1;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT * FROM users WHERE email = $1 LIMIT 1;
|
|
@ -11,16 +11,25 @@ CREATE TYPE target_type AS ENUM (
|
|||
'netlify'
|
||||
);
|
||||
|
||||
CREATE TABLE site (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
theme TEXT NOT NULL,
|
||||
props JSON NOT NULL
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE post (
|
||||
CREATE TABLE sites (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
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,
|
||||
|
||||
FOREIGN KEY (owner_user_id) REFERENCES users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE posts (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
site_id BIGINT NOT NULL,
|
||||
title TEXT,
|
||||
|
@ -30,10 +39,10 @@ CREATE TABLE post (
|
|||
post_date TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
|
||||
FOREIGN KEY (site_id) REFERENCES site (id)
|
||||
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE publish_target (
|
||||
CREATE TABLE publish_targets (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
site_id BIGINT NOT NULL,
|
||||
role target_role NOT NULL,
|
||||
|
@ -41,5 +50,5 @@ CREATE TABLE publish_target (
|
|||
url TEXT NOT NULL,
|
||||
target_ref TEXT NOT NULL,
|
||||
|
||||
FOREIGN KEY (site_id) REFERENCES site (id)
|
||||
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
|
||||
);
|
11
templates/auth/login.html
Normal file
11
templates/auth/login.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<form method="post" action="/auth/login" class="post-form">
|
||||
<p>
|
||||
<input name="email" value="" placeholder="Email">
|
||||
</p>
|
||||
<p>
|
||||
<input name="password" type="password" value="" placeholder="Password">
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" value="Login">
|
||||
</p>
|
||||
</form>
|
|
@ -3,6 +3,7 @@ package templates
|
|||
import "embed"
|
||||
|
||||
//go:embed *.html
|
||||
//go:embed auth/*.html
|
||||
//go:embed layouts/*.html
|
||||
//go:embed posts/*.html
|
||||
var FS embed.FS
|
||||
|
|
13
templates/layouts/login.html
Normal file
13
templates/layouts/login.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/assets/css/reset.css">
|
||||
<link rel="stylesheet" href="/assets/css/main.css">
|
||||
<title>Hugo CRM</title>
|
||||
</head>
|
||||
<body>
|
||||
{{embed}}
|
||||
</body>
|
||||
</html>
|
|
@ -6,6 +6,7 @@
|
|||
<link rel="stylesheet" href="/assets/css/reset.css">
|
||||
<link rel="stylesheet" href="/assets/css/main.css">
|
||||
<title>Hugo CRM</title>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
</head>
|
||||
<body>
|
||||
{{embed}}
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
<link rel="stylesheet" href="/assets/css/reset.css">
|
||||
<link rel="stylesheet" href="/assets/css/main.css">
|
||||
<title>Hugo CMS</title>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
</head>
|
||||
<body class="role-site">
|
||||
<header>
|
||||
<h1>Hugo CMS</h1>
|
||||
<nav>
|
||||
<span>{{.site.Name}}</span>
|
||||
<a href="{{.site.URL}}">Visit</a>
|
||||
<a href="#">User</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
@ -3,18 +3,18 @@
|
|||
</div>
|
||||
|
||||
{{range .posts}}
|
||||
{{if .Title}}
|
||||
<h3>{{.Title}}</h3>
|
||||
{{end}}
|
||||
|
||||
<div class="post">
|
||||
{{if .Title}}
|
||||
<h3>{{.Title}}</h3>
|
||||
{{end}}
|
||||
|
||||
{{.Body | markdown}}
|
||||
|
||||
<div>
|
||||
<a href="/sites/{{$.site.ID}}/posts/{{.ID}}">Edit</a>
|
||||
<a href="/sites/{{$.site.ID}}/posts/{{.ID}}">Edit</a> |
|
||||
<a hx-delete="/sites/{{$.site.ID}}/posts/{{.ID}}" hx-confirm="Delete post?" hx-target="closest .post" href="#">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
{{else}}
|
||||
<p>No posts yet</p>
|
||||
{{end}}
|
|
@ -1,5 +1,5 @@
|
|||
{{- $postTarget := printf "/sites/%v/posts" .site.ID -}}
|
||||
{{- if .post.ID -}}
|
||||
{{- if (ne .post.ID 0) -}}
|
||||
{{- $postTarget = printf "/sites/%v/posts/%v" .site.ID .post.ID -}}
|
||||
{{- end -}}
|
||||
<form method="post" action="{{$postTarget}}" class="post-form">
|
||||
|
|
Loading…
Reference in a new issue