mirror of
https://github.com/Floriansylvain/RenewCMS.git
synced 2026-08-19 19:53:21 +02:00
1633 lines
41 KiB
Plaintext
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)
|
|
}
|
|
|