feat: custom content type icons, built-in types, user lookup

This commit is contained in:
Florian Sylvain committed 2026-10-19 18:22:55 +02:00
1 parent e6e088c963
commit 73a75d5cb9
31 files changed
+1611 -36

No files matched your search

@@ -0,0 +1,141 @@
package builtintype
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/user"
"errors"
)
var ErrNotFound = errors.New("built-in type not found")
const (
KeyArticle = "article"
KeyAuthor = "author"
KeySite = "site"
KeyMedia = "media"
KeyUser = "user"
)
type Type struct {
Key string `json:"key"`
Name string `json:"name"`
Fields []contenttype.Field `json:"fields"`
}
func Catalogue() []Type {
return []Type{
{Key: KeyArticle, Name: "Articles", Fields: articleFields()},
{Key: KeyAuthor, Name: "Authors", Fields: authorFields()},
{Key: KeySite, Name: "Sites", Fields: siteFields()},
{Key: KeyMedia, Name: "Media", Fields: mediaFields()},
{Key: KeyUser, Name: "Users", Fields: userFields()},
}
}
func Find(key string) (Type, error) {
for _, candidate := range Catalogue() {
if candidate.Key == key {
return candidate, nil
}
}
return Type{}, ErrNotFound
}
func field(key string, label string, fieldType contenttype.FieldType) contenttype.Field {
return contenttype.Field{Key: key, Label: label, Type: fieldType, Options: []string{}}
}
func (b builder) required() builder {
b.Required = true
return b
}
func (b builder) min(bound int) builder {
b.Min = bound
return b
}
func (b builder) max(bound int) builder {
b.Max = bound
return b
}
func (b builder) options(options ...string) builder {
b.Options = options
return b
}
type builder contenttype.Field
func text(key string, label string) builder {
return builder(field(key, label, contenttype.TypeText))
}
func of(key string, label string, fieldType contenttype.FieldType) builder {
return builder(field(key, label, fieldType))
}
func build(builders ...builder) []contenttype.Field {
fields := make([]contenttype.Field, 0, len(builders))
for _, b := range builders {
fields = append(fields, contenttype.Field(b))
}
return fields
}
func articleFields() []contenttype.Field {
return build(
text("title", "Title").required().min(article.MinTitleRunes).max(article.MaxTitleRunes),
of("body", "Body", contenttype.TypeRichText),
text("slug", "Slug"),
text("excerpt", "Excerpt").max(300),
text("seo_title", "SEO title").max(120),
text("seo_description", "SEO description").max(320),
of("is_online", "Online", contenttype.TypeBoolean),
text("canonical_url", "Canonical URL").max(article.MaxCanonicalURLRunes),
of("noindex", "No index", contenttype.TypeBoolean),
)
}
func authorFields() []contenttype.Field {
return build(
text("name", "Name").required(),
text("slug", "Slug"),
text("bio", "Bio"),
text("website", "Website"),
)
}
func siteFields() []contenttype.Field {
return build(
text("name", "Name").required(),
text("key", "Key").required(),
text("base_url", "Base URL").required(),
of("default_robots", "Default robots", contenttype.TypeSelect).options("index", "noindex"),
text("locale", "Locale"),
of("is_default", "Default site", contenttype.TypeBoolean),
text("site_name", "Site name").max(100),
text("title_template", "Title template").max(200),
text("default_description", "Default description").max(320),
)
}
func mediaFields() []contenttype.Field {
return build(
text("filename", "Filename"),
of("size", "Size", contenttype.TypeNumber),
of("width", "Width", contenttype.TypeNumber),
of("height", "Height", contenttype.TypeNumber),
text("content_type", "Content type"),
)
}
func userFields() []contenttype.Field {
return build(
text("username", "Username").required(),
text("display_name", "Display name"),
text("email", "Email").required(),
of("role", "Role", contenttype.TypeSelect).required().options(string(user.RoleOwner), string(user.RoleAdmin)),
)
}
@@ -10,6 +10,7 @@ var (
ErrKeyTaken = errors.New("a content type with that key already exists")
ErrInvalidName = errors.New("invalid content type name")
ErrInvalidKey = errors.New("invalid content type key")
ErrInvalidIcon = errors.New("invalid content type icon")
ErrInvalidField = errors.New("invalid field")
ErrHasEntries = errors.New("the content type still has entries")
ErrSchemaIncompatible = errors.New("the schema is incompatible with the stored entries")
@@ -58,6 +59,7 @@ type ContentType struct {
ID uint32 `json:"id"`
Key string `json:"key"`
Name string `json:"name"`
Icon *string `json:"icon"`
Fields []Field `json:"fields"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -7,6 +7,8 @@ import (
"unicode/utf8"
)
var iconPattern = regexp.MustCompile(`^[a-z][a-z0-9-]{0,39}$`)
var fieldKeyPattern = regexp.MustCompile(`^[a-z][a-z0-9_]{0,39}$`)
func ValidateKey(key string) error {
@@ -24,6 +26,16 @@ func ValidateName(name string) error {
return nil
}
func ValidateIcon(icon *string) (*string, error) {
if icon == nil || *icon == "" {
return nil, nil
}
if !iconPattern.MatchString(*icon) {
return nil, ErrInvalidIcon
}
return icon, nil
}
func ValidateFields(fields []Field) ([]Field, error) {
if len(fields) < MinFields || len(fields) > MaxFields {
return nil, ErrInvalidField
+11
View File
@@ -2,6 +2,7 @@ package user
import (
"errors"
"strings"
"time"
)
@@ -15,8 +16,11 @@ var ErrInvalidPasswordResetCode = errors.New("invalid password reset code")
var ErrPasswordResetCodeExpired = errors.New("password reset code expired")
var ErrInvalidDisplayName = errors.New("invalid display name")
var ErrUsernameTaken = errors.New("username taken")
var ErrInvalidLookup = errors.New("invalid user lookup")
var ErrVerificationEmailNotSent = errors.New("verification email not sent")
const MaxLookupIDs = 100
type Role string
const (
@@ -40,3 +44,10 @@ type User struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (u User) NameOrUsername() string {
if trimmed := strings.TrimSpace(u.DisplayName); trimmed != "" {
return trimmed
}
return u.Username
}
@@ -7,6 +7,7 @@ type Repository interface {
GetByUsername(username string) (User, error)
GetByEmail(email string) (User, error)
GetAll() []User
FindByIDs(ids []uint32) ([]User, error)
Create(user User) (User, error)
Delete(id uint32) error
UpdateVerificationStatus(userId uint32, isVerified bool) (User, error)