===== FILE: ./api/dependecyInjection.go =====
package api

import (
	domainArticle "RenewCMS/internal/domain/article"
	domainImage "RenewCMS/internal/domain/image"
	domainMail "RenewCMS/internal/domain/mail"
	domainUser "RenewCMS/internal/domain/user"
	"RenewCMS/internal/infrastructure/mailer"
	"RenewCMS/internal/infrastructure/persistence"
	"RenewCMS/internal/infrastructure/persistence/models"
	"RenewCMS/internal/infrastructure/useCases"
	"os"
	"path/filepath"

	"github.com/glebarez/sqlite"
	"github.com/go-chi/jwtauth/v5"
	"gorm.io/gorm"
)

type UseCases struct {
	Token                *jwtauth.JWTAuth
	CreateArticleUseCase *useCases.CreateArticleUseCase
	GetArticleUseCase    *useCases.GetArticleUseCase
	ListArticlesUseCase  *useCases.ListArticlesUseCase
	UpdateArticleUseCase *useCases.UpdateArticleUseCase
	DeleteArticleUseCase *useCases.DeleteArticleUseCase
	GetUserUseCase       *useCases.GetUserUseCase
	CreateUserUseCase    *useCases.CreateUserUseCase
	UpdateUserUseCase    *useCases.UpdateUserUseCase
	DeleteUserUseCase    *useCases.DeleteUserUseCase
	ListUsersUseCase     *useCases.ListUsersUseCase
	SendMailUseCase      *useCases.SendMailUseCase
	CreateImageUseCase   *useCases.CreateImageUseCase
	DeleteImageUseCase   *useCases.DeleteImageUseCase
}

type Repositories struct {
	ArticleRepo domainArticle.Repository
	UserRepo    domainUser.Repository
	ImageRepo   domainImage.Repository
	MailRepo    domainMail.Repository
}

func getDb() *gorm.DB {
	dbName := os.Getenv("DB_FILE")
	if err := os.MkdirAll(filepath.Dir(dbName), os.ModePerm); err != nil {
		panic("Unable to create necessary subdirectories: " + err.Error())
	}
	db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
	if err != nil {
		panic("Unable to open the database: " + err.Error())
	}

	if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil {
		panic("Failed to migrate database: " + err.Error())
	}

	return db
}

func getRepositories(db *gorm.DB) *Repositories {
	return &Repositories{
		ArticleRepo: persistence.NewArticleRepository(db),
		UserRepo:    persistence.NewUserRepository(db),
		ImageRepo:   persistence.NewImageRepository(db),
		MailRepo:    mailer.NewMailRepository(),
	}
}

func getUseCases(repos *Repositories) *UseCases {
	return &UseCases{
		Token:                jwtauth.New("HS256", []byte(os.Getenv("JWT_SECRET")), nil),
		CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo),
		GetArticleUseCase:    useCases.NewGetArticleUseCase(repos.ArticleRepo),
		ListArticlesUseCase:  useCases.NewListArticlesUseCase(repos.ArticleRepo),
		UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo),
		DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo),
		GetUserUseCase:       useCases.NewGetUserUseCase(repos.UserRepo),
		CreateUserUseCase:    useCases.NewCreateUserUseCase(repos.UserRepo),
		UpdateUserUseCase:    useCases.NewUpdateUserUseCase(repos.UserRepo),
		DeleteUserUseCase:    useCases.NewDeleteUserUseCase(repos.UserRepo),
		ListUsersUseCase:     useCases.NewListUsersUseCase(repos.UserRepo),
		SendMailUseCase:      useCases.NewSendMailUseCase(repos.MailRepo),
		CreateImageUseCase:   useCases.NewCreateImageUseCase(repos.ImageRepo),
		DeleteImageUseCase:   useCases.NewDeleteImageUseCase(repos.ImageRepo),
	}
}

func InitContainer() *UseCases {
	db := getDb()
	repos := getRepositories(db)
	return getUseCases(repos)
}

===== FILE: ./api/handlers/article/article.go =====
package article

import (
	"RenewCMS/api/pkg/apperr"
	validator "RenewCMS/api/pkg/validator"
	domainArticle "RenewCMS/internal/domain/article"
	"RenewCMS/internal/infrastructure/useCases"
	"encoding/json"
	"net/http"
	"strconv"

	"github.com/go-chi/chi/v5"
)

type Handler struct {
	CreateUseCase *useCases.CreateArticleUseCase
	GetUseCase    *useCases.GetArticleUseCase
	ListUseCase   *useCases.ListArticlesUseCase
	DeleteUseCase *useCases.DeleteArticleUseCase
}

type PostArticle struct {
	Title string `json:"title" validate:"required,min=3,max=50"`
	Body  string `json:"body" validate:"required,max=10000"`
}

func NewHandler(create *useCases.CreateArticleUseCase, get *useCases.GetArticleUseCase, list *useCases.ListArticlesUseCase, delete *useCases.DeleteArticleUseCase) *Handler {
	return &Handler{
		CreateUseCase: create,
		GetUseCase:    get,
		ListUseCase:   list,
		DeleteUseCase: delete,
	}
}

func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
	if err != nil {
		http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
		return
	}

	localArticle, err := h.GetUseCase.GetArticle(uint32(id))
	if err != nil || !localArticle.IsOnline {
		http.Error(w, "The requested resource, identified by its unique ID, could not be found on the server.", http.StatusNotFound)
		return
	}

	articleJson, _ := json.Marshal(localArticle)
	_, _ = w.Write(articleJson)
}

func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
	var localArticle PostArticle
	err := json.NewDecoder(r.Body).Decode(&localArticle)
	if err != nil {
		http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
		return
	}

	err = validator.Validate.Struct(localArticle)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	createdArticle, err := h.CreateUseCase.CreateArticle(useCases.CreateArticleCommand{
		Title: localArticle.Title,
		Body:  localArticle.Body,
	})
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	articleJson, _ := json.Marshal(createdArticle)

	_, _ = w.Write(articleJson)
}

func (h *Handler) ListArticles(w http.ResponseWriter, _ *http.Request) {
	articles := h.ListUseCase.ListArticles()
	onlineArticles := make([]domainArticle.Article, 0)
	for _, localArticle := range articles {
		if localArticle.IsOnline {
			onlineArticles = append(onlineArticles, localArticle)
		}
	}
	articlesJson, _ := json.Marshal(onlineArticles)
	_, _ = w.Write(articlesJson)
}

func (h *Handler) DeleteArticle(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
	if err != nil {
		http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
		return
	}

	err = h.DeleteUseCase.DeleteArticle(uint32(id))
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}

	_, _ = w.Write([]byte("article deleted"))
}

===== FILE: ./api/handlers/auth/auth.go =====
package auth

import (
	"RenewCMS/api/pkg/apperr"
	validator "RenewCMS/api/pkg/validator"
	"RenewCMS/internal/domain/user"
	"RenewCMS/internal/infrastructure/useCases"
	"encoding/json"
	"net/http"
	"os"
	"time"

	"github.com/go-chi/jwtauth/v5"
	"golang.org/x/crypto/bcrypt"
)

type Handler struct {
	Token         *jwtauth.JWTAuth
	GetUseCase    *useCases.GetUserUseCase
	ListUseCase   *useCases.ListUsersUseCase
	CreateUseCase *useCases.CreateUserUseCase
}

type RegisterCredentials struct {
	Username string `json:"username" validate:"required,min=3,max=20"`
	Password string `json:"password" validate:"required,min=8"`
	Email    string `json:"email" validate:"required,email"`
}

type LoginCredentials struct {
	Username string `json:"username" validate:"required,min=3,max=20"`
	Password string `json:"password" validate:"required,min=8"`
}

var shouldCookieBeSecure = os.Getenv("ENVIRONMENT") == "production"

func removeJwtCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name:     "jwt",
		Value:    "",
		Expires:  time.Now(),
		MaxAge:   -1,
		Secure:   shouldCookieBeSecure,
		HttpOnly: true,
		Path:     "/",
	})
}

func NewHandler(token *jwtauth.JWTAuth, get *useCases.GetUserUseCase, list *useCases.ListUsersUseCase, create *useCases.CreateUserUseCase) *Handler {
	return &Handler{
		Token:         token,
		GetUseCase:    get,
		ListUseCase:   list,
		CreateUseCase: create,
	}
}

func (h *Handler) SetJwtCookie(w *http.ResponseWriter, userId uint32) error {
	_, tokenString, err := h.Token.Encode(map[string]any{"user_id": userId})
	if err != nil {
		return err
	}
	http.SetCookie(*w, &http.Cookie{
		Name:     "jwt",
		Value:    tokenString,
		Expires:  time.Now().Add(24 * time.Hour),
		Secure:   shouldCookieBeSecure,
		HttpOnly: true,
		Path:     "/",
	})
	return nil
}

func (h *Handler) SomeUsersVerified() bool {
	users := h.ListUseCase.ListUsers()
	for _, localUser := range users {
		if localUser.IsVerified {
			return true
		}
	}
	return false
}

func (h *Handler) IsUserTableEmpty() bool {
	users := h.ListUseCase.ListUsers()
	return len(users) == 0
}

func (h *Handler) IsLoggedIn(r *http.Request) bool {
	token, err := jwtauth.VerifyRequest(
		h.Token,
		r,
		jwtauth.TokenFromCookie,
		jwtauth.TokenFromHeader,
		jwtauth.TokenFromQuery)
	return token != nil && err == nil
}

func (h *Handler) IsVerified(r *http.Request) bool {
	token, err := jwtauth.VerifyRequest(
		h.Token,
		r,
		jwtauth.TokenFromCookie,
		jwtauth.TokenFromHeader,
		jwtauth.TokenFromQuery)
	if err != nil {
		return false
	}
	userId := token.PrivateClaims()["user_id"].(float64)
	currentUser, _ := h.GetUseCase.GetUser(uint32(userId))
	return currentUser.IsVerified
}

func (h *Handler) GetUserFromCredentials(credentials LoginCredentials) (user.User, error) {
	dbUser, err := h.GetUseCase.GetUserByUsername(credentials.Username)
	if err != nil {
		return user.User{}, err
	}

	err = bcrypt.CompareHashAndPassword([]byte(dbUser.Password), []byte(credentials.Password))
	if err != nil {
		return user.User{}, err
	}

	return dbUser, nil
}

func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
	var credentials LoginCredentials

	err := json.NewDecoder(r.Body).Decode(&credentials)
	if err != nil {
		http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
		return
	}

	err = validator.Validate.Struct(credentials)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	dbUser, err := h.GetUserFromCredentials(credentials)
	if err != nil {
		http.Error(w, apperr.LogsErrorMessage, http.StatusForbidden)
		return
	}

	_ = h.SetJwtCookie(&w, dbUser.ID)

	message, _ := json.Marshal(map[string]any{"message": "User logged in! HTTPonly jwt cookie created"})
	_, _ = w.Write(message)
}

func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
	if !h.IsLoggedIn(r) && !h.SomeUsersVerified() {
		http.Error(w, "You are not allowed to create a user. Log in or reset database.", http.StatusForbidden)
		return
	}

	var credentials RegisterCredentials
	err := json.NewDecoder(r.Body).Decode(&credentials)
	if err != nil {
		http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
		return
	}

	err = validator.Validate.Struct(credentials)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	createdUser, err := h.CreateUseCase.CreateUser(useCases.CreateUserCommand{
		Username: credentials.Username,
		Password: credentials.Password,
		Email:    credentials.Email,
	})
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	_ = h.SetJwtCookie(&w, createdUser.ID)

	message, _ := json.Marshal(map[string]any{"message": "User registered! HTTPonly jwt cookie created"})
	_, _ = w.Write(message)
}

func (h *Handler) Logout(w http.ResponseWriter, _ *http.Request) {
	removeJwtCookie(w)
	message, _ := json.Marshal(map[string]any{"message": "User logged out! HTTPonly jwt cookie deleted"})
	_, _ = w.Write(message)
}

===== FILE: ./api/handlers/image/image.go =====
package image

import (
	"RenewCMS/internal/infrastructure/useCases"
	"encoding/json"
	"net/http"
	"strconv"

	"github.com/go-chi/chi/v5"
)

type Handler struct {
	CreateUseCase *useCases.CreateImageUseCase
	UpdateUseCase *useCases.UpdateArticleUseCase
}

func NewHandler(create *useCases.CreateImageUseCase, update *useCases.UpdateArticleUseCase) *Handler {
	return &Handler{
		CreateUseCase: create,
		UpdateUseCase: update,
	}
}

func (h *Handler) PostImage(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()

	id := chi.URLParam(r, "id")
	idInt, _ := strconv.Atoi(id)

	file, fileHeader, err := r.FormFile("file")
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	newImage, err := h.CreateUseCase.CreateImage(file, *fileHeader)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	err = h.UpdateUseCase.AddImage(uint32(idInt), newImage.ID)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	newJson := map[string]any{"location": newImage.Path}
	newJsonBytes, _ := json.Marshal(newJson)

	_, _ = w.Write(newJsonBytes)
}

===== FILE: ./api/middleware/auth.go =====
package middleware

import "net/http"

const KeyContentType = "Content-Type"

func JsonContentTypeMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(KeyContentType, "application/json")
		next.ServeHTTP(w, r)
	})
}

func HtmlContentTypeMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(KeyContentType, "text/html")
		next.ServeHTTP(w, r)
	})
}

===== FILE: ./api/router.go =====
package api

import (
	"RenewCMS/api/handlers/article"
	"RenewCMS/api/handlers/auth"
	"RenewCMS/api/middleware"
	"encoding/json"
	"net/http"
	"os"
	"strings"

	"github.com/MadAppGang/httplog"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/cors"
	"github.com/go-chi/jwtauth/v5"
)

func GetHelloWorld(w http.ResponseWriter, _ *http.Request) {
	msg, _ := json.Marshal(map[string]string{"message": "Hello World"})
	_, _ = w.Write(msg)
}

func NewArticleRouter(container *UseCases) http.Handler {
	r := chi.NewRouter()

	articleHandler := article.NewHandler(container.CreateArticleUseCase, container.GetArticleUseCase, container.ListArticlesUseCase, container.DeleteArticleUseCase)

	r.Get("/{id}", articleHandler.GetArticle)
	r.Post("/", articleHandler.PostArticle)
	r.Get("/", articleHandler.ListArticles)
	r.Delete("/{id}", articleHandler.DeleteArticle)
	return r
}

func NewAuthRouter(container *UseCases) http.Handler {
	r := chi.NewRouter()

	authHandler := auth.NewHandler(container.Token, container.GetUserUseCase, container.ListUsersUseCase, container.CreateUserUseCase)

	r.Post("/register", authHandler.Register)
	r.Post("/login", authHandler.Login)
	r.Post("/logout", authHandler.Logout)
	return r
}

func InitBackendRoutes(container *UseCases) *chi.Mux {
	r := chi.NewRouter()

	r.Use(httplog.LoggerWithName("backend"))
	r.Use(middleware.JsonContentTypeMiddleware)
	r.Get("/", GetHelloWorld)
	r.Group(func(r chi.Router) {
		r.Use(jwtauth.Verifier(container.Token))
		r.Use(jwtauth.Authenticator(container.Token))
		r.Mount("/article", NewArticleRouter(container))
	})
	r.Mount("/auth", NewAuthRouter(container))

	return r
}

func InitRoutes(container *UseCases) *chi.Mux {
	backend := InitBackendRoutes(container)

	apiRouter := chi.NewRouter()
	apiRouter.Use(cors.Handler(cors.Options{
		AllowedOrigins:   strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ";"),
		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
		AllowedHeaders:   []string{"Accept", "Authorization", middleware.KeyContentType, "X-CSRF-Token"},
		ExposedHeaders:   []string{"Link"},
		AllowCredentials: false,
		MaxAge:           300,
	}))
	apiRouter.Mount("/v1", backend)

	return apiRouter
}

===== FILE: ./api/pkg/validator/validator.go =====
package api

import "github.com/go-playground/validator/v10"

var Validate *validator.Validate

func InitValidator() {
	Validate = validator.New()
}

===== FILE: ./api/pkg/apperr/messages.go =====
package apperr

const LogsErrorMessage = "Access to the requested resource is forbidden due to incorrect password and/or username."
const BodyErrorMessage = "The request cannot be processed due to a mismatch in the format of the body."
const IdUint32ErrorMessage = "The server expects the ID to be in the format of an unsigned 32-bit integer (uint32)."

===== FILE: ./cmd/server/main.go =====
package main

import (
	"RenewCMS/api"
	validator "RenewCMS/api/pkg/validator"
	"fmt"
	"net/http"
	"os"

	"github.com/go-chi/chi/v5"
	"github.com/joho/godotenv"
)

var possibleEnvFileLocations = []string{".env", "../.env"}
var envVarsToLoad = []string{
	"HOST",
	"PORT",
	"JWT_SECRET",
	"ENVIRONMENT",
	"CORS_ALLOWED_ORIGINS",
	"DB_FILE",
	"SMTP_EMAIL",
	"SMTP_PASSWORD",
	"SMTP_HOST",
	"SMTP_PORT",
}

func initEnvVariables() {
	var err error
	for _, envLocation := range possibleEnvFileLocations {
		err = godotenv.Load(envLocation)
		if err == nil {
			break
		}
	}
	if err != nil {
		fmt.Println("WARNING: Could not load any .env file")
	}

	for _, envVar := range envVarsToLoad {
		if _, ok := os.LookupEnv(envVar); !ok {
			panic(fmt.Sprintf("Environment variable %s is not set", envVar))
		}
	}
}

func initServer() *chi.Mux {
	initEnvVariables()

	container := api.InitContainer()
	validator.InitValidator()

	return api.InitRoutes(container)
}

func startServer(router *chi.Mux) error {
	fmt.Println("Server starting on http://" + os.Getenv("HOST"))
	err := http.ListenAndServe(":"+os.Getenv("PORT"), router)
	return err
}

func main() {
	router := initServer()
	err := startServer(router)
	if err != nil {
		panic(err)
	}
}

===== FILE: ./internal/domain/article/article.go =====
package article

import (
	domain "RenewCMS/internal/domain/image"
	entity "RenewCMS/internal/infrastructure/persistence/models"
	"time"
)

type Article struct {
	ID        uint32          `json:"id"`
	Title     string          `json:"title"`
	Body      string          `json:"body"`
	Images    []*domain.Image `json:"images"`
	IsOnline  bool            `json:"is_online"`
	CreatedAt time.Time       `json:"created_at"`
	UpdatedAt time.Time       `json:"updated_at"`
}

func FromApi(
	title string,
	body string,
) Article {
	return Article{
		Title: title,
		Body:  body,
	}
}

func FromDb(
	id uint32,
	title string,
	body string,
	images []*entity.Image,
	isOnline bool,
	createdAt time.Time,
	updatedAt time.Time,
) Article {
	domainImages := make([]*domain.Image, len(images))
	for i, img := range images {
		domainImage := domain.FromDB(
			img.ID,
			img.Path,
			img.ArticleID,
			img.CreatedAt,
			img.UpdatedAt,
		)
		domainImages[i] = &domainImage
	}
	return Article{
		ID:        id,
		Title:     title,
		Body:      body,
		Images:    domainImages,
		IsOnline:  isOnline,
		CreatedAt: createdAt,
		UpdatedAt: updatedAt,
	}
}

===== FILE: ./internal/domain/article/repository.go =====
package article

type Repository interface {
	Get(id uint32) (Article, error)
	GetByName(name string) (Article, error)
	GetAll() []Article
	Create(post Article) (Article, error)
	UpdateBody(id uint32, body string) (Article, error)
	UpdateIsOnline(id uint32, isOnline bool) (Article, error)
	Delete(id uint32) error
	AddImage(postId uint32, imageId uint32) error
}

===== FILE: ./internal/domain/image/image.go =====
package image

import (
	"time"
)

type Image struct {
	ID        uint32    `json:"id"`
	Path      string    `json:"path"`
	ArticleID uint32    `json:"article_id"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

func FromDB(id uint32, path string, articleId uint32, createdAt time.Time, updatedAt time.Time) Image {
	return Image{
		ID:        id,
		Path:      path,
		ArticleID: articleId,
		CreatedAt: createdAt,
		UpdatedAt: updatedAt,
	}
}

===== FILE: ./internal/domain/image/repository.go =====
package image

import (
	"mime/multipart"
)

type Repository interface {
	Create(file multipart.File, fileHeader multipart.FileHeader) (Image, error)
	Delete(id uint32) error
}

===== FILE: ./internal/domain/user/user.go =====
package user

import (
	"time"
)

type User struct {
	ID                     uint32    `json:"id"`
	Username               string    `json:"username"`
	Password               string    `json:"password"`
	PasswordResetCode      string    `json:"password_reset_code"`
	Email                  string    `json:"email"`
	IsVerified             bool      `json:"is_verified"`
	VerificationCode       string    `json:"verification_code"`
	VerificationExpiration time.Time `json:"verification_expiration"`
	CreatedAt              time.Time `json:"created_at"`
	UpdatedAt              time.Time `json:"updated_at"`
}

func FromApi(
	username string,
	password string,
	passwordResetCode string,
	email string,
	verificationCode string,
) User {
	expiration := time.Now().Add(2 * time.Hour)
	return User{
		Username:               username,
		Password:               password,
		PasswordResetCode:      passwordResetCode,
		Email:                  email,
		IsVerified:             false,
		VerificationCode:       verificationCode,
		VerificationExpiration: expiration,
	}
}

func FromDb(
	id uint32,
	username string,
	password string,
	passwordResetCode string,
	email string,
	isVerified bool,
	verificationCode string,
	verificationExpiration time.Time,
	createdAt time.Time,
	updatedAt time.Time,
) User {
	return User{
		ID:                     id,
		Username:               username,
		Password:               password,
		PasswordResetCode:      passwordResetCode,
		Email:                  email,
		IsVerified:             isVerified,
		VerificationCode:       verificationCode,
		VerificationExpiration: verificationExpiration,
		CreatedAt:              createdAt,
		UpdatedAt:              updatedAt,
	}
}

===== FILE: ./internal/domain/user/repository.go =====
package user

type Repository interface {
	Get(id uint32) (User, error)
	GetByUsername(username string) (User, error)
	GetByEmail(email string) (User, error)
	GetAll() []User
	Create(user User) (User, error)
	Delete(id uint32) error
	UpdateVerificationStatus(userId uint32, isVerified bool) (User, error)
	UpdatePassword(userId uint32, password string) (User, error)
	UpdatePasswordResetCode(userId uint32, code string) (User, error)
}

===== FILE: ./internal/domain/mail/repository.go =====
package mail

type Repository interface {
	Send(receiverAddress string, templateName string, data any) error
}

===== FILE: ./internal/infrastructure/persistence/userRepository.go =====
package persistence

import (
	domainUser "RenewCMS/internal/domain/user"
	entity "RenewCMS/internal/infrastructure/persistence/models"

	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
)

type UserRepository struct {
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
	return &UserRepository{db}
}

func mapUserToDomain(user entity.User) domainUser.User {
	return domainUser.FromDb(
		user.ID,
		user.Username,
		user.Password,
		user.PasswordResetCode,
		user.Email,
		user.IsVerified,
		user.VerificationCode,
		user.VerificationExpiration,
		user.CreatedAt, user.UpdatedAt,
	)
}

func (u *UserRepository) Get(id uint32) (domainUser.User, error) {
	var localUser entity.User
	err := u.db.Model(&entity.User{}).First(&localUser, id).Error
	if err != nil {
		return domainUser.User{}, err
	}

	return mapUserToDomain(localUser), nil
}

func (u *UserRepository) Create(user domainUser.User) (domainUser.User, error) {
	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
	hashedVerificationCode, _ := bcrypt.GenerateFromPassword([]byte(user.VerificationCode), 12)

	creationResult := u.db.Create(&entity.User{
		Username:               user.Username,
		Password:               string(hashedPassword),
		Email:                  user.Email,
		VerificationCode:       string(hashedVerificationCode),
		VerificationExpiration: user.VerificationExpiration,
	})
	if creationResult.Error != nil {
		return domainUser.User{}, creationResult.Error
	}

	var createdUser entity.User
	creationResult.Scan(&createdUser)

	return mapUserToDomain(createdUser),
		nil
}

func (u *UserRepository) Delete(id uint32) error {
	return u.db.Delete(&domainUser.User{}, id).Error
}

func (u *UserRepository) GetAll() []domainUser.User {
	var users []entity.User
	u.db.Model(&entity.User{}).Find(&users)

	var domainUsers []domainUser.User
	for _, localUser := range users {
		domainUsers = append(domainUsers, mapUserToDomain(localUser))
	}

	return domainUsers
}

func (u *UserRepository) GetByUsername(username string) (domainUser.User, error) {
	var localUser entity.User
	err := u.db.Model(&entity.User{}).Where("username = ?", username).First(&localUser).Error
	if err != nil {
		return domainUser.User{}, err
	}

	return mapUserToDomain(localUser), nil
}

func (u *UserRepository) GetByEmail(email string) (domainUser.User, error) {
	var localUser entity.User
	err := u.db.Model(&entity.User{}).Where("email = ?", email).First(&localUser).Error
	if err != nil {
		return domainUser.User{}, err
	}

	return mapUserToDomain(localUser), nil
}

func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool) (domainUser.User, error) {
	var localUser entity.User
	err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
	if err != nil {
		return domainUser.User{}, err
	}

	localUser.IsVerified = isVerified
	err = u.db.Save(&localUser).Error
	if err != nil {
		return domainUser.User{}, err
	}

	return mapUserToDomain(localUser), nil
}

func (u *UserRepository) UpdatePassword(userId uint32, password string) (domainUser.User, error) {
	var localUser entity.User
	err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
	if err != nil {
		return domainUser.User{}, err
	}

	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
	localUser.Password = string(hashedPassword)
	err = u.db.Save(&localUser).Error
	if err != nil {
		return domainUser.User{}, err
	}

	return mapUserToDomain(localUser), nil
}

func (u *UserRepository) UpdatePasswordResetCode(userId uint32, code string) (domainUser.User, error) {
	var localUser entity.User
	err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
	if err != nil {
		return domainUser.User{}, err
	}

	hashedCode, _ := bcrypt.GenerateFromPassword([]byte(code), 12)
	localUser.PasswordResetCode = string(hashedCode)
	err = u.db.Save(&localUser).Error
	if err != nil {
		return domainUser.User{}, err
	}

	return mapUserToDomain(localUser), nil
}

var _ domainUser.Repository = &UserRepository{}

===== FILE: ./internal/infrastructure/persistence/articleRepository.go =====
package persistence

import (
	domain "RenewCMS/internal/domain/article"
	entity "RenewCMS/internal/infrastructure/persistence/models"

	"gorm.io/gorm"
)

type ArticleRepository struct {
	db *gorm.DB
}

func NewArticleRepository(db *gorm.DB) *ArticleRepository {
	return &ArticleRepository{db}
}

func mapArticleToDomain(article entity.Article) domain.Article {
	return domain.FromDb(article.ID, article.Title, article.Body, article.Images, article.IsOnline, article.CreatedAt, article.UpdatedAt)
}

func (a *ArticleRepository) Get(id uint32) (domain.Article, error) {
	var article entity.Article
	err := a.db.Model(&entity.Article{}).Preload("Images").First(&article, id).Error
	if err != nil {
		return domain.Article{}, err
	}

	return mapArticleToDomain(article), nil
}

func (a *ArticleRepository) GetByName(name string) (domain.Article, error) {
	var article entity.Article
	err := a.db.Model(&entity.Article{}).Where("title = ?", name).First(&article).Error
	if err != nil {
		return domain.Article{}, err
	}

	return mapArticleToDomain(article), nil
}

func (a *ArticleRepository) Create(article domain.Article) (domain.Article, error) {
	creationResult := a.db.Create(&entity.Article{
		Title: article.Title,
		Body:  article.Body,
	})
	if creationResult.Error != nil {
		return domain.Article{}, creationResult.Error
	}

	var createdArticle entity.Article
	creationResult.Scan(&createdArticle)

	return mapArticleToDomain(createdArticle), nil
}

func (a *ArticleRepository) GetAll() []domain.Article {
	var articles []entity.Article
	err := a.db.Model(&entity.Article{}).Find(&articles).Error
	if err != nil {
		return []domain.Article{}
	}

	var domainArticles = make([]domain.Article, 0)
	for _, article := range articles {
		domainArticles = append(domainArticles, mapArticleToDomain(article))
	}

	return domainArticles
}

func (a *ArticleRepository) UpdateBody(id uint32, body string) (domain.Article, error) {
	var localArticle entity.Article
	err := a.db.Model(&entity.Article{}).First(&localArticle, id).Error
	if err != nil {
		return domain.Article{}, err
	}

	localArticle.Body = body
	err = a.db.Save(&localArticle).Error
	if err != nil {
		return domain.Article{}, err
	}

	newArticle := domain.FromDb(
		localArticle.ID,
		localArticle.Title,
		localArticle.Body,
		localArticle.Images,
		localArticle.IsOnline,
		localArticle.CreatedAt,
		localArticle.UpdatedAt,
	)

	return newArticle, nil
}

func (a *ArticleRepository) UpdateIsOnline(id uint32, isOnline bool) (domain.Article, error) {
	var localArticle entity.Article
	err := a.db.Model(&entity.Article{}).First(&localArticle, id).Error
	if err != nil {
		return domain.Article{}, err
	}

	localArticle.IsOnline = isOnline
	err = a.db.Save(&localArticle).Error
	if err != nil {
		return domain.Article{}, err
	}

	return mapArticleToDomain(localArticle), nil
}

func (a *ArticleRepository) Delete(id uint32) error {
	return a.db.Delete(&entity.Article{}, id).Error
}

func (a *ArticleRepository) AddImage(articleId uint32, imageId uint32) error {
	var localArticle entity.Article
	err := a.db.Model(&entity.Article{}).First(&localArticle, articleId).Error
	if err != nil {
		return err
	}

	var localImage entity.Image
	err = a.db.Model(&entity.Image{}).First(&localImage, imageId).Error
	if err != nil {
		return err
	}

	err = a.db.Model(&localArticle).Association("Images").Append(&localImage)
	if err != nil {
		return err
	}

	return nil
}

var _ domain.Repository = &ArticleRepository{}

===== FILE: ./internal/infrastructure/persistence/imageRepository.go =====
package persistence

import (
	domain "RenewCMS/internal/domain/image"
	entity "RenewCMS/internal/infrastructure/persistence/models"
	"errors"
	"mime/multipart"
	"os"

	"github.com/google/uuid"
	"gorm.io/gorm"
)

type ImageRepository struct {
	db *gorm.DB
}

func NewImageRepository(db *gorm.DB) *ImageRepository {
	return &ImageRepository{db}
}

var contentTypeExtensions = map[string]string{
	"image/png":     ".png",
	"image/jpeg":    ".jpeg",
	"image/webp":    ".webp",
	"image/svg+xml": ".svg",
}

func mapImageToDomain(image entity.Image) domain.Image {
	return domain.FromDB(
		image.ID,
		image.Path,
		image.ArticleID,
		image.CreatedAt,
		image.UpdatedAt,
	)
}

func (i ImageRepository) Create(file multipart.File, fileHeader multipart.FileHeader) (domain.Image, error) {
	fileBytes := make([]byte, fileHeader.Size)
	_, err := file.Read(fileBytes)
	if err != nil {
		return domain.Image{}, err
	}

	err = os.MkdirAll("api/static/uploadedImages/", 0666)
	if err != nil {
		return domain.Image{}, err
	}

	contentType := fileHeader.Header.Get("Content-Type")
	extension := contentTypeExtensions[contentType]
	if extension == "" {
		return domain.Image{}, errors.New("the file must be a PNG, JPEG, WEBP, or SVG image")
	}

	newName := uuid.NewString() + extension
	err = os.WriteFile("api/static/uploadedImages/"+newName, fileBytes, 0666)
	if err != nil {
		return domain.Image{}, err
	}

	newImage := i.db.Create(&domain.Image{Path: "/static/uploadedImages/" + newName})
	var createdImage entity.Image
	newImage.Scan(&createdImage)

	return mapImageToDomain(createdImage), nil
}

func (i ImageRepository) Delete(id uint32) error {
	var image entity.Image
	err := i.db.Model(&entity.Image{}).First(&image, id).Error
	if err != nil {
		return err
	}

	err = os.Remove("api" + image.Path)
	if err != nil {
		return err
	}

	return i.db.Delete(&entity.Image{}, id).Error
}

var _ domain.Repository = &ImageRepository{}

===== FILE: ./internal/infrastructure/persistence/models/article.go =====
package models

import (
	"time"

	"gorm.io/gorm"
)

type Article struct {
	gorm.Model
	ID        uint32 `gorm:"primary_key;auto_increment;not_null"`
	Title     string
	Body      string
	Images    []*Image  `gorm:"many2many:article_images;"`
	IsOnline  bool      `gorm:"not_null;default:false"`
	CreatedAt time.Time `gorm:"autoCreateTime"`
	UpdatedAt time.Time `gorm:"autoUpdateTime"`
}

===== FILE: ./internal/infrastructure/persistence/models/image.go =====
package models

import (
	"time"

	"gorm.io/gorm"
)

type Image struct {
	gorm.Model
	ID        uint32 `gorm:"primary_key;auto_increment;not_null"`
	Path      string
	ArticleID uint32
	CreatedAt time.Time `gorm:"autoCreateTime"`
	UpdatedAt time.Time `gorm:"autoUpdateTime"`
}

===== FILE: ./internal/infrastructure/persistence/models/user.go =====
package models

import (
	"gorm.io/gorm"
	"time"
)

type User struct {
	gorm.Model
	ID                     uint32 `gorm:"primaryKey;autoIncrement"`
	Username               string `gorm:"unique;not null"`
	Password               string `gorm:"not null"`
	PasswordResetCode      string `gorm:"unique"`
	Email                  string `gorm:"unique;not null"`
	IsVerified             bool   `gorm:"default=false;not null"`
	VerificationCode       string `gorm:"unique"`
	VerificationExpiration time.Time
	CreatedAt              time.Time `gorm:"autoCreateTime"`
	UpdatedAt              time.Time `gorm:"autoUpdateTime"`
}

===== FILE: ./internal/infrastructure/mailer/mailRepository.go =====
package mailer

import (
	domain "RenewCMS/internal/domain/mail"
	"bytes"
	"embed"
	"html/template"
	"log"
	"os"
	"strconv"

	"gopkg.in/gomail.v2"
)

type MailRepository struct{}

//go:embed templates/*
var mailTemplateFiles embed.FS

func NewMailRepository() *MailRepository {
	return &MailRepository{}
}

func (m MailRepository) Send(receiverAddress string, templateName string, data any) error {
	from := os.Getenv("SMTP_EMAIL")
	password := os.Getenv("SMTP_PASSWORD")
	smtpHost := os.Getenv("SMTP_HOST")
	smtpPort, _ := strconv.Atoi(os.Getenv("SMTP_PORT"))

	d := gomail.NewDialer(smtpHost, smtpPort, from, password)

	tmpl, err := template.ParseFS(mailTemplateFiles, "mail/templates/"+templateName+".html")
	if err != nil {
		log.Println("error when template.ParseFS:", err)
		return err
	}

	var body bytes.Buffer
	err = tmpl.Execute(&body, data)
	if err != nil {
		log.Println("error when tmpl.Execute:", err)
		return err
	}

	msg := gomail.NewMessage()
	msg.SetHeaders(map[string][]string{
		"From":         {"RenewCMS <" + from + ">"},
		"To":           {receiverAddress},
		"MIME-version": {"1.0"},
		"Content-Type": {"text/html"},
		"charset":      {"UTF-8"},
		"Subject":      {"RenewCMS | Action required"},
	})
	msg.SetBody("text/html", body.String())

	if err := d.DialAndSend(msg); err != nil {
		log.Println("error when sending email:", err)
		return err
	}

	return nil
}

var _ domain.Repository = &MailRepository{}

===== FILE: ./internal/infrastructure/useCases/CreateArticle.go =====
package useCases

import (
	"RenewCMS/internal/domain/article"
)

type CreateArticleUseCase struct {
	articleRepository article.Repository
}

type CreateArticleCommand struct {
	Title string
	Body  string
}

func NewCreateArticleUseCase(articleRepository article.Repository) *CreateArticleUseCase {
	return &CreateArticleUseCase{articleRepository}
}

func (g *CreateArticleUseCase) CreateArticle(createArticle CreateArticleCommand) (article.Article, error) {
	return g.articleRepository.Create(article.FromApi(
		createArticle.Title,
		createArticle.Body,
	))
}

===== FILE: ./internal/infrastructure/useCases/CreateImage.go =====
package useCases

import (
	"RenewCMS/internal/domain/image"
	"mime/multipart"
)

type CreateImageUseCase struct {
	imageRepository image.Repository
}

func NewCreateImageUseCase(imageRepository image.Repository) *CreateImageUseCase {
	return &CreateImageUseCase{imageRepository}
}

func (g *CreateImageUseCase) CreateImage(file multipart.File, fileHeader multipart.FileHeader) (image.Image, error) {
	return g.imageRepository.Create(file, fileHeader)
}

===== FILE: ./internal/infrastructure/useCases/CreateUser.go =====
package useCases

import (
	"RenewCMS/internal/domain/user"

	"github.com/google/uuid"
)

type CreateUserUseCase struct {
	userRepository user.Repository
}

type CreateUserCommand struct {
	Username string
	Password string
	Email    string
}

func NewCreateUserUseCase(userRepository user.Repository) *CreateUserUseCase {
	return &CreateUserUseCase{userRepository}
}

func (g *CreateUserUseCase) CreateUser(createUser CreateUserCommand) (user.User, error) {
	rawUuid := uuid.NewString()

	createdUser, err := g.userRepository.Create(user.FromApi(
		createUser.Username,
		createUser.Password,
		"",
		createUser.Email,
		rawUuid,
	))
	if err != nil {
		return user.User{}, err
	}

	createdUser.VerificationCode = rawUuid
	return createdUser, nil
}

===== FILE: ./internal/infrastructure/useCases/DeleteArticle.go =====
package useCases

import "RenewCMS/internal/domain/article"

type DeleteArticleUseCase struct {
	articleRepository article.Repository
}

func NewDeleteArticleUseCase(articleRepository article.Repository) *DeleteArticleUseCase {
	return &DeleteArticleUseCase{articleRepository}
}

func (g *DeleteArticleUseCase) DeleteArticle(userId uint32) error {
	return g.articleRepository.Delete(userId)
}

===== FILE: ./internal/infrastructure/useCases/DeleteImage.go =====
package useCases

import "RenewCMS/internal/domain/image"

type DeleteImageUseCase struct {
	imageRepository image.Repository
}

func NewDeleteImageUseCase(imageRepository image.Repository) *DeleteImageUseCase {
	return &DeleteImageUseCase{imageRepository}
}

func (g *DeleteImageUseCase) DeleteImage(imageId uint32) error {
	return g.imageRepository.Delete(imageId)
}

===== FILE: ./internal/infrastructure/useCases/DeleteUser.go =====
package useCases

import "RenewCMS/internal/domain/user"

type DeleteUserUseCase struct {
	userRepository user.Repository
}

func NewDeleteUserUseCase(userRepository user.Repository) *DeleteUserUseCase {
	return &DeleteUserUseCase{userRepository}
}

func (g *DeleteUserUseCase) DeleteUser(userId uint32) error {
	return g.userRepository.Delete(userId)
}

===== FILE: ./internal/infrastructure/useCases/GetArticle.go =====
package useCases

import "RenewCMS/internal/domain/article"

type GetArticleUseCase struct {
	articleRepository article.Repository
}

func NewGetArticleUseCase(articleRepository article.Repository) *GetArticleUseCase {
	return &GetArticleUseCase{articleRepository}
}

func (g *GetArticleUseCase) GetArticle(id uint32) (article.Article, error) {
	return g.articleRepository.Get(id)
}

func (g *GetArticleUseCase) GetArticleByName(name string) (article.Article, error) {
	return g.articleRepository.GetByName(name)
}

===== FILE: ./internal/infrastructure/useCases/GetUser.go =====
package useCases

import "RenewCMS/internal/domain/user"

type GetUserUseCase struct {
	userRepository user.Repository
}

func NewGetUserUseCase(userRepository user.Repository) *GetUserUseCase {
	return &GetUserUseCase{userRepository}
}

func (g *GetUserUseCase) GetUser(id uint32) (user.User, error) {
	return g.userRepository.Get(id)
}

func (g *GetUserUseCase) GetUserByUsername(username string) (user.User, error) {
	return g.userRepository.GetByUsername(username)
}

func (g *GetUserUseCase) GetUserByEmail(email string) (user.User, error) {
	return g.userRepository.GetByEmail(email)
}

===== FILE: ./internal/infrastructure/useCases/ListArticles.go =====
package useCases

import "RenewCMS/internal/domain/article"

type ListArticlesUseCase struct {
	articleRepository article.Repository
}

func NewListArticlesUseCase(articleRepository article.Repository) *ListArticlesUseCase {
	return &ListArticlesUseCase{articleRepository}
}

func (g *ListArticlesUseCase) ListArticles() []article.Article {
	return g.articleRepository.GetAll()
}

===== FILE: ./internal/infrastructure/useCases/ListUsers.go =====
package useCases

import "RenewCMS/internal/domain/user"

type ListUsersUseCase struct {
	userRepository user.Repository
}

func NewListUsersUseCase(userRepository user.Repository) *ListUsersUseCase {
	return &ListUsersUseCase{userRepository}
}

func (g *ListUsersUseCase) ListUsers() []user.User {
	return g.userRepository.GetAll()
}

===== FILE: ./internal/infrastructure/useCases/SendMail.go =====
package useCases

import "RenewCMS/internal/domain/mail"

type SendMailUseCase struct {
	mailRepository mail.Repository
}

func NewSendMailUseCase(mailRepository mail.Repository) *SendMailUseCase {
	return &SendMailUseCase{mailRepository}
}

func (g *SendMailUseCase) SendMail(receiverAddress string, templateName string, data any) error {
	return g.mailRepository.Send(receiverAddress, templateName, data)
}

===== FILE: ./internal/infrastructure/useCases/UpdateArticle.go =====
package useCases

import "RenewCMS/internal/domain/article"

type UpdateArticleUseCase struct {
	articleRepository article.Repository
}

func NewUpdateArticleUseCase(articleRepository article.Repository) *UpdateArticleUseCase {
	return &UpdateArticleUseCase{articleRepository}
}

func (g *UpdateArticleUseCase) UpdateBody(id uint32, body string) (article.Article, error) {
	return g.articleRepository.UpdateBody(id, body)
}

func (g *UpdateArticleUseCase) AddImage(articleId uint32, imageId uint32) error {
	return g.articleRepository.AddImage(articleId, imageId)
}

func (g *UpdateArticleUseCase) UpdateIsOnline(id uint32, isOnline bool) (article.Article, error) {
	return g.articleRepository.UpdateIsOnline(id, isOnline)
}

===== FILE: ./internal/infrastructure/useCases/UpdateUser.go =====
package useCases

import "RenewCMS/internal/domain/user"

type UpdateUserUseCase struct {
	userRepository user.Repository
}

func NewUpdateUserUseCase(userRepository user.Repository) *UpdateUserUseCase {
	return &UpdateUserUseCase{userRepository}
}

func (g *UpdateUserUseCase) UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error) {
	return g.userRepository.UpdateVerificationStatus(userId, isVerified)
}

func (g *UpdateUserUseCase) UpdatePasswordResetCode(userId uint32, code string) (user.User, error) {
	return g.userRepository.UpdatePasswordResetCode(userId, code)
}

func (g *UpdateUserUseCase) UpdatePassword(userId uint32, password string) (user.User, error) {
	return g.userRepository.UpdatePassword(userId, password)
}

