Have got first run working and publishing to Netlify
This commit is contained in:
parent
b7e0269e9d
commit
30d99eeb9e
66
assets/js/controllers/firstrun.js
Normal file
66
assets/js/controllers/firstrun.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import { showToast } from "../services/toast";
|
||||
|
||||
export default class FirstRunController extends Controller {
|
||||
static targets = ['pages'];
|
||||
|
||||
connect() {
|
||||
this.pagesTargets.forEach((x) => x.classList.add('d-none'));
|
||||
this.pagesTargets[0].classList.remove('d-none');
|
||||
this.element.querySelector('input[name="username"]').focus();
|
||||
}
|
||||
|
||||
nextPage(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
const currentIndex = this.pagesTargets.findIndex(x => !x.classList.contains('d-none'));
|
||||
if (currentIndex === -1) {
|
||||
return;
|
||||
}
|
||||
if (!this._validate(currentIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextPage = currentIndex + 1;
|
||||
if (nextPage >= this.pagesTargets.length) {
|
||||
this.element.querySelector('form').submit();
|
||||
return;
|
||||
}
|
||||
|
||||
this.pagesTargets[currentIndex].classList.add('d-none');
|
||||
this.pagesTargets[nextPage].classList.remove('d-none');
|
||||
|
||||
if (nextPage === 1) {
|
||||
this.element.querySelector('input[name="siteName"]').focus();
|
||||
}
|
||||
}
|
||||
|
||||
_validate(pageNumber) {
|
||||
let newUsername = this.element.querySelector('input[name="username"]');
|
||||
let newPassword1 = this.element.querySelector('input[name="password1"]');
|
||||
let newPassword2 = this.element.querySelector('input[name="password2"]');
|
||||
|
||||
if (newUsername.value === '') {
|
||||
alert('Please enter a username');
|
||||
newUsername.focus();
|
||||
return false;
|
||||
}
|
||||
if (!newUsername.value.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
alert('Please enter a username with letters, numbers, underscores, and dashes only');
|
||||
newUsername.focus();
|
||||
newUsername.select();
|
||||
return false;
|
||||
}
|
||||
if (newPassword1.value === '') {
|
||||
alert('Please enter a password');
|
||||
newPassword1.focus();
|
||||
return false;
|
||||
}
|
||||
if (newPassword2.value !== newPassword1.value) {
|
||||
alert('Passwords do not match');
|
||||
newPassword2.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,11 @@ import ToastController from "./controllers/toast";
|
|||
import PostlistController from "./controllers/postlist";
|
||||
import PosteditController from "./controllers/postedit";
|
||||
import LogoutController from "./controllers/logout";
|
||||
import FirstRunController from "./controllers/firstrun";
|
||||
|
||||
window.Stimulus = Application.start()
|
||||
Stimulus.register("toast", ToastController);
|
||||
Stimulus.register("postlist", PostlistController);
|
||||
Stimulus.register("postedit", PosteditController);
|
||||
Stimulus.register("logout", LogoutController);
|
||||
Stimulus.register("first-run", FirstRunController);
|
||||
1
go.mod
1
go.mod
|
|
@ -37,6 +37,7 @@ require (
|
|||
github.com/go-openapi/strfmt v0.19.11 // indirect
|
||||
github.com/go-openapi/swag v0.19.12 // indirect
|
||||
github.com/go-openapi/validate v0.20.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/gofiber/fiber/v3 v3.1.0 // indirect
|
||||
github.com/gofiber/schema v1.7.0 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -178,6 +178,8 @@ github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0
|
|||
github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI=
|
||||
github.com/go-openapi/validate v0.20.0 h1:pzutNCCBZGZlE+u8HD3JZyWdc/TVbtVwlWUp8/vgUKk=
|
||||
github.com/go-openapi/validate v0.20.0/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
|
|
|
|||
77
handlers/index.go
Normal file
77
handlers/index.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/session"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/services/sites"
|
||||
)
|
||||
|
||||
var sitePath = regexp.MustCompile(`^/sites/([0-9]+)`)
|
||||
|
||||
type IndexHandler struct {
|
||||
SiteService *sites.Service
|
||||
}
|
||||
|
||||
func (h IndexHandler) Index(c fiber.Ctx) error {
|
||||
hasUserAndSites, err := h.SiteService.HasUsersAsSites(c.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !hasUserAndSites {
|
||||
return c.Redirect().To("/first-run")
|
||||
}
|
||||
|
||||
user, hasUser := models.GetUser(c.Context())
|
||||
if !hasUser {
|
||||
return c.Redirect().To("/login")
|
||||
}
|
||||
|
||||
if refUrl, rerr := url.Parse(c.Get("Referer")); rerr == nil {
|
||||
if parts := sitePath.FindStringSubmatch(refUrl.Path); len(parts) == 2 {
|
||||
return c.Redirect().To(fmt.Sprintf("/sites/%v/posts", parts[1]))
|
||||
}
|
||||
}
|
||||
|
||||
site, err := h.SiteService.BestSite(c.Context(), user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect().To(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
}
|
||||
|
||||
func (h IndexHandler) FirstRun(c fiber.Ctx) error {
|
||||
hasUserAndSites, err := h.SiteService.HasUsersAsSites(c.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
} else if hasUserAndSites {
|
||||
return errors.New("you already have a site")
|
||||
}
|
||||
|
||||
return c.Render("index/first-run", fiber.Map{}, "layouts/bare_with_scripts")
|
||||
}
|
||||
|
||||
func (h IndexHandler) FirstRunSubmit(c fiber.Ctx) error {
|
||||
var req sites.FirstRunRequest
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return errors.Wrap(err, "failed to parse first run request")
|
||||
}
|
||||
|
||||
sess := session.FromContext(c)
|
||||
if err := sess.Regenerate(); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Failed to login")
|
||||
}
|
||||
|
||||
user, site, err := h.SiteService.FirstRun(c.Context(), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess.Set("user_id", user.ID)
|
||||
return c.Redirect().To(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ func (lh *LoginHandler) DoLogin(c fiber.Ctx) error {
|
|||
}
|
||||
|
||||
sess.Set("user_id", user.ID)
|
||||
sess.Delete("_login_challenge")
|
||||
|
||||
return c.Redirect().To("/")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,27 @@ import (
|
|||
"lmika.dev/lmika/weiro/services/auth"
|
||||
)
|
||||
|
||||
func AuthUser(auth *auth.Service) func(c fiber.Ctx) error {
|
||||
func OptionalUser(auth *auth.Service) func(c fiber.Ctx) error {
|
||||
return func(c fiber.Ctx) error {
|
||||
sess := session.FromContext(c)
|
||||
userID, _ := sess.Get("user_id").(int64)
|
||||
if userID == 0 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
user, err := auth.GetUser(c.Context(), userID)
|
||||
if err != nil {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
c.Locals("user", user)
|
||||
c.SetContext(models.WithUser(c.Context(), user))
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RequireUser(auth *auth.Service) func(c fiber.Ctx) error {
|
||||
return func(c fiber.Ctx) error {
|
||||
sess := session.FromContext(c)
|
||||
userID, _ := sess.Get("user_id").(int64)
|
||||
|
|
|
|||
12
main.go
12
main.go
|
|
@ -26,6 +26,7 @@ import (
|
|||
"lmika.dev/lmika/weiro/services/auth"
|
||||
"lmika.dev/lmika/weiro/services/posts"
|
||||
"lmika.dev/lmika/weiro/services/publisher"
|
||||
"lmika.dev/lmika/weiro/services/sites"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ func main() {
|
|||
publisherSvc := publisher.New(dbp)
|
||||
publisherQueue := publisher.NewQueue(publisherSvc)
|
||||
postService := posts.New(dbp, publisherQueue)
|
||||
siteService := sites.New(dbp)
|
||||
|
||||
// CLI tools
|
||||
if *flagPasswd != "" && *flagUser != "" {
|
||||
|
|
@ -120,6 +122,7 @@ func main() {
|
|||
},
|
||||
}))
|
||||
|
||||
ih := handlers.IndexHandler{SiteService: siteService}
|
||||
lh := handlers.LoginHandler{Config: cfg, AuthService: authSvc}
|
||||
ph := handlers.PostsHandler{PostService: postService}
|
||||
|
||||
|
|
@ -127,7 +130,7 @@ func main() {
|
|||
app.Post("/login", lh.DoLogin)
|
||||
app.Post("/logout", lh.Logout)
|
||||
|
||||
siteGroup := app.Group("/sites/:siteID", middleware.AuthUser(authSvc), middleware.RequiresSite(dbp))
|
||||
siteGroup := app.Group("/sites/:siteID", middleware.RequireUser(authSvc), middleware.RequiresSite(dbp))
|
||||
|
||||
siteGroup.Get("/posts", ph.Index)
|
||||
siteGroup.Get("/posts/new", ph.New)
|
||||
|
|
@ -136,9 +139,10 @@ func main() {
|
|||
siteGroup.Patch("/posts/:postID", ph.Patch)
|
||||
siteGroup.Delete("/posts/:postID", ph.Delete)
|
||||
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
return c.Redirect().To("/sites/1/posts")
|
||||
})
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type PublishTargetType int
|
||||
|
||||
const (
|
||||
|
|
@ -11,10 +13,10 @@ const (
|
|||
type Site struct {
|
||||
ID int64
|
||||
OwnerID int64
|
||||
Created time.Time
|
||||
|
||||
Title string
|
||||
Tagline string
|
||||
//Meta SiteMeta
|
||||
//Posts []*Post
|
||||
}
|
||||
|
||||
type SitePublishTarget struct {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var ValidUserName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
Username string
|
||||
PasswordHashed []byte
|
||||
TimeZone string
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
func (u *User) SetPassword(pwd string) {
|
||||
|
|
|
|||
|
|
@ -33,10 +33,12 @@ type Site struct {
|
|||
OwnerID int64
|
||||
Title string
|
||||
Tagline string
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
Username string
|
||||
Password string
|
||||
CreatedAt int64
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,27 @@ package sqlgen
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const hasUsersAndSites = `-- name: HasUsersAndSites :one
|
||||
SELECT (SELECT COUNT(*) FROM users) > 0 AND (SELECT COUNT(*) FROM sites) > 0 AS has_users_and_sites
|
||||
`
|
||||
|
||||
func (q *Queries) HasUsersAndSites(ctx context.Context) (sql.NullBool, error) {
|
||||
row := q.db.QueryRowContext(ctx, hasUsersAndSites)
|
||||
var has_users_and_sites sql.NullBool
|
||||
err := row.Scan(&has_users_and_sites)
|
||||
return has_users_and_sites, err
|
||||
}
|
||||
|
||||
const insertSite = `-- name: InsertSite :one
|
||||
INSERT INTO sites (
|
||||
owner_id,
|
||||
title,
|
||||
tagline
|
||||
) VALUES (?, ?, ?)
|
||||
tagline,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
|
|
@ -22,17 +35,23 @@ type InsertSiteParams struct {
|
|||
OwnerID int64
|
||||
Title string
|
||||
Tagline string
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
func (q *Queries) InsertSite(ctx context.Context, arg InsertSiteParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertSite, arg.OwnerID, arg.Title, arg.Tagline)
|
||||
row := q.db.QueryRowContext(ctx, insertSite,
|
||||
arg.OwnerID,
|
||||
arg.Title,
|
||||
arg.Tagline,
|
||||
arg.CreatedAt,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const selectSiteByID = `-- name: SelectSiteByID :one
|
||||
SELECT id, owner_id, title, tagline FROM sites WHERE id = ?
|
||||
SELECT id, owner_id, title, tagline, created_at FROM sites WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) SelectSiteByID(ctx context.Context, id int64) (Site, error) {
|
||||
|
|
@ -43,12 +62,13 @@ func (q *Queries) SelectSiteByID(ctx context.Context, id int64) (Site, error) {
|
|||
&i.OwnerID,
|
||||
&i.Title,
|
||||
&i.Tagline,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const selectSitesOwnedByUser = `-- name: SelectSitesOwnedByUser :many
|
||||
SELECT id, owner_id, title, tagline FROM sites WHERE owner_id = ?
|
||||
SELECT id, owner_id, title, tagline, created_at FROM sites WHERE owner_id = ? ORDER BY title ASC
|
||||
`
|
||||
|
||||
func (q *Queries) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([]Site, error) {
|
||||
|
|
@ -65,6 +85,7 @@ func (q *Queries) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([]
|
|||
&i.OwnerID,
|
||||
&i.Title,
|
||||
&i.Tagline,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,40 +10,51 @@ import (
|
|||
)
|
||||
|
||||
const insertUser = `-- name: InsertUser :one
|
||||
INSERT INTO users (username, password) VALUES (?, ?) RETURNING id
|
||||
INSERT INTO users (username, password, created_at) VALUES (?, ?, ?) RETURNING id
|
||||
`
|
||||
|
||||
type InsertUserParams struct {
|
||||
Username string
|
||||
Password string
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertUser, arg.Username, arg.Password)
|
||||
row := q.db.QueryRowContext(ctx, insertUser, arg.Username, arg.Password, arg.CreatedAt)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const selectUserByID = `-- name: SelectUserByID :one
|
||||
SELECT id, username, password FROM users WHERE id = ? LIMIT 1
|
||||
SELECT id, username, password, created_at FROM users WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) SelectUserByID(ctx context.Context, id int64) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, selectUserByID, id)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Username, &i.Password)
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Password,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const selectUserByUsername = `-- name: SelectUserByUsername :one
|
||||
SELECT id, username, password FROM users WHERE username = ? LIMIT 1
|
||||
SELECT id, username, password, created_at FROM users WHERE username = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) SelectUserByUsername(ctx context.Context, username string) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, selectUserByUsername, username)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Username, &i.Password)
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Password,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package db
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/providers/db/gen/sqlgen"
|
||||
|
|
@ -18,6 +19,7 @@ func (db *Provider) SelectSiteByID(ctx context.Context, id int64) (models.Site,
|
|||
OwnerID: row.OwnerID,
|
||||
Title: row.Title,
|
||||
Tagline: row.Tagline,
|
||||
Created: time.Unix(row.CreatedAt, 0).UTC(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +36,7 @@ func (db *Provider) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) (
|
|||
OwnerID: row.OwnerID,
|
||||
Title: row.Title,
|
||||
Tagline: row.Tagline,
|
||||
Created: time.Unix(row.CreatedAt, 0).UTC(),
|
||||
}
|
||||
}
|
||||
return sites, nil
|
||||
|
|
@ -45,6 +48,7 @@ func (db *Provider) SaveSite(ctx context.Context, site *models.Site) error {
|
|||
OwnerID: site.OwnerID,
|
||||
Title: site.Title,
|
||||
Tagline: site.Tagline,
|
||||
CreatedAt: timeToInt(site.Created),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -56,3 +60,11 @@ func (db *Provider) SaveSite(ctx context.Context, site *models.Site) error {
|
|||
// No update query defined in sqlgen yet
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Provider) HasUsersAndSites(ctx context.Context) (bool, error) {
|
||||
nullBool, err := db.queries.HasUsersAndSites(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return nullBool.Valid && nullBool.Bool, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package db
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/providers/db/gen/sqlgen"
|
||||
|
|
@ -33,6 +34,7 @@ func (db *Provider) SaveUser(ctx context.Context, user *models.User) error {
|
|||
newID, err := db.queries.InsertUser(ctx, sqlgen.InsertUserParams{
|
||||
Username: user.Username,
|
||||
Password: hashedPassword,
|
||||
CreatedAt: timeToInt(user.Created),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -58,5 +60,6 @@ func dbUserToUser(res sqlgen.User) (models.User, error) {
|
|||
ID: res.ID,
|
||||
Username: res.Username,
|
||||
PasswordHashed: pwdBytes,
|
||||
Created: time.Unix(res.CreatedAt, 0).UTC(),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
103
services/sites/services.go
Normal file
103
services/sites/services.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package sites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-ozzo/ozzo-validation/v4"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/providers/db"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *db.Provider
|
||||
}
|
||||
|
||||
func New(dbp *db.Provider) *Service {
|
||||
return &Service{
|
||||
db: dbp,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) HasUsersAsSites(ctx context.Context) (bool, error) {
|
||||
return s.db.HasUsersAndSites(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) BestSite(ctx context.Context, user models.User) (models.Site, error) {
|
||||
sites, err := s.db.SelectSitesOwnedByUser(ctx, user.ID)
|
||||
if err != nil {
|
||||
return models.Site{}, err
|
||||
} else if len(sites) == 0 {
|
||||
return models.Site{}, errors.New("no sites found")
|
||||
}
|
||||
|
||||
return sites[0], nil
|
||||
}
|
||||
|
||||
type FirstRunRequest struct {
|
||||
Username string `form:"username"`
|
||||
Password1 string `form:"password1"`
|
||||
Password2 string `form:"password2"`
|
||||
SiteName string `form:"siteName"`
|
||||
SiteURL string `form:"siteUrl"`
|
||||
NetlifySiteID string `form:"netlifySiteId"`
|
||||
NetlifyAPIKey string `form:"netlifyAPIToken"`
|
||||
}
|
||||
|
||||
func (frr FirstRunRequest) Validate() error {
|
||||
return validation.ValidateStruct(&frr,
|
||||
validation.Field(&frr.Username, validation.Required, validation.Match(models.ValidUserName)),
|
||||
validation.Field(&frr.Password1, validation.Required),
|
||||
validation.Field(&frr.Password2, validation.Required, validation.By(stringEquals(frr.Password1))),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) FirstRun(ctx context.Context, req FirstRunRequest) (newUser models.User, newSite models.Site, _ error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return newUser, newSite, err
|
||||
}
|
||||
|
||||
hasSite, err := s.db.HasUsersAndSites(ctx)
|
||||
if err != nil {
|
||||
return newUser, newSite, err
|
||||
} else if hasSite {
|
||||
return newUser, newSite, errors.New("user and site already exists")
|
||||
}
|
||||
|
||||
newUser = models.User{
|
||||
Username: req.Username,
|
||||
TimeZone: "UTC",
|
||||
Created: time.Now(),
|
||||
}
|
||||
newUser.SetPassword(req.Password1)
|
||||
if err := s.db.SaveUser(ctx, &newUser); err != nil {
|
||||
return newUser, newSite, err
|
||||
}
|
||||
|
||||
newSite = models.Site{
|
||||
Title: defaultIfEmpty(req.SiteName, "New Site"),
|
||||
OwnerID: newUser.ID,
|
||||
Created: time.Now(),
|
||||
}
|
||||
if err := s.db.SaveSite(ctx, &newSite); err != nil {
|
||||
return newUser, newSite, err
|
||||
}
|
||||
|
||||
hasNetlifyConfig := req.SiteURL != "" && req.NetlifySiteID != "" && req.NetlifyAPIKey != ""
|
||||
if hasNetlifyConfig {
|
||||
target := models.SitePublishTarget{
|
||||
SiteID: newSite.ID,
|
||||
Enabled: true,
|
||||
BaseURL: req.SiteURL,
|
||||
TargetType: "netlify",
|
||||
TargetRef: req.NetlifySiteID,
|
||||
TargetKey: req.NetlifyAPIKey,
|
||||
}
|
||||
if err := s.db.SavePublishTarget(ctx, &target); err != nil {
|
||||
return newUser, newSite, err
|
||||
}
|
||||
}
|
||||
|
||||
return newUser, newSite, nil
|
||||
}
|
||||
23
services/sites/utils.go
Normal file
23
services/sites/utils.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package sites
|
||||
|
||||
import (
|
||||
"emperror.dev/errors"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
func stringEquals(str string) validation.RuleFunc {
|
||||
return func(value interface{}) error {
|
||||
s, _ := value.(string)
|
||||
if s != str {
|
||||
return errors.New("unexpected string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func defaultIfEmpty(value string, defaultValue string) string {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
-- name: SelectSitesOwnedByUser :many
|
||||
SELECT * FROM sites WHERE owner_id = ?;
|
||||
SELECT * FROM sites WHERE owner_id = ? ORDER BY title ASC;
|
||||
|
||||
-- name: SelectSiteByID :one
|
||||
SELECT * FROM sites WHERE id = ?;
|
||||
|
|
@ -8,6 +8,10 @@ SELECT * FROM sites WHERE id = ?;
|
|||
INSERT INTO sites (
|
||||
owner_id,
|
||||
title,
|
||||
tagline
|
||||
) VALUES (?, ?, ?)
|
||||
tagline,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
RETURNING id;
|
||||
|
||||
-- name: HasUsersAndSites :one
|
||||
SELECT (SELECT COUNT(*) FROM users) > 0 AND (SELECT COUNT(*) FROM sites) > 0 AS has_users_and_sites;
|
||||
|
|
@ -5,7 +5,7 @@ SELECT * FROM users WHERE username = ? LIMIT 1;
|
|||
SELECT * FROM users WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: InsertUser :one
|
||||
INSERT INTO users (username, password) VALUES (?, ?) RETURNING id;
|
||||
INSERT INTO users (username, password, created_at) VALUES (?, ?, ?) RETURNING id;
|
||||
|
||||
-- name: UpdateUser :exec
|
||||
UPDATE users SET username = ?, password = ? WHERE id = ?;
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
password TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_users_username ON users (username);
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ CREATE TABLE sites (
|
|||
owner_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
tagline TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
|
|
|||
52
views/index/first-run.html
Normal file
52
views/index/first-run.html
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<div class="mx-auto p-2" style="width: 400px; margin-block-start: 50px;" data-controller="first-run">
|
||||
<div class="text-center mb-4">
|
||||
<h4>Welcome to</h4>
|
||||
<h1>Weiro</h1>
|
||||
</div>
|
||||
<form action="/first-run" method="post">
|
||||
<div data-first-run-target="pages needs-validation">
|
||||
<div class="text-center mb-4">
|
||||
<p>Please enter the username and password you'd like to use to login.</p>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" name="username" id="username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password1" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" name="password1" id="password1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password2" class="form-label">Re-enter password</label>
|
||||
<input type="password" class="form-control" name="password2" id="password2">
|
||||
</div>
|
||||
<div class="mb-3 text-end">
|
||||
<button class="btn btn-primary" value="Next" data-action="click->first-run#nextPage">Next »</button>
|
||||
</div>
|
||||
</div>
|
||||
<div data-first-run-target="pages">
|
||||
<div class="text-center mb-4">
|
||||
<p>Enter the details of your blog, if you know them.<br>All fields are optional, and can be changed later.</p>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="siteName" class="form-label">Site Name</label>
|
||||
<input type="text" class="form-control" name="siteName" id="siteName">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="siteUrl" class="form-label">Site URL</label>
|
||||
<input type="text" class="form-control" name="siteUrl" id="siteUrl">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="netlifySiteId" class="form-label">Netlify Site ID</label>
|
||||
<input type="text" class="form-control" name="netlifySiteId" id="netlifySiteId">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="netlifyAPIToken" class="form-label">Netlify API Token</label>
|
||||
<input type="text" class="form-control" name="netlifyAPIToken" id="netlifyAPIToken">
|
||||
</div>
|
||||
<div class="mb-3 text-end">
|
||||
<input type="submit" class="btn btn-primary" value="Finish">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
13
views/layouts/bare_with_scripts.html
Normal file
13
views/layouts/bare_with_scripts.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Weiro</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/static/assets/main.css">
|
||||
<script src="/static/assets/main.js" type="module"></script>
|
||||
</head>
|
||||
<body class="min-vh-100 d-flex flex-column">
|
||||
{{ embed }}
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue