From a9927763f43d59c418b264790ccee59defe8ca29 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Tue, 24 Oct 2023 08:38:40 +0200 Subject: refactor: Rename packages for clarity --- auth/config.go | 34 ------------------- auth/cookie.go | 21 ------------ auth/crypto.go | 54 ----------------------------- auth/middlewares.go | 92 -------------------------------------------------- cmd/gettext/gettext.go | 14 ++++---- core/core.go | 34 ------------------- core/error.go | 16 --------- core/file.go | 38 --------------------- core/http.go | 28 --------------- core/image.go | 38 --------------------- core/middleware.go | 31 ----------------- core/template.go | 33 ------------------ kauth/config.go | 34 +++++++++++++++++++ kauth/cookie.go | 21 ++++++++++++ kauth/crypto.go | 54 +++++++++++++++++++++++++++++ kauth/middlewares.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++ kcore/core.go | 34 +++++++++++++++++++ kcore/error.go | 16 +++++++++ kcore/file.go | 38 +++++++++++++++++++++ kcore/http.go | 28 +++++++++++++++ kcore/image.go | 38 +++++++++++++++++++++ kcore/middleware.go | 31 +++++++++++++++++ kcore/template.go | 33 ++++++++++++++++++ 23 files changed, 426 insertions(+), 426 deletions(-) delete mode 100644 auth/config.go delete mode 100644 auth/cookie.go delete mode 100644 auth/crypto.go delete mode 100644 auth/middlewares.go delete mode 100644 core/core.go delete mode 100644 core/error.go delete mode 100644 core/file.go delete mode 100644 core/http.go delete mode 100644 core/image.go delete mode 100644 core/middleware.go delete mode 100644 core/template.go create mode 100644 kauth/config.go create mode 100644 kauth/cookie.go create mode 100644 kauth/crypto.go create mode 100644 kauth/middlewares.go create mode 100644 kcore/core.go create mode 100644 kcore/error.go create mode 100644 kcore/file.go create mode 100644 kcore/http.go create mode 100644 kcore/image.go create mode 100644 kcore/middleware.go create mode 100644 kcore/template.go diff --git a/auth/config.go b/auth/config.go deleted file mode 100644 index 6a55839..0000000 --- a/auth/config.go +++ /dev/null @@ -1,34 +0,0 @@ -package auth - -import ( - "encoding/hex" - "errors" - "os" - - "github.com/martinlehoux/kagamigo/core" - "golang.org/x/exp/slog" -) - -var ( - ErrCookieBadLength = errors.New("cookie secret must be 32 bytes") -) - -type AuthConfig struct { - Domain string - CookieSecret []byte -} - -func LoadCookieSecret(cookieSecretString string) []byte { - cookiesSecret, err := hex.DecodeString(cookieSecretString) - if err != nil { - err = core.Wrap(err, "error decoding cookie secret") - slog.Error(err.Error()) - os.Exit(1) - } - if len(cookiesSecret) != 32 { - slog.Error(ErrCookieBadLength.Error()) - os.Exit(1) - } - slog.Info("cookie secret loaded") - return cookiesSecret -} diff --git a/auth/cookie.go b/auth/cookie.go deleted file mode 100644 index 61349fa..0000000 --- a/auth/cookie.go +++ /dev/null @@ -1,21 +0,0 @@ -package auth - -import ( - "fmt" - "net/http" - "time" - - "github.com/martinlehoux/kagamigo/core" -) - -func CraftCookie(id core.ID, config AuthConfig) http.Cookie { - expiresAt := time.Now().Add(24 * time.Hour) - cookieValue := encrypt(config.CookieSecret, fmt.Sprintf("%s:%d", id.String(), expiresAt.Unix())) - return http.Cookie{ - Domain: config.Domain, - Name: "authentication", - Value: cookieValue, - Expires: expiresAt, - Path: "/", - } -} diff --git a/auth/crypto.go b/auth/crypto.go deleted file mode 100644 index 254539f..0000000 --- a/auth/crypto.go +++ /dev/null @@ -1,54 +0,0 @@ -package auth - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "errors" - - "github.com/martinlehoux/kagamigo/core" -) - -var ( - ErrEncryptedTooShort = errors.New("encrypted text is too short") -) - -func encrypt(secret []byte, plainText string) string { - block, err := aes.NewCipher(secret) - core.Expect(err, "error creating AES cipher") - - aead, err := cipher.NewGCM(block) - core.Expect(err, "error creating AEAD") - - nonce := make([]byte, aead.NonceSize()) - _, err = rand.Read(nonce) - core.Expect(err, "error generating nonce") - - return base64.URLEncoding.EncodeToString(aead.Seal(nonce, nonce, []byte(plainText), nil)) -} - -func decrypt(secret []byte, encryptedText string) (string, error) { - encryptedBytes, err := base64.URLEncoding.DecodeString(encryptedText) - if err != nil { - err = core.Wrap(err, "error decoding base64") - return "", err - } - block, err := aes.NewCipher(secret) - core.Expect(err, "error creating AES cipher") - - aead, err := cipher.NewGCM(block) - core.Expect(err, "error creating AEAD") - - nonceSize := aead.NonceSize() - if len(encryptedBytes) < nonceSize { - return "", ErrEncryptedTooShort - } - nonce, cipherText := encryptedBytes[:nonceSize], encryptedBytes[nonceSize:] - plainBytes, err := aead.Open(nil, nonce, cipherText, nil) - if err != nil { - err = core.Wrap(err, "error decrypting") - return "", err - } - return string(plainBytes), nil -} diff --git a/auth/middlewares.go b/auth/middlewares.go deleted file mode 100644 index 1534b16..0000000 --- a/auth/middlewares.go +++ /dev/null @@ -1,92 +0,0 @@ -package auth - -import ( - "context" - "errors" - "net/http" - "strconv" - "strings" - "time" - - "github.com/martinlehoux/kagamigo/core" - "golang.org/x/exp/slog" -) - -var ( - ErrUserNotLoggedIn = errors.New("user not logged in") - ErrCookieExpired = errors.New("cookie expired") - ErrBadCookie = errors.New("invalid cookie") -) - -type userContext struct{} - -func UserFromContext[U any](ctx context.Context) (U, bool) { - user, ok := ctx.Value(userContext{}).(U) - return user, ok -} - -func LoginFromContext[U core.WithLanguage](ctx context.Context) core.Login[U] { - user, ok := UserFromContext[U](ctx) - return core.LoginFromUser(user, ok) -} - -func Unauthorized(w http.ResponseWriter, err error) { - w.WriteHeader(http.StatusUnauthorized) - if err != nil { - _, err = w.Write([]byte(err.Error())) - core.Expect(err, "error writing response") - } -} - -func CookieAuthMiddleware(loadUser func(context.Context, core.ID) (any, error), config AuthConfig) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - cookie, err := r.Cookie("authentication") - if errors.Is(err, http.ErrNoCookie) { - next.ServeHTTP(w, r) - return - } - core.Expect(err, "error reading cookie") - - authentication, err := decrypt(config.CookieSecret, cookie.Value) - if err != nil { - err = core.Wrap(err, "error decrypting cookie") - slog.Warn(err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - parts := strings.Split(authentication, ":") - if len(parts) != 2 { - slog.Warn(ErrBadCookie.Error()) - http.Error(w, ErrBadCookie.Error(), http.StatusBadRequest) - return - } - userId, err := core.ParseID(parts[0]) - if err != nil { - err = core.Wrap(err, "error parsing user id") - slog.Warn(err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - expiresAtSeconds, err := strconv.Atoi(parts[1]) - if err != nil { - err = core.Wrap(err, "error parsing expires at") - slog.Warn(err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - expiresAt := time.Unix(int64(expiresAtSeconds), 0) - if time.Now().After(expiresAt) { - slog.Warn(ErrCookieExpired.Error()) - http.Error(w, ErrCookieExpired.Error(), http.StatusUnauthorized) - return - } - user, err := loadUser(ctx, userId) - core.Expect(err, "error loading user") - - ctx = context.WithValue(ctx, userContext{}, user) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} diff --git a/cmd/gettext/gettext.go b/cmd/gettext/gettext.go index b753da8..2dbd65e 100644 --- a/cmd/gettext/gettext.go +++ b/cmd/gettext/gettext.go @@ -9,7 +9,7 @@ import ( "regexp" "strings" - "github.com/martinlehoux/kagamigo/core" + "github.com/martinlehoux/kagamigo/kcore" "golang.org/x/exp/slog" "gopkg.in/yaml.v3" ) @@ -98,14 +98,14 @@ func extractAllKeys() map[string]int { err := filepath.Walk("templates", func(path string, info fs.FileInfo, err error) error { if !info.IsDir() && filepath.Ext(path) == ".html" { content, err := os.ReadFile(path) // #nosec G304 - core.Expect(err, "error reading file") + kcore.Expect(err, "error reading file") for key, value := range extractKeys(string(content)) { extractedKeys[key] = value } } return nil }) - core.Expect(err, "error walking templates directory") + kcore.Expect(err, "error walking templates directory") return extractedKeys } @@ -125,8 +125,8 @@ func main() { newLocales := make(map[string]string, 0) locales, err := os.ReadFile(filepath.Join("locales", lang, "index.yml")) // #nosec G304 - core.Expect(err, "error reading file") - core.Expect(yaml.Unmarshal(locales, ¤tLocales), "error unmarshalling yaml") + kcore.Expect(err, "error reading file") + kcore.Expect(yaml.Unmarshal(locales, ¤tLocales), "error unmarshalling yaml") correctLocales := 0 for key, translation := range currentLocales { @@ -157,8 +157,8 @@ func main() { if *write { content, err := yaml.Marshal(newLocales) - core.Expect(err, "error marshalling yaml") - core.Expect(os.WriteFile(filepath.Join("locales", lang, "index.yml"), content, 0600), "error writing file") + kcore.Expect(err, "error marshalling yaml") + kcore.Expect(os.WriteFile(filepath.Join("locales", lang, "index.yml"), content, 0600), "error writing file") } } } diff --git a/core/core.go b/core/core.go deleted file mode 100644 index 5cd654d..0000000 --- a/core/core.go +++ /dev/null @@ -1,34 +0,0 @@ -package core - -import ( - "encoding/base64" - - "github.com/gofrs/uuid" -) - -type ID struct { - uuid.UUID -} - -func NewID() ID { - id, err := uuid.NewV4() - Expect(err, "error generating uuid") - - return ID{id} -} - -func (id ID) String() string { - return base64.RawURLEncoding.EncodeToString(id.Bytes()) -} - -func ParseID(value string) (ID, error) { - bytes, err := base64.RawURLEncoding.DecodeString(value) - if err != nil { - return ID{}, Wrap(err, "error decoding value") - } - id, err := uuid.FromBytes(bytes) - if err != nil { - return ID{}, Wrap(err, "error parsing uuid") - } - return ID{id}, nil -} diff --git a/core/error.go b/core/error.go deleted file mode 100644 index 7b1871a..0000000 --- a/core/error.go +++ /dev/null @@ -1,16 +0,0 @@ -package core - -import "fmt" - -func Wrap(err error, msg string) error { - return fmt.Errorf("%s: %w", msg, err) -} - -func Expect(err error, msg string) { - if err != nil { - if msg != "" { - err = Wrap(err, msg) - } - panic(err) - } -} diff --git a/core/file.go b/core/file.go deleted file mode 100644 index 083d8f7..0000000 --- a/core/file.go +++ /dev/null @@ -1,38 +0,0 @@ -package core - -import ( - "fmt" - "io" - "mime/multipart" - "os" -) - -type File (string) - -func NewFile(ext string) File { - return File(NewID().String() + ext) -} - -func (file File) Path() string { - return fmt.Sprintf("media/files/%s", file) -} - -func (file *File) Save(raw multipart.File) error { - dest, err := os.Create(file.Path()) - if err != nil { - return Wrap(err, "error creating file destination") - } - _, err = io.Copy(dest, raw) - if err != nil { - return Wrap(err, "error copying to file destination") - } - return nil -} - -func (file *File) Delete() error { - err := os.Remove(file.Path()) - if err != nil { - return Wrap(err, "error deleting file") - } - return nil -} diff --git a/core/http.go b/core/http.go deleted file mode 100644 index 778c5c9..0000000 --- a/core/http.go +++ /dev/null @@ -1,28 +0,0 @@ -package core - -import ( - "bytes" - "context" - "html/template" - "net/http" - - "github.com/a-h/templ" -) - -func ExecuteTemplate(w http.ResponseWriter, tpl template.Template, name string, data interface{}) { - var buf bytes.Buffer - err := tpl.ExecuteTemplate(&buf, name, data) - Expect(err, "error executing template") - w.Header().Set("Content-Type", "text/html; charset=UTF-8") - _, err = buf.WriteTo(w) - Expect(err, "error writing template to response writer") -} - -func RenderPage(ctx context.Context, page templ.Component, w http.ResponseWriter) { - var buf bytes.Buffer - err := page.Render(ctx, &buf) - Expect(err, "error rendering page") - w.Header().Set("Content-Type", "text/html; charset=UTF-8") - _, err = buf.WriteTo(w) - Expect(err, "error writing page to response writer") -} diff --git a/core/image.go b/core/image.go deleted file mode 100644 index c06cbd8..0000000 --- a/core/image.go +++ /dev/null @@ -1,38 +0,0 @@ -package core - -import ( - "fmt" - "io" - "mime/multipart" - "os" -) - -type Image (ID) - -func NewImage() Image { - return Image(NewID()) -} - -func (image Image) Path() string { - return fmt.Sprintf("media/images/%s", image) -} - -func (image *Image) Save(raw multipart.File) error { - dest, err := os.Create(image.Path()) - if err != nil { - return Wrap(err, "error creating image destination") - } - _, err = io.Copy(dest, raw) - if err != nil { - return Wrap(err, "error copying to image destination") - } - return nil -} - -func (image *Image) Delete() error { - err := os.Remove(image.Path()) - if err != nil { - return Wrap(err, "error deleting image") - } - return nil -} diff --git a/core/middleware.go b/core/middleware.go deleted file mode 100644 index 55e67fb..0000000 --- a/core/middleware.go +++ /dev/null @@ -1,31 +0,0 @@ -package core - -import ( - "errors" - "net/http" - - "golang.org/x/exp/slog" -) - -func RecoverMiddleware(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rcv := recover(); rcv != nil { - switch err := rcv.(type) { - case error: - if errors.Is(err, http.ErrAbortHandler) { - panic(rcv) - } - slog.Error(err.Error()) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - default: - panic(err) - } - } - }() - - next.ServeHTTP(w, r) - } - - return http.HandlerFunc(fn) -} diff --git a/core/template.go b/core/template.go deleted file mode 100644 index a0d0668..0000000 --- a/core/template.go +++ /dev/null @@ -1,33 +0,0 @@ -package core - -import ( - "github.com/kataras/i18n" -) - -type Tr = func(format string, args ...any) string - -type WithLanguage interface { - Language() string -} - -func GetTr(wl WithLanguage) Tr { - lang := "en-GB" - if wl != nil { - lang = wl.Language() - } - return func(format string, args ...any) string { return i18n.Tr(lang, format, args...) } -} - -type Login[U any] struct { - Ok bool - User U - Tr Tr -} - -func LoginFromUser[U WithLanguage](user U, ok bool) Login[U] { - return Login[U]{ - Ok: ok, - User: user, - Tr: GetTr(user), - } -} diff --git a/kauth/config.go b/kauth/config.go new file mode 100644 index 0000000..777a22d --- /dev/null +++ b/kauth/config.go @@ -0,0 +1,34 @@ +package kauth + +import ( + "encoding/hex" + "errors" + "os" + + "github.com/martinlehoux/kagamigo/kcore" + "golang.org/x/exp/slog" +) + +var ( + ErrCookieBadLength = errors.New("cookie secret must be 32 bytes") +) + +type AuthConfig struct { + Domain string + CookieSecret []byte +} + +func LoadCookieSecret(cookieSecretString string) []byte { + cookiesSecret, err := hex.DecodeString(cookieSecretString) + if err != nil { + err = kcore.Wrap(err, "error decoding cookie secret") + slog.Error(err.Error()) + os.Exit(1) + } + if len(cookiesSecret) != 32 { + slog.Error(ErrCookieBadLength.Error()) + os.Exit(1) + } + slog.Info("cookie secret loaded") + return cookiesSecret +} diff --git a/kauth/cookie.go b/kauth/cookie.go new file mode 100644 index 0000000..edc30b7 --- /dev/null +++ b/kauth/cookie.go @@ -0,0 +1,21 @@ +package kauth + +import ( + "fmt" + "net/http" + "time" + + "github.com/martinlehoux/kagamigo/kcore" +) + +func CraftCookie(id kcore.ID, config AuthConfig) http.Cookie { + expiresAt := time.Now().Add(24 * time.Hour) + cookieValue := encrypt(config.CookieSecret, fmt.Sprintf("%s:%d", id.String(), expiresAt.Unix())) + return http.Cookie{ + Domain: config.Domain, + Name: "authentication", + Value: cookieValue, + Expires: expiresAt, + Path: "/", + } +} diff --git a/kauth/crypto.go b/kauth/crypto.go new file mode 100644 index 0000000..702c9a7 --- /dev/null +++ b/kauth/crypto.go @@ -0,0 +1,54 @@ +package kauth + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + + "github.com/martinlehoux/kagamigo/kcore" +) + +var ( + ErrEncryptedTooShort = errors.New("encrypted text is too short") +) + +func encrypt(secret []byte, plainText string) string { + block, err := aes.NewCipher(secret) + kcore.Expect(err, "error creating AES cipher") + + aead, err := cipher.NewGCM(block) + kcore.Expect(err, "error creating AEAD") + + nonce := make([]byte, aead.NonceSize()) + _, err = rand.Read(nonce) + kcore.Expect(err, "error generating nonce") + + return base64.URLEncoding.EncodeToString(aead.Seal(nonce, nonce, []byte(plainText), nil)) +} + +func decrypt(secret []byte, encryptedText string) (string, error) { + encryptedBytes, err := base64.URLEncoding.DecodeString(encryptedText) + if err != nil { + err = kcore.Wrap(err, "error decoding base64") + return "", err + } + block, err := aes.NewCipher(secret) + kcore.Expect(err, "error creating AES cipher") + + aead, err := cipher.NewGCM(block) + kcore.Expect(err, "error creating AEAD") + + nonceSize := aead.NonceSize() + if len(encryptedBytes) < nonceSize { + return "", ErrEncryptedTooShort + } + nonce, cipherText := encryptedBytes[:nonceSize], encryptedBytes[nonceSize:] + plainBytes, err := aead.Open(nil, nonce, cipherText, nil) + if err != nil { + err = kcore.Wrap(err, "error decrypting") + return "", err + } + return string(plainBytes), nil +} diff --git a/kauth/middlewares.go b/kauth/middlewares.go new file mode 100644 index 0000000..23e9e71 --- /dev/null +++ b/kauth/middlewares.go @@ -0,0 +1,92 @@ +package kauth + +import ( + "context" + "errors" + "net/http" + "strconv" + "strings" + "time" + + "github.com/martinlehoux/kagamigo/kcore" + "golang.org/x/exp/slog" +) + +var ( + ErrUserNotLoggedIn = errors.New("user not logged in") + ErrCookieExpired = errors.New("cookie expired") + ErrBadCookie = errors.New("invalid cookie") +) + +type userContext struct{} + +func UserFromContext[U any](ctx context.Context) (U, bool) { + user, ok := ctx.Value(userContext{}).(U) + return user, ok +} + +func LoginFromContext[U kcore.WithLanguage](ctx context.Context) kcore.Login[U] { + user, ok := UserFromContext[U](ctx) + return kcore.LoginFromUser(user, ok) +} + +func Unauthorized(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusUnauthorized) + if err != nil { + _, err = w.Write([]byte(err.Error())) + kcore.Expect(err, "error writing response") + } +} + +func CookieAuthMiddleware(loadUser func(context.Context, kcore.ID) (any, error), config AuthConfig) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + cookie, err := r.Cookie("authentication") + if errors.Is(err, http.ErrNoCookie) { + next.ServeHTTP(w, r) + return + } + kcore.Expect(err, "error reading cookie") + + authentication, err := decrypt(config.CookieSecret, cookie.Value) + if err != nil { + err = kcore.Wrap(err, "error decrypting cookie") + slog.Warn(err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + parts := strings.Split(authentication, ":") + if len(parts) != 2 { + slog.Warn(ErrBadCookie.Error()) + http.Error(w, ErrBadCookie.Error(), http.StatusBadRequest) + return + } + userId, err := kcore.ParseID(parts[0]) + if err != nil { + err = kcore.Wrap(err, "error parsing user id") + slog.Warn(err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + expiresAtSeconds, err := strconv.Atoi(parts[1]) + if err != nil { + err = kcore.Wrap(err, "error parsing expires at") + slog.Warn(err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + expiresAt := time.Unix(int64(expiresAtSeconds), 0) + if time.Now().After(expiresAt) { + slog.Warn(ErrCookieExpired.Error()) + http.Error(w, ErrCookieExpired.Error(), http.StatusUnauthorized) + return + } + user, err := loadUser(ctx, userId) + kcore.Expect(err, "error loading user") + + ctx = context.WithValue(ctx, userContext{}, user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/kcore/core.go b/kcore/core.go new file mode 100644 index 0000000..5e206e1 --- /dev/null +++ b/kcore/core.go @@ -0,0 +1,34 @@ +package kcore + +import ( + "encoding/base64" + + "github.com/gofrs/uuid" +) + +type ID struct { + uuid.UUID +} + +func NewID() ID { + id, err := uuid.NewV4() + Expect(err, "error generating uuid") + + return ID{id} +} + +func (id ID) String() string { + return base64.RawURLEncoding.EncodeToString(id.Bytes()) +} + +func ParseID(value string) (ID, error) { + bytes, err := base64.RawURLEncoding.DecodeString(value) + if err != nil { + return ID{}, Wrap(err, "error decoding value") + } + id, err := uuid.FromBytes(bytes) + if err != nil { + return ID{}, Wrap(err, "error parsing uuid") + } + return ID{id}, nil +} diff --git a/kcore/error.go b/kcore/error.go new file mode 100644 index 0000000..bbacbf6 --- /dev/null +++ b/kcore/error.go @@ -0,0 +1,16 @@ +package kcore + +import "fmt" + +func Wrap(err error, msg string) error { + return fmt.Errorf("%s: %w", msg, err) +} + +func Expect(err error, msg string) { + if err != nil { + if msg != "" { + err = Wrap(err, msg) + } + panic(err) + } +} diff --git a/kcore/file.go b/kcore/file.go new file mode 100644 index 0000000..f7f5e37 --- /dev/null +++ b/kcore/file.go @@ -0,0 +1,38 @@ +package kcore + +import ( + "fmt" + "io" + "mime/multipart" + "os" +) + +type File (string) + +func NewFile(ext string) File { + return File(NewID().String() + ext) +} + +func (file File) Path() string { + return fmt.Sprintf("media/files/%s", file) +} + +func (file *File) Save(raw multipart.File) error { + dest, err := os.Create(file.Path()) + if err != nil { + return Wrap(err, "error creating file destination") + } + _, err = io.Copy(dest, raw) + if err != nil { + return Wrap(err, "error copying to file destination") + } + return nil +} + +func (file *File) Delete() error { + err := os.Remove(file.Path()) + if err != nil { + return Wrap(err, "error deleting file") + } + return nil +} diff --git a/kcore/http.go b/kcore/http.go new file mode 100644 index 0000000..e5d218b --- /dev/null +++ b/kcore/http.go @@ -0,0 +1,28 @@ +package kcore + +import ( + "bytes" + "context" + "html/template" + "net/http" + + "github.com/a-h/templ" +) + +func ExecuteTemplate(w http.ResponseWriter, tpl template.Template, name string, data interface{}) { + var buf bytes.Buffer + err := tpl.ExecuteTemplate(&buf, name, data) + Expect(err, "error executing template") + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + _, err = buf.WriteTo(w) + Expect(err, "error writing template to response writer") +} + +func RenderPage(ctx context.Context, page templ.Component, w http.ResponseWriter) { + var buf bytes.Buffer + err := page.Render(ctx, &buf) + Expect(err, "error rendering page") + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + _, err = buf.WriteTo(w) + Expect(err, "error writing page to response writer") +} diff --git a/kcore/image.go b/kcore/image.go new file mode 100644 index 0000000..20b3563 --- /dev/null +++ b/kcore/image.go @@ -0,0 +1,38 @@ +package kcore + +import ( + "fmt" + "io" + "mime/multipart" + "os" +) + +type Image (ID) + +func NewImage() Image { + return Image(NewID()) +} + +func (image Image) Path() string { + return fmt.Sprintf("media/images/%s", image) +} + +func (image *Image) Save(raw multipart.File) error { + dest, err := os.Create(image.Path()) + if err != nil { + return Wrap(err, "error creating image destination") + } + _, err = io.Copy(dest, raw) + if err != nil { + return Wrap(err, "error copying to image destination") + } + return nil +} + +func (image *Image) Delete() error { + err := os.Remove(image.Path()) + if err != nil { + return Wrap(err, "error deleting image") + } + return nil +} diff --git a/kcore/middleware.go b/kcore/middleware.go new file mode 100644 index 0000000..1537f55 --- /dev/null +++ b/kcore/middleware.go @@ -0,0 +1,31 @@ +package kcore + +import ( + "errors" + "net/http" + + "golang.org/x/exp/slog" +) + +func RecoverMiddleware(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rcv := recover(); rcv != nil { + switch err := rcv.(type) { + case error: + if errors.Is(err, http.ErrAbortHandler) { + panic(rcv) + } + slog.Error(err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + default: + panic(err) + } + } + }() + + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) +} diff --git a/kcore/template.go b/kcore/template.go new file mode 100644 index 0000000..33b2f5a --- /dev/null +++ b/kcore/template.go @@ -0,0 +1,33 @@ +package kcore + +import ( + "github.com/kataras/i18n" +) + +type Tr = func(format string, args ...any) string + +type WithLanguage interface { + Language() string +} + +func GetTr(wl WithLanguage) Tr { + lang := "en-GB" + if wl != nil { + lang = wl.Language() + } + return func(format string, args ...any) string { return i18n.Tr(lang, format, args...) } +} + +type Login[U any] struct { + Ok bool + User U + Tr Tr +} + +func LoginFromUser[U WithLanguage](user U, ok bool) Login[U] { + return Login[U]{ + Ok: ok, + User: user, + Tr: GetTr(user), + } +} -- cgit v1.3