feat: Sites feature backend
This commit is contained in:
1 parent
45fcbd890d
commit
a11338094e
98 files changed
+5365
-214
No files matched your search
@@ -30,6 +30,7 @@ type Article struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
SiteID uint32 `json:"site_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -30,6 +30,7 @@ func DefaultSort() Sort {
|
||||
|
||||
type Filters struct {
|
||||
IsOnline *bool
|
||||
SiteID *uint32
|
||||
Page int
|
||||
Size int
|
||||
Q string
|
||||
@@ -37,6 +38,11 @@ type Filters struct {
|
||||
Sort Sort
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Body *string
|
||||
SiteID *uint32
|
||||
}
|
||||
|
||||
type Counts struct {
|
||||
All int `json:"all"`
|
||||
Online int `json:"online"`
|
||||
@@ -57,6 +63,7 @@ type Repository interface {
|
||||
GetAll() []Article
|
||||
Create(post Article) (Article, error)
|
||||
UpdateBody(id uint32, body string) (Article, error)
|
||||
Update(id uint32, update Update) (Article, error)
|
||||
UpdateIsOnline(id uint32, isOnline bool) (Article, error)
|
||||
Delete(id uint32) error
|
||||
FindByBodyContaining(filename string) ([]Article, error)
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
CategoryAPIKeys Category = "api_keys"
|
||||
CategoryAuth Category = "auth"
|
||||
CategorySystem Category = "system"
|
||||
CategorySites Category = "sites"
|
||||
)
|
||||
|
||||
type Outcome string
|
||||
@@ -71,6 +72,10 @@ const (
|
||||
ActionArticlePublish = "article.publish"
|
||||
ActionArticleUnpublish = "article.unpublish"
|
||||
ActionArticleDelete = "article.delete"
|
||||
ActionSiteCreate = "site.create"
|
||||
ActionSiteEdit = "site.edit"
|
||||
ActionSiteSetDefault = "site.set_default"
|
||||
ActionSiteDelete = "site.delete"
|
||||
ActionMediaUpload = "media.upload"
|
||||
ActionMediaRename = "media.rename"
|
||||
ActionMediaDelete = "media.delete"
|
||||
@@ -107,6 +112,7 @@ const (
|
||||
TargetVariant = "image_variant"
|
||||
TargetInvite = "invite"
|
||||
TargetRelease = "release"
|
||||
TargetSite = "site"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -146,6 +152,12 @@ const (
|
||||
FieldFit = "fit"
|
||||
FieldFormat = "format"
|
||||
FieldQuality = "quality"
|
||||
FieldName = "name"
|
||||
FieldBaseURL = "base_url"
|
||||
FieldDefaultRobots = "default_robots"
|
||||
FieldLocale = "locale"
|
||||
FieldSocialImage = "default_social_image"
|
||||
FieldSite = "site"
|
||||
maxNameLength = 255
|
||||
maxActionLength = 64
|
||||
maxFailureReasonSize = 255
|
||||
@@ -156,7 +168,7 @@ func ActorTypes() []ActorType {
|
||||
}
|
||||
|
||||
func Categories() []Category {
|
||||
return []Category{CategoryArticles, CategoryMedia, CategoryUsers, CategoryAPIKeys, CategoryAuth, CategorySystem}
|
||||
return []Category{CategoryArticles, CategoryMedia, CategoryUsers, CategoryAPIKeys, CategoryAuth, CategorySystem, CategorySites}
|
||||
}
|
||||
|
||||
func Outcomes() []Outcome {
|
||||
|
||||
@@ -52,5 +52,6 @@ type RunResult struct {
|
||||
GroupsPurged int
|
||||
Merged int
|
||||
ArticlesUpdated int
|
||||
SitesUpdated int
|
||||
FailedGroups int
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package site
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSiteNotFound = errors.New("site not found")
|
||||
ErrSiteKeyTaken = errors.New("a site with that key already exists")
|
||||
ErrSiteHasArticles = errors.New("the site still has articles")
|
||||
ErrDefaultSite = errors.New("the default site cannot be deleted")
|
||||
ErrKeyImmutable = errors.New("the key of a site cannot be changed")
|
||||
ErrInvalidSiteURL = errors.New("invalid site url")
|
||||
ErrInvalidLocale = errors.New("invalid locale")
|
||||
ErrUseSetDefault = errors.New("the default flag is changed through the default route")
|
||||
ErrInvalidReassign = errors.New("invalid reassign destination")
|
||||
)
|
||||
|
||||
const (
|
||||
RobotsIndex = "index"
|
||||
RobotsNoIndex = "noindex"
|
||||
|
||||
DefaultName = "Default"
|
||||
DefaultKey = "default"
|
||||
|
||||
MinNameRunes = 1
|
||||
MaxNameRunes = 100
|
||||
|
||||
MinKeyRunes = 2
|
||||
MaxKeyRunes = 32
|
||||
)
|
||||
|
||||
var keyPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
|
||||
var localePattern = regexp.MustCompile(`^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$`)
|
||||
|
||||
var loopbackHosts = []string{"localhost", "127.0.0.1", "::1"}
|
||||
|
||||
type Site struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
BaseURL string `json:"base_url"`
|
||||
DefaultSocialImageID *uint32 `json:"default_social_image_id"`
|
||||
DefaultRobots string `json:"default_robots"`
|
||||
Locale *string `json:"locale"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
ArticleCount int `json:"article_count"`
|
||||
}
|
||||
|
||||
type Nullable[T any] struct {
|
||||
Set bool
|
||||
Value *T
|
||||
}
|
||||
|
||||
type Patch struct {
|
||||
Name *string
|
||||
Key *string
|
||||
BaseURL *string
|
||||
DefaultRobots *string
|
||||
Locale Nullable[string]
|
||||
DefaultSocialImageID Nullable[uint32]
|
||||
IsDefault *bool
|
||||
}
|
||||
|
||||
func IsValidKey(key string) bool {
|
||||
length := utf8.RuneCountInString(key)
|
||||
return length >= MinKeyRunes && length <= MaxKeyRunes && keyPattern.MatchString(key)
|
||||
}
|
||||
|
||||
func IsValidName(name string) bool {
|
||||
length := utf8.RuneCountInString(name)
|
||||
return length >= MinNameRunes && length <= MaxNameRunes
|
||||
}
|
||||
|
||||
func NormalizeLocale(locale *string) (*string, error) {
|
||||
if locale == nil || *locale == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if !localePattern.MatchString(*locale) {
|
||||
return nil, ErrInvalidLocale
|
||||
}
|
||||
return locale, nil
|
||||
}
|
||||
|
||||
func isLoopback(hostname string) bool {
|
||||
for _, loopback := range loopbackHosts {
|
||||
if hostname == loopback {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type origin struct {
|
||||
scheme string
|
||||
host string
|
||||
hostname string
|
||||
parsed *url.URL
|
||||
}
|
||||
|
||||
func parseOrigin(raw string) (origin, error) {
|
||||
parsed, err := url.Parse(strings.TrimSpace(raw))
|
||||
if err != nil || parsed.Host == "" || parsed.Hostname() == "" {
|
||||
return origin{}, ErrInvalidSiteURL
|
||||
}
|
||||
scheme := strings.ToLower(parsed.Scheme)
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return origin{}, ErrInvalidSiteURL
|
||||
}
|
||||
return origin{scheme: scheme, host: strings.ToLower(parsed.Host), hostname: strings.ToLower(parsed.Hostname()), parsed: parsed}, nil
|
||||
}
|
||||
|
||||
func NormalizeBaseURL(raw string) (string, error) {
|
||||
found, err := parseOrigin(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsed := found.parsed
|
||||
if parsed.User != nil || parsed.RawQuery != "" || parsed.ForceQuery || parsed.Fragment != "" || (parsed.Path != "" && parsed.Path != "/") {
|
||||
return "", ErrInvalidSiteURL
|
||||
}
|
||||
if found.scheme == "http" && !isLoopback(found.hostname) {
|
||||
return "", ErrInvalidSiteURL
|
||||
}
|
||||
return found.scheme + "://" + found.host, nil
|
||||
}
|
||||
|
||||
func SeedBaseURL(publicURL string) (string, error) {
|
||||
found, err := parseOrigin(publicURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
scheme := found.scheme
|
||||
if scheme == "http" && !isLoopback(found.hostname) {
|
||||
scheme = "https"
|
||||
}
|
||||
return scheme + "://" + found.host, nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package site
|
||||
|
||||
type Repository interface {
|
||||
List() ([]Site, error)
|
||||
Get(id uint32) (Site, error)
|
||||
GetByKey(key string) (Site, error)
|
||||
GetDefault() (Site, error)
|
||||
Create(site Site) (Site, error)
|
||||
Update(site Site) (Site, error)
|
||||
SetDefault(id uint32) (Site, error)
|
||||
DeleteMovingArticles(id uint32, reassignTo *uint32) error
|
||||
ClearImage(imageID uint32) ([]uint32, error)
|
||||
AssignImage(siteIDs []uint32, imageID uint32) error
|
||||
RepointImage(fromImageIDs []uint32, toImageID uint32) (int, error)
|
||||
}
|
||||
Reference in new issue
Block a user