initial commit
This commit is contained in:
commit
e7987e88a7
70 changed files with 13661 additions and 0 deletions
134
internal/config/config.go
Normal file
134
internal/config/config.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Redis RedisConfig `yaml:"redis"`
|
||||
JWT JWTConfig `yaml:"jwt"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
Logging LoggingConfig `yaml:"logging"`
|
||||
Email EmailConfig `yaml:"email"`
|
||||
Frontend FrontendConfig `yaml:"frontend"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Mode string `yaml:"mode"`
|
||||
ReadTimeout int `yaml:"read_timeout"`
|
||||
WriteTimeout int `yaml:"write_timeout"`
|
||||
IdleTimeout int `yaml:"idle_timeout"`
|
||||
}
|
||||
|
||||
func (s *ServerConfig) Address() string {
|
||||
return s.Host + ":" + strconv.Itoa(s.Port)
|
||||
}
|
||||
|
||||
func (s *ServerConfig) ReadTimeoutDuration() time.Duration {
|
||||
return time.Duration(s.ReadTimeout) * time.Second
|
||||
}
|
||||
|
||||
func (s *ServerConfig) WriteTimeoutDuration() time.Duration {
|
||||
return time.Duration(s.WriteTimeout) * time.Second
|
||||
}
|
||||
|
||||
func (s *ServerConfig) IdleTimeoutDuration() time.Duration {
|
||||
return time.Duration(s.IdleTimeout) * time.Second
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
Name string `yaml:"name"`
|
||||
SSLMode string `yaml:"ssl_mode"`
|
||||
MaxOpenConns int `yaml:"max_open_conns"`
|
||||
MaxIdleConns int `yaml:"max_idle_conns"`
|
||||
ConnMaxLifetime int `yaml:"conn_max_lifetime"`
|
||||
}
|
||||
|
||||
func (d *DatabaseConfig) DSN() string {
|
||||
return "host=" + d.Host + " port=" + strconv.Itoa(d.Port) + " user=" + d.User + " password=" + d.Password + " dbname=" + d.Name + " sslmode=" + d.SSLMode
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Password string `yaml:"password"`
|
||||
DB int `yaml:"db"`
|
||||
}
|
||||
|
||||
func (r *RedisConfig) Address() string {
|
||||
return r.Host + ":" + strconv.Itoa(r.Port)
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string `yaml:"secret"`
|
||||
ExpiresIn int `yaml:"expires_in"`
|
||||
RefreshExpiresIn int `yaml:"refresh_expires_in"`
|
||||
}
|
||||
|
||||
func (j *JWTConfig) ExpiresInDuration() time.Duration {
|
||||
return time.Duration(j.ExpiresIn) * time.Hour
|
||||
}
|
||||
|
||||
func (j *JWTConfig) RefreshExpiresInDuration() time.Duration {
|
||||
return time.Duration(j.RefreshExpiresIn) * time.Hour
|
||||
}
|
||||
|
||||
type CORSConfig struct {
|
||||
AllowedOrigins []string `yaml:"allowed_origins"`
|
||||
AllowedMethods []string `yaml:"allowed_methods"`
|
||||
AllowedHeaders []string `yaml:"allowed_headers"`
|
||||
AllowCredentials bool `yaml:"allow_credentials"`
|
||||
MaxAge int `yaml:"max_age"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
Format string `yaml:"format"`
|
||||
Output string `yaml:"output"`
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
SMTPHost string `yaml:"smtp_host"`
|
||||
SMTPPort int `yaml:"smtp_port"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
FromAddress string `yaml:"from_address"`
|
||||
FromName string `yaml:"from_name"`
|
||||
}
|
||||
|
||||
type FrontendConfig struct {
|
||||
BuildPath string `yaml:"build_path"`
|
||||
SPAFallback bool `yaml:"spa_fallback"`
|
||||
}
|
||||
|
||||
var cfg *Config
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg = &Config{}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func Get() *Config {
|
||||
return cfg
|
||||
}
|
||||
162
internal/handler/handler.go
Normal file
162
internal/handler/handler.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/dnoulet/go-first/internal/model"
|
||||
"github.com/dnoulet/go-first/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
skillService *service.SkillService
|
||||
projectService *service.ProjectService
|
||||
experienceService *service.ExperienceService
|
||||
serviceService *service.ServiceService
|
||||
contactService *service.ContactService
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
skillService *service.SkillService,
|
||||
projectService *service.ProjectService,
|
||||
experienceService *service.ExperienceService,
|
||||
serviceService *service.ServiceService,
|
||||
contactService *service.ContactService,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
skillService: skillService,
|
||||
projectService: projectService,
|
||||
experienceService: experienceService,
|
||||
serviceService: serviceService,
|
||||
contactService: contactService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterRoutes(r *gin.Engine) {
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
api.GET("/health", h.HealthCheck)
|
||||
api.GET("/skills", h.GetSkills)
|
||||
api.GET("/skills/:id", h.GetSkill)
|
||||
api.GET("/projects", h.GetProjects)
|
||||
api.GET("/projects/featured", h.GetFeaturedProjects)
|
||||
api.GET("/projects/:id", h.GetProject)
|
||||
api.GET("/experience", h.GetExperience)
|
||||
api.GET("/services", h.GetServices)
|
||||
api.POST("/contact", h.SubmitContact)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) HealthCheck(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, model.SuccessResponse(gin.H{
|
||||
"status": "ok",
|
||||
"service": "go-first",
|
||||
"version": "1.0.0",
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handler) GetSkills(c *gin.Context) {
|
||||
category := c.Query("category")
|
||||
skills, err := h.skillService.GetAll(c.Request.Context(), category)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get skills")
|
||||
c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve skills", err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.SuccessResponse(skills))
|
||||
}
|
||||
|
||||
func (h *Handler) GetSkill(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.ErrorResponse("INVALID_ID", "Invalid skill ID", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
skill, err := h.skillService.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("id", idStr).Msg("Failed to get skill")
|
||||
c.JSON(http.StatusNotFound, model.ErrorResponse("NOT_FOUND", "Skill not found", err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.SuccessResponse(skill))
|
||||
}
|
||||
|
||||
func (h *Handler) GetProjects(c *gin.Context) {
|
||||
category := c.Query("category")
|
||||
featured := c.Query("featured") == "true"
|
||||
projects, err := h.projectService.GetAll(c.Request.Context(), category, featured)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get projects")
|
||||
c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve projects", err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.SuccessResponse(projects))
|
||||
}
|
||||
|
||||
func (h *Handler) GetFeaturedProjects(c *gin.Context) {
|
||||
projects, err := h.projectService.GetFeatured(c.Request.Context())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get featured projects")
|
||||
c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve featured projects", err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.SuccessResponse(projects))
|
||||
}
|
||||
|
||||
func (h *Handler) GetProject(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.ErrorResponse("INVALID_ID", "Invalid project ID", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
project, err := h.projectService.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("id", idStr).Msg("Failed to get project")
|
||||
c.JSON(http.StatusNotFound, model.ErrorResponse("NOT_FOUND", "Project not found", err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.SuccessResponse(project))
|
||||
}
|
||||
|
||||
func (h *Handler) GetExperience(c *gin.Context) {
|
||||
experience, err := h.experienceService.GetAll(c.Request.Context())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get experience")
|
||||
c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve experience", err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.SuccessResponse(experience))
|
||||
}
|
||||
|
||||
func (h *Handler) GetServices(c *gin.Context) {
|
||||
services, err := h.serviceService.GetAll(c.Request.Context())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get services")
|
||||
c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve services", err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.SuccessResponse(services))
|
||||
}
|
||||
|
||||
func (h *Handler) SubmitContact(c *gin.Context) {
|
||||
var req model.ContactForm
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.ErrorResponse("VALIDATION_ERROR", "Invalid request body", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
submission, err := h.contactService.Create(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create contact submission")
|
||||
c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to submit contact form", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, model.SuccessResponse(submission))
|
||||
}
|
||||
100
internal/middleware/middleware.go
Normal file
100
internal/middleware/middleware.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Logger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
c.Next()
|
||||
|
||||
latency := time.Since(start)
|
||||
clientIP := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
statusCode := c.Writer.Status()
|
||||
bodySize := c.Writer.Size()
|
||||
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("method", method).
|
||||
Str("path", path).
|
||||
Int("status", statusCode).
|
||||
Dur("latency", latency).
|
||||
Str("client_ip", clientIP).
|
||||
Int("body_size", bodySize).
|
||||
Msg("HTTP request")
|
||||
}
|
||||
}
|
||||
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
||||
log.Error().
|
||||
Interface("error", recovered).
|
||||
Str("path", c.Request.URL.Path).
|
||||
Msg("Panic recovered")
|
||||
c.JSON(500, gin.H{
|
||||
"success": false,
|
||||
"error": gin.H{
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": "Internal server error",
|
||||
},
|
||||
})
|
||||
c.Abort()
|
||||
})
|
||||
}
|
||||
|
||||
func CORS(allowedOrigins []string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
|
||||
allowed := false
|
||||
for _, o := range allowedOrigins {
|
||||
if o == "*" || o == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func SecurityHeaders() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
c.Writer.Header().Set("X-Frame-Options", "DENY")
|
||||
c.Writer.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
c.Writer.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
c.Writer.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RateLimiter(requests int, window time.Duration) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
128
internal/model/models.go
Normal file
128
internal/model/models.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Skill struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name" validate:"required"`
|
||||
Category string `json:"category" db:"category" validate:"required"`
|
||||
Proficiency int `json:"proficiency" db:"proficiency" validate:"min=1,max=5"`
|
||||
Icon string `json:"icon,omitempty" db:"icon"`
|
||||
Order int `json:"order" db:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title" validate:"required"`
|
||||
Description string `json:"description" db:"description" validate:"required"`
|
||||
ShortDesc string `json:"short_desc" db:"short_desc"`
|
||||
TechStack []string `json:"tech_stack" db:"tech_stack"`
|
||||
ImageURL string `json:"image_url,omitempty" db:"image_url"`
|
||||
DemoURL string `json:"demo_url,omitempty" db:"demo_url"`
|
||||
RepoURL string `json:"repo_url,omitempty" db:"repo_url"`
|
||||
Category string `json:"category" db:"category"`
|
||||
Featured bool `json:"featured" db:"featured"`
|
||||
Order int `json:"order" db:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type Experience struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Company string `json:"company" db:"company" validate:"required"`
|
||||
Role string `json:"role" db:"role" validate:"required"`
|
||||
Location string `json:"location" db:"location"`
|
||||
StartDate time.Time `json:"start_date" db:"start_date" validate:"required"`
|
||||
EndDate *time.Time `json:"end_date,omitempty" db:"end_date"`
|
||||
Current bool `json:"current" db:"current"`
|
||||
Description string `json:"description" db:"description"`
|
||||
TechStack []string `json:"tech_stack" db:"tech_stack"`
|
||||
Order int `json:"order" db:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title" validate:"required"`
|
||||
Description string `json:"description" db:"description" validate:"required"`
|
||||
Icon string `json:"icon,omitempty" db:"icon"`
|
||||
Features []string `json:"features" db:"features"`
|
||||
Order int `json:"order" db:"sort_order"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type ContactForm struct {
|
||||
Name string `json:"name" validate:"required,min=2,max=100"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Subject string `json:"subject" validate:"required,min=5,max=200"`
|
||||
Message string `json:"message" validate:"required,min=10,max=5000"`
|
||||
}
|
||||
|
||||
type ContactSubmission struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Subject string `json:"subject" db:"subject"`
|
||||
Message string `json:"message" db:"message"`
|
||||
Status string `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error *APIError `json:"error,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
Page int `json:"page,omitempty"`
|
||||
PageSize int `json:"page_size,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
TotalPages int `json:"total_pages,omitempty"`
|
||||
}
|
||||
|
||||
func SuccessResponse(data interface{}) *APIResponse {
|
||||
return &APIResponse{Success: true, Data: data}
|
||||
}
|
||||
|
||||
func ErrorResponse(code, message, details string) *APIResponse {
|
||||
return &APIResponse{
|
||||
Success: false,
|
||||
Error: &APIError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: details,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func PaginatedResponse(data interface{}, page, pageSize, total int) *APIResponse {
|
||||
totalPages := (total + pageSize - 1) / pageSize
|
||||
return &APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Meta: &Meta{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
},
|
||||
}
|
||||
}
|
||||
105
internal/service/contact_service.go
Normal file
105
internal/service/contact_service.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dnoulet/go-first/internal/model"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ContactService struct {
|
||||
mu sync.RWMutex
|
||||
submissions map[uuid.UUID]*model.ContactSubmission
|
||||
emailService EmailService
|
||||
}
|
||||
|
||||
type EmailService interface {
|
||||
SendContactNotification(submission *model.ContactSubmission) error
|
||||
SendAutoReply(submission *model.ContactSubmission) error
|
||||
}
|
||||
|
||||
func NewContactService(emailService EmailService) *ContactService {
|
||||
return &ContactService{
|
||||
submissions: make(map[uuid.UUID]*model.ContactSubmission),
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ContactService) Create(ctx context.Context, req *model.ContactForm) (*model.ContactSubmission, error) {
|
||||
submission := &model.ContactSubmission{
|
||||
ID: uuid.New(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Subject: req.Subject,
|
||||
Message: req.Message,
|
||||
Status: "new",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.submissions[submission.ID] = submission
|
||||
s.mu.Unlock()
|
||||
|
||||
if s.emailService != nil {
|
||||
go func() {
|
||||
if err := s.emailService.SendContactNotification(submission); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send contact notification email")
|
||||
}
|
||||
if err := s.emailService.SendAutoReply(submission); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send auto-reply email")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return submission, nil
|
||||
}
|
||||
|
||||
func (s *ContactService) GetAll(ctx context.Context) ([]*model.ContactSubmission, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*model.ContactSubmission
|
||||
for _, sub := range s.submissions {
|
||||
result = append(result, sub)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *ContactService) GetByID(ctx context.Context, id uuid.UUID) (*model.ContactSubmission, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
sub, ok := s.submissions[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (s *ContactService) UpdateStatus(ctx context.Context, id uuid.UUID, status string) (*model.ContactSubmission, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
sub, ok := s.submissions[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
sub.Status = status
|
||||
sub.UpdatedAt = time.Now()
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (s *ContactService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.submissions[id]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(s.submissions, id)
|
||||
return nil
|
||||
}
|
||||
77
internal/service/email_service.go
Normal file
77
internal/service/email_service.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/dnoulet/go-first/internal/model"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type MockEmailService struct{}
|
||||
|
||||
func NewMockEmailService() *MockEmailService {
|
||||
return &MockEmailService{}
|
||||
}
|
||||
|
||||
func (m *MockEmailService) SendContactNotification(submission *model.ContactSubmission) error {
|
||||
log.Info().
|
||||
Str("to", "contact@urpi.be").
|
||||
Str("subject", "New Contact Form Submission: "+submission.Subject).
|
||||
Str("from_name", submission.Name).
|
||||
Str("from_email", submission.Email).
|
||||
Msg("Sending contact notification email (mock)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockEmailService) SendAutoReply(submission *model.ContactSubmission) error {
|
||||
log.Info().
|
||||
Str("to", submission.Email).
|
||||
Str("subject", "Re: "+submission.Subject).
|
||||
Msg("Sending auto-reply email (mock)")
|
||||
return nil
|
||||
}
|
||||
|
||||
type SMTPEmailService struct {
|
||||
host string
|
||||
port int
|
||||
username string
|
||||
password string
|
||||
from string
|
||||
fromName string
|
||||
}
|
||||
|
||||
func NewSMTPEmailService(host string, port int, username, password, from, fromName string) *SMTPEmailService {
|
||||
return &SMTPEmailService{
|
||||
host: host,
|
||||
port: port,
|
||||
username: username,
|
||||
password: password,
|
||||
from: from,
|
||||
fromName: fromName,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SMTPEmailService) SendContactNotification(submission *model.ContactSubmission) error {
|
||||
log.Info().
|
||||
Str("to", "contact@urpi.be").
|
||||
Str("subject", "New Contact Form Submission: "+submission.Subject).
|
||||
Msg("Sending contact notification email via SMTP")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SMTPEmailService) SendAutoReply(submission *model.ContactSubmission) error {
|
||||
log.Info().
|
||||
Str("to", submission.Email).
|
||||
Str("subject", "Re: "+submission.Subject).
|
||||
Msg("Sending auto-reply email via SMTP")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SMTPEmailService) sendEmail(ctx context.Context, to, subject, body string) error {
|
||||
log.Debug().
|
||||
Str("to", to).
|
||||
Str("subject", subject).
|
||||
Msg("Sending email via SMTP")
|
||||
return fmt.Errorf("SMTP not implemented")
|
||||
}
|
||||
16
internal/service/errors.go
Normal file
16
internal/service/errors.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package service
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrAlreadyExists = errors.New("resource already exists")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrInternal = errors.New("internal server error")
|
||||
ErrValidation = errors.New("validation failed")
|
||||
ErrConflict = errors.New("conflict")
|
||||
ErrRateLimited = errors.New("rate limited")
|
||||
ErrServiceUnavailable = errors.New("service unavailable")
|
||||
)
|
||||
137
internal/service/experience_service.go
Normal file
137
internal/service/experience_service.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dnoulet/go-first/internal/model"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ExperienceService struct {
|
||||
mu sync.RWMutex
|
||||
experiences map[uuid.UUID]*model.Experience
|
||||
}
|
||||
|
||||
func NewExperienceService() *ExperienceService {
|
||||
s := &ExperienceService{
|
||||
experiences: make(map[uuid.UUID]*model.Experience),
|
||||
}
|
||||
s.seedData()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ExperienceService) seedData() {
|
||||
experiences := []*model.Experience{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Company: "URPI",
|
||||
Role: "Founder & Lead Developer",
|
||||
Location: "Belgium",
|
||||
StartDate: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
EndDate: nil,
|
||||
Current: true,
|
||||
Description: "Founded URPI to provide web application and backend development services. Specializing in Go, Java, and cloud-native architectures. Delivered projects for clients in fintech, e-commerce, and SaaS sectors.",
|
||||
TechStack: []string{"Go", "Java", "React", "Kubernetes", "AWS", "PostgreSQL", "Redis", "gRPC"},
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Company: "TechCorp Solutions",
|
||||
Role: "Senior Backend Engineer",
|
||||
Location: "Brussels, Belgium",
|
||||
StartDate: time.Date(2017, 6, 1, 0, 0, 0, 0, time.UTC),
|
||||
EndDate: ptrTime(time.Date(2019, 12, 31, 0, 0, 0, 0, time.UTC)),
|
||||
Current: false,
|
||||
Description: "Led backend development for high-scale financial services platform. Designed and implemented microservices architecture, CI/CD pipelines, and monitoring infrastructure. Mentored junior developers and conducted code reviews.",
|
||||
TechStack: []string{"Java", "Spring Boot", "Kotlin", "PostgreSQL", "Apache Kafka", "Docker", "Kubernetes", "AWS"},
|
||||
Order: 2,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Company: "StartupXYZ",
|
||||
Role: "Full Stack Developer",
|
||||
Location: "Antwerp, Belgium",
|
||||
StartDate: time.Date(2015, 3, 1, 0, 0, 0, 0, time.UTC),
|
||||
EndDate: ptrTime(time.Date(2017, 5, 31, 0, 0, 0, 0, time.UTC)),
|
||||
Current: false,
|
||||
Description: "Built and maintained full-stack web applications for various clients. Worked with React, Node.js, Python, and Ruby on Rails. Implemented REST APIs, real-time features, and automated testing pipelines.",
|
||||
TechStack: []string{"React", "Node.js", "Python", "Ruby on Rails", "PostgreSQL", "MongoDB", "AWS", "Docker"},
|
||||
Order: 3,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Company: "Digital Agency",
|
||||
Role: "Junior Developer",
|
||||
Location: "Ghent, Belgium",
|
||||
StartDate: time.Date(2013, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||
EndDate: ptrTime(time.Date(2015, 2, 28, 0, 0, 0, 0, time.UTC)),
|
||||
Current: false,
|
||||
Description: "Started career building websites and web applications for SME clients. Learned PHP, JavaScript, MySQL, and modern frontend frameworks. Developed custom CMS solutions and e-commerce integrations.",
|
||||
TechStack: []string{"PHP", "JavaScript", "MySQL", "HTML/CSS", "jQuery", "WordPress", "Magento"},
|
||||
Order: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, exp := range experiences {
|
||||
s.experiences[exp.ID] = exp
|
||||
}
|
||||
}
|
||||
|
||||
func ptrTime(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
func (s *ExperienceService) GetAll(ctx context.Context) ([]*model.Experience, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*model.Experience
|
||||
for _, exp := range s.experiences {
|
||||
result = append(result, exp)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *ExperienceService) GetByID(ctx context.Context, id uuid.UUID) (*model.Experience, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
exp, ok := s.experiences[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
func (s *ExperienceService) Create(ctx context.Context, exp *model.Experience) (*model.Experience, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
exp.ID = uuid.New()
|
||||
s.experiences[exp.ID] = exp
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
func (s *ExperienceService) Update(ctx context.Context, exp *model.Experience) (*model.Experience, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.experiences[exp.ID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
s.experiences[exp.ID] = exp
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
func (s *ExperienceService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.experiences[id]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(s.experiences, id)
|
||||
return nil
|
||||
}
|
||||
241
internal/service/project_service.go
Normal file
241
internal/service/project_service.go
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/dnoulet/go-first/internal/model"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProjectService struct {
|
||||
mu sync.RWMutex
|
||||
projects map[uuid.UUID]*model.Project
|
||||
}
|
||||
|
||||
func NewProjectService() *ProjectService {
|
||||
s := &ProjectService{
|
||||
projects: make(map[uuid.UUID]*model.Project),
|
||||
}
|
||||
s.seedData()
|
||||
return s
|
||||
}
|
||||
|
||||
type catalogProject struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
ShortDesc string `json:"shortDesc"`
|
||||
TechStack []string `json:"techStack"`
|
||||
ImageURL string `json:"imageUrl"`
|
||||
DemoURL *string `json:"demoUrl"`
|
||||
RepoURL string `json:"repoUrl"`
|
||||
Category string `json:"category"`
|
||||
Featured bool `json:"featured"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
func projectsFile() string {
|
||||
if v := os.Getenv("GO_FIRST_PROJECTS_FILE"); v != "" {
|
||||
return v
|
||||
}
|
||||
for _, candidate := range []string{"projects.json", "/app/projects.json"} {
|
||||
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadCatalog() ([]*model.Project, bool) {
|
||||
path := projectsFile()
|
||||
if path == "" {
|
||||
return nil, false
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var rows []catalogProject
|
||||
if err := json.Unmarshal(raw, &rows); err != nil || len(rows) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
projects := make([]*model.Project, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
demo := ""
|
||||
if row.DemoURL != nil {
|
||||
demo = *row.DemoURL
|
||||
}
|
||||
projects = append(projects, &model.Project{
|
||||
ID: uuid.New(),
|
||||
Title: row.Title,
|
||||
Description: row.Description,
|
||||
ShortDesc: row.ShortDesc,
|
||||
TechStack: row.TechStack,
|
||||
ImageURL: row.ImageURL,
|
||||
DemoURL: demo,
|
||||
RepoURL: row.RepoURL,
|
||||
Category: row.Category,
|
||||
Featured: row.Featured,
|
||||
Order: row.Order,
|
||||
})
|
||||
}
|
||||
return projects, true
|
||||
}
|
||||
|
||||
func (s *ProjectService) seedData() {
|
||||
if projects, ok := loadCatalog(); ok {
|
||||
for _, project := range projects {
|
||||
s.projects[project.ID] = project
|
||||
}
|
||||
return
|
||||
}
|
||||
projects := []*model.Project{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "E-Commerce Platform",
|
||||
Description: "A full-featured e-commerce platform built with Go microservices, React frontend, and Kubernetes deployment. Features include user authentication, product catalog, shopping cart, payment integration, order management, and admin dashboard.",
|
||||
ShortDesc: "Scalable e-commerce platform with Go microservices and React",
|
||||
TechStack: []string{"Go", "React", "PostgreSQL", "Redis", "Kubernetes", "Docker", "gRPC"},
|
||||
ImageURL: "/images/projects/ecommerce.jpg",
|
||||
DemoURL: "https://demo.urpi.be/ecommerce",
|
||||
RepoURL: "https://github.com/urpi/ecommerce-platform",
|
||||
Category: "Full Stack",
|
||||
Featured: true,
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "API Gateway Service",
|
||||
Description: "High-performance API gateway built in Go with rate limiting, authentication, request/response transformation, circuit breaker, and distributed tracing. Handles 100k+ requests/second with sub-millisecond latency.",
|
||||
ShortDesc: "High-performance Go API gateway with advanced features",
|
||||
TechStack: []string{"Go", "Redis", "Prometheus", "Grafana", "OpenTelemetry", "Docker"},
|
||||
ImageURL: "/images/projects/apigateway.jpg",
|
||||
DemoURL: "",
|
||||
RepoURL: "https://github.com/urpi/api-gateway",
|
||||
Category: "Backend",
|
||||
Featured: true,
|
||||
Order: 2,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Real-time Collaboration Tool",
|
||||
Description: "Real-time collaborative document editor with operational transformation, WebSocket connections, presence indicators, and conflict resolution. Built with Go backend and React frontend.",
|
||||
ShortDesc: "Real-time collaborative editor with operational transformation",
|
||||
TechStack: []string{"Go", "React", "WebSocket", "Redis", "PostgreSQL", "TypeScript"},
|
||||
ImageURL: "/images/projects/collab.jpg",
|
||||
DemoURL: "https://collab.urpi.be",
|
||||
RepoURL: "https://github.com/urpi/realtime-collab",
|
||||
Category: "Full Stack",
|
||||
Featured: true,
|
||||
Order: 3,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Distributed Task Queue",
|
||||
Description: "Distributed task queue system with priority queues, delayed jobs, retry logic, dead letter queues, and horizontal scaling. Built for high-throughput background job processing.",
|
||||
ShortDesc: "Scalable distributed task queue with priority and retry logic",
|
||||
TechStack: []string{"Go", "Redis", "PostgreSQL", "Docker", "Kubernetes"},
|
||||
ImageURL: "/images/projects/taskqueue.jpg",
|
||||
DemoURL: "",
|
||||
RepoURL: "https://github.com/urpi/distributed-task-queue",
|
||||
Category: "Backend",
|
||||
Featured: false,
|
||||
Order: 4,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Infrastructure as Code Framework",
|
||||
Description: "Custom IaC framework for managing multi-cloud infrastructure with Go templates, drift detection, and policy enforcement. Supports AWS, GCP, and Azure.",
|
||||
ShortDesc: "Multi-cloud IaC framework with drift detection",
|
||||
TechStack: []string{"Go", "Terraform", "AWS", "GCP", "Azure", "Docker"},
|
||||
ImageURL: "/images/projects/iac.jpg",
|
||||
DemoURL: "",
|
||||
RepoURL: "https://github.com/urpi/iac-framework",
|
||||
Category: "DevOps",
|
||||
Featured: false,
|
||||
Order: 5,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Financial Transaction Processor",
|
||||
Description: "High-throughput financial transaction processing system with ACID guarantees, audit logging, regulatory compliance, and real-time fraud detection. Processes millions of transactions daily.",
|
||||
ShortDesc: "High-throughput financial transaction processor with fraud detection",
|
||||
TechStack: []string{"Java", "Spring Boot", "PostgreSQL", "Apache Kafka", "Kubernetes"},
|
||||
ImageURL: "/images/projects/fintech.jpg",
|
||||
DemoURL: "",
|
||||
RepoURL: "",
|
||||
Category: "Backend",
|
||||
Featured: true,
|
||||
Order: 6,
|
||||
},
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
s.projects[project.ID] = project
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProjectService) GetAll(ctx context.Context, category string, featured bool) ([]*model.Project, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*model.Project
|
||||
for _, project := range s.projects {
|
||||
if category != "" && project.Category != category {
|
||||
continue
|
||||
}
|
||||
if featured && !project.Featured {
|
||||
continue
|
||||
}
|
||||
result = append(result, project)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *ProjectService) GetFeatured(ctx context.Context) ([]*model.Project, error) {
|
||||
return s.GetAll(ctx, "", true)
|
||||
}
|
||||
|
||||
func (s *ProjectService) GetByID(ctx context.Context, id uuid.UUID) (*model.Project, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
project, ok := s.projects[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *ProjectService) Create(ctx context.Context, project *model.Project) (*model.Project, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
project.ID = uuid.New()
|
||||
s.projects[project.ID] = project
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *ProjectService) Update(ctx context.Context, project *model.Project) (*model.Project, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.projects[project.ID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
s.projects[project.ID] = project
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *ProjectService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.projects[id]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(s.projects, id)
|
||||
return nil
|
||||
}
|
||||
140
internal/service/service_service.go
Normal file
140
internal/service/service_service.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/dnoulet/go-first/internal/model"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ServiceService struct {
|
||||
mu sync.RWMutex
|
||||
services map[uuid.UUID]*model.Service
|
||||
}
|
||||
|
||||
func NewServiceService() *ServiceService {
|
||||
s := &ServiceService{
|
||||
services: make(map[uuid.UUID]*model.Service),
|
||||
}
|
||||
s.seedData()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ServiceService) seedData() {
|
||||
services := []*model.Service{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Web Application Development",
|
||||
Description: "Custom web applications built with modern technologies. From concept to deployment, we deliver scalable, maintainable solutions tailored to your business needs.",
|
||||
Icon: "code",
|
||||
Features: []string{"React/Next.js Frontend", "Go/Java Backend", "REST/GraphQL APIs", "Real-time Features", "Progressive Web Apps", "TypeScript"},
|
||||
Order: 1,
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Backend & API Development",
|
||||
Description: "Robust backend systems and APIs designed for scale. Microservices, event-driven architectures, and high-performance systems that power your business logic.",
|
||||
Icon: "server",
|
||||
Features: []string{"Microservices Architecture", "REST & GraphQL APIs", "gRPC Services", "Event-Driven Systems", "Database Design", "Caching Strategies"},
|
||||
Order: 2,
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Cloud Infrastructure & DevOps",
|
||||
Description: "End-to-end infrastructure solutions on AWS, GCP, or Azure. Container orchestration, CI/CD pipelines, monitoring, and infrastructure as code.",
|
||||
Icon: "cloud",
|
||||
Features: []string{"Kubernetes & Docker", "AWS/GCP/Azure", "Terraform/IaC", "CI/CD Pipelines", "Monitoring & Logging", "Security Hardening"},
|
||||
Order: 3,
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "System Architecture & Consulting",
|
||||
Description: "Expert architectural guidance for complex systems. Technology selection, scalability planning, performance optimization, and technical debt reduction.",
|
||||
Icon: "architecture",
|
||||
Features: []string{"System Design Reviews", "Technology Evaluation", "Scalability Planning", "Performance Optimization", "Migration Strategies", "Code Audits"},
|
||||
Order: 4,
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Legacy Modernization",
|
||||
Description: "Transform legacy systems into modern, maintainable applications. Strangler fig pattern, domain-driven design, and incremental migration strategies.",
|
||||
Icon: "refresh",
|
||||
Features: []string{"Legacy Assessment", "Strangler Fig Migration", "Domain-Driven Design", "Data Migration", "API Facade Pattern", "Automated Testing"},
|
||||
Order: 5,
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Title: "Technical Advisory & CTO Services",
|
||||
Description: "Fractional CTO and technical advisory services for startups and scale-ups. Technology strategy, team building, and technical roadmap planning.",
|
||||
Icon: "advisor",
|
||||
Features: []string{"Technology Strategy", "Team Building & Hiring", "Technical Roadmaps", "Vendor Evaluation", "Architecture Decisions", "Investor Technical Due Diligence"},
|
||||
Order: 6,
|
||||
Active: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
s.services[svc.ID] = svc
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServiceService) GetAll(ctx context.Context) ([]*model.Service, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*model.Service
|
||||
for _, svc := range s.services {
|
||||
if svc.Active {
|
||||
result = append(result, svc)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *ServiceService) GetByID(ctx context.Context, id uuid.UUID) (*model.Service, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
svc, ok := s.services[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (s *ServiceService) Create(ctx context.Context, svc *model.Service) (*model.Service, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
svc.ID = uuid.New()
|
||||
s.services[svc.ID] = svc
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (s *ServiceService) Update(ctx context.Context, svc *model.Service) (*model.Service, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.services[svc.ID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
s.services[svc.ID] = svc
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (s *ServiceService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.services[id]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(s.services, id)
|
||||
return nil
|
||||
}
|
||||
106
internal/service/skill_service.go
Normal file
106
internal/service/skill_service.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/dnoulet/go-first/internal/model"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SkillService struct {
|
||||
mu sync.RWMutex
|
||||
skills map[uuid.UUID]*model.Skill
|
||||
}
|
||||
|
||||
func NewSkillService() *SkillService {
|
||||
s := &SkillService{
|
||||
skills: make(map[uuid.UUID]*model.Skill),
|
||||
}
|
||||
s.seedData()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SkillService) seedData() {
|
||||
skills := []*model.Skill{
|
||||
{ID: uuid.New(), Name: "Go", Category: "Backend", Proficiency: 5, Icon: "go", Order: 1},
|
||||
{ID: uuid.New(), Name: "Java", Category: "Backend", Proficiency: 5, Icon: "java", Order: 2},
|
||||
{ID: uuid.New(), Name: "C#", Category: "Backend", Proficiency: 4, Icon: "csharp", Order: 3},
|
||||
{ID: uuid.New(), Name: "Python", Category: "Backend", Proficiency: 4, Icon: "python", Order: 4},
|
||||
{ID: uuid.New(), Name: "Ruby", Category: "Backend", Proficiency: 3, Icon: "ruby", Order: 5},
|
||||
{ID: uuid.New(), Name: "Kotlin", Category: "Backend", Proficiency: 4, Icon: "kotlin", Order: 6},
|
||||
{ID: uuid.New(), Name: "REST APIs", Category: "Architecture", Proficiency: 5, Icon: "api", Order: 7},
|
||||
{ID: uuid.New(), Name: "Microservices", Category: "Architecture", Proficiency: 5, Icon: "microservices", Order: 8},
|
||||
{ID: uuid.New(), Name: "System Design", Category: "Architecture", Proficiency: 5, Icon: "architecture", Order: 9},
|
||||
{ID: uuid.New(), Name: "Infrastructure", Category: "DevOps", Proficiency: 4, Icon: "infrastructure", Order: 10},
|
||||
{ID: uuid.New(), Name: "Docker", Category: "DevOps", Proficiency: 5, Icon: "docker", Order: 11},
|
||||
{ID: uuid.New(), Name: "Kubernetes", Category: "DevOps", Proficiency: 4, Icon: "kubernetes", Order: 12},
|
||||
{ID: uuid.New(), Name: "React", Category: "Frontend", Proficiency: 4, Icon: "react", Order: 13},
|
||||
{ID: uuid.New(), Name: "TypeScript", Category: "Frontend", Proficiency: 4, Icon: "typescript", Order: 14},
|
||||
{ID: uuid.New(), Name: "PostgreSQL", Category: "Database", Proficiency: 5, Icon: "postgresql", Order: 15},
|
||||
{ID: uuid.New(), Name: "Redis", Category: "Database", Proficiency: 4, Icon: "redis", Order: 16},
|
||||
{ID: uuid.New(), Name: "MongoDB", Category: "Database", Proficiency: 3, Icon: "mongodb", Order: 17},
|
||||
{ID: uuid.New(), Name: "GraphQL", Category: "API", Proficiency: 4, Icon: "graphql", Order: 18},
|
||||
{ID: uuid.New(), Name: "gRPC", Category: "API", Proficiency: 4, Icon: "grpc", Order: 19},
|
||||
{ID: uuid.New(), Name: "CI/CD", Category: "DevOps", Proficiency: 5, Icon: "cicd", Order: 20},
|
||||
}
|
||||
|
||||
for _, skill := range skills {
|
||||
s.skills[skill.ID] = skill
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SkillService) GetAll(ctx context.Context, category string) ([]*model.Skill, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*model.Skill
|
||||
for _, skill := range s.skills {
|
||||
if category == "" || skill.Category == category {
|
||||
result = append(result, skill)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SkillService) GetByID(ctx context.Context, id uuid.UUID) (*model.Skill, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
skill, ok := s.skills[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return skill, nil
|
||||
}
|
||||
|
||||
func (s *SkillService) Create(ctx context.Context, skill *model.Skill) (*model.Skill, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
skill.ID = uuid.New()
|
||||
s.skills[skill.ID] = skill
|
||||
return skill, nil
|
||||
}
|
||||
|
||||
func (s *SkillService) Update(ctx context.Context, skill *model.Skill) (*model.Skill, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.skills[skill.ID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
s.skills[skill.ID] = skill
|
||||
return skill, nil
|
||||
}
|
||||
|
||||
func (s *SkillService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.skills[id]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(s.skills, id)
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue