Init Project
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s

This commit is contained in:
enkly
2026-10-30 18:06:55 +01:00
parent da8af794bd
commit 79ce8dc4c0
211 changed files with 44434 additions and 1 deletions

View File

@@ -0,0 +1,17 @@
# See services/iam/Dockerfile for the pattern explanation. This service
# has no gRPC API — only a RabbitMQ consumer and a tiny health endpoint.
FROM golang:1.23-bookworm AS build
WORKDIR /src
COPY go.mod go.sum* ./
COPY gen/ gen/
COPY pkg/ pkg/
COPY services/notification/ services/notification/
RUN go mod tidy
RUN CGO_ENABLED=0 go build -o /out/notification-server ./services/notification/cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=build /out/notification-server ./notification-server
ENV ENKLY_NOTIFICATION_HEALTH_ADDR=:9007
EXPOSE 9007
ENTRYPOINT ["./notification-server"]

View File

@@ -0,0 +1,81 @@
// Command server runs the notification microservice: a pure RabbitMQ
// consumer with no gRPC server and no database of its own — it sends
// email in reaction to events published by iam. Replaces the email-
// sending half of internal/handlers/auth.go + internal/mail/mail.go.
package main
import (
"context"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"enkly/pkg/envkit"
"enkly/pkg/eventbus"
"enkly/pkg/mailkit"
"enkly/services/notification/internal/application"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
if err := envkit.LoadDotEnv(".env"); err != nil {
logger.Error("load .env", "err", err)
os.Exit(1)
}
var (
rabbitURL = envkit.String("ENKLY_RABBITMQ_URL", "amqp://enkly:enkly@localhost:5672/")
healthAddr = envkit.String("ENKLY_NOTIFICATION_HEALTH_ADDR", ":9007")
smtpHost = envkit.String("ENKLY_SMTP_HOST", "smtp.gmail.com")
smtpPort = envkit.String("ENKLY_SMTP_PORT", "587")
smtpUsername = envkit.String("ENKLY_SMTP_USERNAME", "")
smtpPassword = envkit.String("ENKLY_SMTP_PASSWORD", "")
smtpFrom = envkit.String("ENKLY_SMTP_FROM", "")
)
mailer := mailkit.New(mailkit.Config{
Host: smtpHost, Port: smtpPort, Username: smtpUsername, Password: smtpPassword, From: smtpFrom,
})
if !mailer.Configured() {
logger.Warn("mail is not configured — verification codes and reset links will be logged, not emailed",
"hint", "set ENKLY_SMTP_USERNAME and ENKLY_SMTP_PASSWORD (a Gmail App Password) in .env")
}
bus, err := eventbus.Connect(rabbitURL, logger)
if err != nil {
logger.Error("connect rabbitmq", "err", err)
os.Exit(1)
}
defer bus.Close()
svc := &application.Service{Mailer: mailer, Logger: logger}
// A minimal HTTP health endpoint — this service has no gRPC API
// (nothing calls it synchronously), but docker-compose and an
// orchestrator still want something to poll.
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
lis, err := net.Listen("tcp", healthAddr)
if err != nil {
logger.Error("health listen", "err", err)
return
}
_ = http.Serve(lis, mux)
}()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
logger.Info("notification service consuming", "queue", eventbus.NotificationQueueBindings.Queue, "mail_configured", mailer.Configured())
if err := bus.Consume(ctx, eventbus.NotificationQueueBindings, svc.HandleEvent); err != nil && ctx.Err() == nil {
logger.Error("consume", "err", err)
os.Exit(1)
}
logger.Info("shutting down")
}

View File

@@ -0,0 +1,96 @@
// Package application is the notification service's entire job: listen
// for the events iam publishes when it needs an email sent, and send
// it. Replaces the parts of internal/handlers/auth.go that called
// mail.Mailer.Send inline (sendVerificationEmail, sendResetEmail),
// decoupled onto RabbitMQ per the task's requirement that asynchronous,
// no-reply-needed work use the message bus rather than a synchronous
// call — sending an email has no result the caller
// (iam's SignUp/ResendVerification/ForgotPassword RPCs) needs to wait
// for.
package application
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"enkly/pkg/eventbus"
"enkly/pkg/eventbus/events"
"enkly/pkg/mailkit"
)
type Service struct {
Mailer *mailkit.Mailer
Logger *slog.Logger
}
// HandleEvent is the single eventbus.Handler this service registers —
// see cmd/server/main.go's Consume call. It switches on the envelope's
// routing key the same way the monolith's handlers switched on which
// function (sendVerificationEmail vs. sendResetEmail) was calling
// mail.Send.
func (s *Service) HandleEvent(ctx context.Context, env eventbus.Envelope) error {
switch env.RoutingKey {
case eventbus.RoutingKeyVerificationCodeIssued:
var ev events.VerificationCodeIssued
if err := decode(env, &ev); err != nil {
return err
}
return s.sendVerificationEmail(ctx, ev)
case eventbus.RoutingKeyPasswordResetRequested:
var ev events.PasswordResetRequested
if err := decode(env, &ev); err != nil {
return err
}
return s.sendResetEmail(ctx, ev)
default:
// Bound to this queue by mistake (topology drift) — log and ack
// rather than requeue-looping forever on a message we'll never
// know how to handle.
s.Logger.Warn("notification: unhandled routing key", "routing_key", env.RoutingKey)
return nil
}
}
func decode(env eventbus.Envelope, v any) error {
if err := json.Unmarshal(env.Payload, v); err != nil {
return fmt.Errorf("decode %s payload: %w", env.RoutingKey, err)
}
return nil
}
// sendVerificationEmail replaces Handlers.sendVerificationEmail.
func (s *Service) sendVerificationEmail(_ context.Context, ev events.VerificationCodeIssued) error {
subject := "Verify your enkly account"
body := fmt.Sprintf(
"Hi,\n\nYour enkly verification code is: %s\n\nThis code expires in 15 minutes. If you didn't request this, you can ignore this email.\n",
ev.Code)
if !s.Mailer.Configured() {
s.Logger.Info("mail not configured — verification code logged instead", "email", ev.Email, "code", ev.Code)
return nil
}
if err := s.Mailer.Send(ev.Email, subject, body); err != nil {
return fmt.Errorf("send verification email to %s: %w", ev.Email, err)
}
return nil
}
// sendResetEmail replaces Handlers.sendResetEmail.
func (s *Service) sendResetEmail(_ context.Context, ev events.PasswordResetRequested) error {
subject := "Reset your enkly password"
body := fmt.Sprintf(
"Hi,\n\nSomeone (hopefully you) requested a password reset for your enkly account.\n\n"+
"Reset it here: %s\n\nThis link expires in an hour. If you didn't request this, you can ignore this email.\n",
ev.ResetLink)
if !s.Mailer.Configured() {
s.Logger.Info("mail not configured — reset link logged instead", "email", ev.Email, "link", ev.ResetLink)
return nil
}
if err := s.Mailer.Send(ev.Email, subject, body); err != nil {
return fmt.Errorf("send reset email to %s: %w", ev.Email, err)
}
return nil
}