Files
RenewCMS/all_go_files.txt
T

1633 lines
41 KiB
Plaintext

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