===== 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) }