Compare commits

...

11 Commits

42 changed files with 610 additions and 467 deletions

View File

@ -1 +1,13 @@
nil
Домашний проект, без конкретной цели
Пишу нагрузочные тесты для разных ручек
![Снимок экрана 2024-08-28 005315](https://github.com/user-attachments/assets/82a64eb6-2e1c-49c6-9f84-ad7613cf7417)
Изучаю метрики
![Снимок экрана 2024-08-28 005220](https://github.com/user-attachments/assets/d9f98129-6602-4914-87b1-2b0036fbebae)
Профилирую
![Снимок экрана 2024-08-18 223407](https://github.com/user-attachments/assets/9984e768-c355-40f6-ae9c-8beda010add0)
Трассирую
![image_2024-09-02_21-20-55](https://github.com/user-attachments/assets/a06dbfb4-5822-44d3-9c75-e2a0feee29ba)

23
backend.Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM golang:1.22-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY cmd/backend cmd/backend
COPY pkg pkg
COPY internal internal
RUN GOEXPERIMENT=boringcrypto go build -ldflags "-s -w" -o ./app ./cmd/backend
RUN chmod +x ./app
FROM alpine:3.21.2 AS production
WORKDIR /backend
COPY --from=builder /build/app .
COPY deploy/backend-config.yaml ./config.yaml
COPY deploy/backend-jwt-privkey ./privkey
EXPOSE 8080
CMD ["./app", "-c", "config.yaml", "-k", "privkey"]

View File

@ -1,7 +1,6 @@
package main
import (
"backend/cmd/backend/args_parser"
"backend/cmd/backend/server"
"backend/internal/core/models"
"backend/internal/core/repos"
@ -55,7 +54,7 @@ func (a *App) Run(p RunParams) {
//-----------------------------------------
args, err := args_parser.Parse(osArgs)
args, err := CmdArgsParse(osArgs)
if err != nil {
log.Fatalf("failed to parse os args: %v\n", err)
}
@ -90,7 +89,7 @@ func (a *App) Run(p RunParams) {
var key *rsa.PrivateKey
{
keyRawBytes, err := os.ReadFile(conf.GetJwtSigningKey())
keyRawBytes, err := os.ReadFile(args.GetSigningKeyPath())
if err != nil {
logger.Fatal().Err(err).Msg("failed reading signing key file")
}
@ -122,9 +121,10 @@ func (a *App) Run(p RunParams) {
shortlinkRepo = repos.NewShortlinkRepo(sqlDb, tracer)
eventRepo = repos.NewEventRepo(kafka)
userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger)
jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT)
linksCache = cache.NewCacheInmem[string, string]()
userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger)
loginAttemptsCache = cache.NewCacheInmem[string, int]()
jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT)
linksCache = cache.NewCacheInmem[string, string]()
)
// Periodically trigger cache cleanup
@ -140,20 +140,22 @@ func (a *App) Run(p RunParams) {
userCache.CheckExpired()
jwtCache.CheckExpired()
linksCache.CheckExpired()
loginAttemptsCache.CheckExpired()
}
}
}()
userService = services.NewUserService(
services.UserServiceDeps{
Jwt: jwtUtil,
Password: passwordUtil,
UserRepo: userRepo,
UserCache: userCache,
JwtCache: jwtCache,
EventRepo: *eventRepo,
ActionTokenRepo: actionTokenRepo,
Logger: logger,
Jwt: jwtUtil,
Password: passwordUtil,
UserRepo: userRepo,
UserCache: userCache,
LoginAttemptsCache: loginAttemptsCache,
JwtCache: jwtCache,
EventRepo: *eventRepo,
ActionTokenRepo: actionTokenRepo,
Logger: logger,
},
)
shortlinkService = services.NewShortlinkSevice(

View File

@ -1,19 +1,21 @@
package args_parser
package main
import (
"github.com/akamensky/argparse"
)
type Args interface {
type CmdArgs interface {
GetProfilePath() string
GetConfigPath() string
GetLogPath() string
GetSigningKeyPath() string
}
func Parse(osArgs []string) (Args, error) {
func CmdArgsParse(osArgs []string) (CmdArgs, error) {
parser := argparse.NewParser("backend", "runs backend")
s := parser.String("c", "config", &argparse.Options{Required: true, Help: "Path to a config file"})
k := parser.String("k", "key", &argparse.Options{Required: false, Default: "", Help: "Path to a jwt signing key"})
l := parser.String("o", "log", &argparse.Options{Required: false, Default: "", Help: "Path to a log file"})
p := parser.String("p", "profile", &argparse.Options{Required: false, Default: "", Help: "Path to a cpu profile file"})
@ -23,16 +25,18 @@ func Parse(osArgs []string) (Args, error) {
}
return &args{
ConfigPath: *s,
LogPath: *l,
ProfilePath: *p,
ConfigPath: *s,
LogPath: *l,
ProfilePath: *p,
SigningKeyPath: *k,
}, nil
}
type args struct {
ProfilePath string
ConfigPath string
LogPath string
ProfilePath string
ConfigPath string
LogPath string
SigningKeyPath string
}
func (a *args) GetConfigPath() string {
@ -46,3 +50,7 @@ func (a *args) GetLogPath() string {
func (a *args) GetProfilePath() string {
return a.ProfilePath
}
func (a *args) GetSigningKeyPath() string {
return a.SigningKeyPath
}

View File

@ -5,7 +5,6 @@ import "backend/pkg/config"
type IConfig interface {
GetPort() uint16
GetPostgresUrl() string
GetJwtSigningKey() string
GetKafkaUrl() string
GetKafkaTopic() string
}
@ -15,11 +14,10 @@ func LoadConfig(filePath string) (IConfig, error) {
}
type Config struct {
Port uint16 `yaml:"port"`
PostgresUrl string `yaml:"postgres_url"`
JwtSigningKey string `yaml:"jwt_signing_key" validate:"file"`
KafkaUrl string `yaml:"kafka_url"`
KafkaTopic string `yaml:"kafka_topic"`
Port uint16 `yaml:"port"`
PostgresUrl string `yaml:"postgres_url"`
KafkaUrl string `yaml:"kafka_url"`
KafkaTopic string `yaml:"kafka_topic"`
}
func (c *Config) GetPort() uint16 {
@ -30,10 +28,6 @@ func (c *Config) GetPostgresUrl() string {
return c.PostgresUrl
}
func (c *Config) GetJwtSigningKey() string {
return c.JwtSigningKey
}
func (c *Config) GetKafkaUrl() string {
return c.KafkaUrl
}

View File

@ -1,5 +0,0 @@
port: 8080
postgres_url: "postgres://postgres:postgres@localhost:5432/postgres"
jwt_signing_key: "./jwt_signing_key"
kafka_url: "localhost:9091"
kafka_topic: "events"

View File

@ -28,14 +28,14 @@ func NewServer(opts NewServerOpts) *httpserver.Server {
r := gin.New()
r.ContextWithFallback = true // Use it to allow getting values from c.Request.Context()
// r.Static("/webapp", "./webapp")
metrics := integrations.NewMetrics("backend")
serverMetrics := httpserver.NewServerMetrics(metrics)
r.GET("/health", handlers.New200OkHandler())
r.Any("/metrics", gin.WrapH(metrics.HttpHandler()))
prometheus := integrations.NewPrometheus()
r.Any("/metrics", gin.WrapH(prometheus.GetRequestHandler()))
r.Use(httpserver.NewRecoveryMiddleware(opts.Logger, prometheus, opts.DebugMode))
r.Use(httpserver.NewRequestLogMiddleware(opts.Logger, opts.Tracer, prometheus))
r.Use(httpserver.NewRecoveryMiddleware(opts.Logger, serverMetrics, opts.DebugMode))
r.Use(httpserver.NewRequestLogMiddleware(opts.Logger, opts.Tracer, serverMetrics))
r.Use(httpserver.NewTracingMiddleware(opts.Tracer))
r.GET("/verify-user", handlers.NewUserVerifyEmailHandler(opts.Logger, opts.UserService))

View File

@ -8,6 +8,7 @@ func LoadConfig(filePath string) (Config, error) {
type Config struct {
App struct {
Port uint16 `yaml:"port"`
LogFile string `yaml:"logFile"`
ServiceUrl string `yaml:"serviceUrl"`
}

View File

@ -14,7 +14,7 @@ const MSG_TEXT = `
<body>
<p>{{.Text}}</p>
{{if .Link}}
<a href="{{.Link}}">Click</a>link</p>
<a href="{{.Link}}">link</a>
{{end}}
</body>
</html>
@ -34,7 +34,7 @@ func NewEmailer(conf ConfigSMTP) (*Emailer, error) {
}
defer closer.Close()
htmlTemplate, err := template.New("verify-email").Parse(MSG_TEXT)
htmlTemplate, err := template.New("email").Parse(MSG_TEXT)
if err != nil {
return nil, err
}
@ -52,22 +52,23 @@ type Emailer struct {
dialer *gomail.Dialer
}
func (e *Emailer) SendRestorePassword(email, token string) error {
func (e *Emailer) SendRestorePassword(email, link string) error {
return e.sendEmail("Restore your password", email, MailContent{
Text: "Token: " + token,
Text: "You received this message do request of password change. Use this link to change your password:",
Link: link,
})
}
func (e *Emailer) SendVerifyUser(email, link string) error {
return e.sendEmail("Verify your email", email, MailContent{
Text: "You recieved this message due to registration of account. Use this link to verify email:",
Text: "You received this message due to registration of account. Use this link to verify email:",
Link: link,
})
}
func (e *Emailer) SendPasswordChanged(email string) error {
return e.sendEmail("Password changed", email, MailContent{
Text: "You recieved this message due to password change",
Text: "Your password was successfully changed",
})
}

View File

@ -0,0 +1,91 @@
package main
import (
"backend/internal/integrations"
"backend/pkg/logger"
"context"
"encoding/json"
"fmt"
"io"
"github.com/segmentio/kafka-go"
)
type SendEmailEvent struct {
Email string `json:"email"`
Token string `json:"token"`
}
func NewEventHandler(
config Config,
logger logger.Logger,
metrics *integrations.Metrics,
emailer *Emailer,
) *EventHandler {
eventsCounter := metrics.NewCounter("events_counter", "total events handled")
return &EventHandler{
config: config,
logger: logger,
emailer: emailer,
eventsCounter: eventsCounter,
}
}
type EventHandler struct {
config Config
logger logger.Logger
emailer *Emailer
eventsCounter integrations.Counter
}
func (e *EventHandler) eventLoop(ctx context.Context, kafkaReader *kafka.Reader) {
for {
msg, err := kafkaReader.FetchMessage(ctx)
if err == io.EOF {
e.logger.Fatal().Err(err)
}
if err != nil {
e.logger.Fatal().Err(err)
}
select {
case <-ctx.Done():
return
default:
}
e.logger.Log().Msgf("event: %s", msg.Key)
e.eventsCounter.Inc()
if err := kafkaReader.CommitMessages(ctx, msg); err != nil {
e.logger.Error().Err(err).Msg("failed to commit offset")
continue
}
if err := e.handleEvent(ctx, msg); err != nil {
e.logger.Error().Err(err).Msg("failed to handle event")
continue
}
}
}
func (e *EventHandler) handleEvent(ctx context.Context, msg kafka.Message) error {
event := SendEmailEvent{}
if err := json.Unmarshal(msg.Value, &event); err != nil {
return err
}
// TODO: add context somehow
switch string(msg.Key) {
case "email_forgot_password":
link := fmt.Sprintf("%s/restore-password?token=%s", e.config.App.ServiceUrl, event.Token)
return e.emailer.SendRestorePassword(event.Email, link)
case "email_password_changed":
return e.emailer.SendPasswordChanged(event.Email)
case "email_verify_user":
link := fmt.Sprintf("%s/verify-user?token=%s", e.config.App.ServiceUrl, event.Token)
return e.emailer.SendVerifyUser(event.Email, link)
}
return fmt.Errorf("unknown event type")
}

View File

@ -1,21 +1,16 @@
package main
import (
httpserver "backend/internal/http_server"
"backend/internal/integrations"
"backend/pkg/logger"
"context"
"encoding/json"
"fmt"
"io"
"log"
"github.com/gin-gonic/gin"
"github.com/segmentio/kafka-go"
)
type SendEmailEvent struct {
Email string `json:"email"`
Token string `json:"token"`
}
func main() {
ctx := context.Background()
@ -24,67 +19,44 @@ func main() {
log.Fatal(err.Error())
}
emailer, err := NewEmailer(config.SMTP)
if err != nil {
log.Fatal(err.Error())
}
r := kafka.NewReader(kafka.ReaderConfig{
Brokers: config.Kafka.Brokers,
Topic: config.Kafka.Topic,
GroupID: config.Kafka.ConsumerGroupId,
})
logger, err := logger.New(ctx, logger.NewLoggerOpts{
Debug: true,
OutputFile: config.App.LogFile,
})
if err != nil {
log.Fatal(err.Error())
logger.Fatal().Err(err)
}
logger.Printf("notifyer service started\n")
for {
msg, err := r.FetchMessage(ctx)
if err == io.EOF {
log.Fatal("EOF")
return
}
if err != nil {
log.Fatal(err.Error())
return
}
log.Printf("offset: %d, partition: %d, key: %s, value: %s\n", msg.Offset, msg.Partition, string(msg.Key), string(msg.Value))
if err := r.CommitMessages(ctx, msg); err != nil {
log.Fatalf("failed to commit: %s\n", err.Error())
continue
}
if err := handleEvent(config, emailer, msg); err != nil {
log.Printf("failed to handle event: %s\n", err.Error())
continue
}
emailer, err := NewEmailer(config.SMTP)
if err != nil {
logger.Fatal().Err(err)
}
}
func handleEvent(config Config, emailer *Emailer, msg kafka.Message) error {
event := SendEmailEvent{}
if err := json.Unmarshal(msg.Value, &event); err != nil {
return err
}
switch string(msg.Key) {
case "email_forgot_password":
return emailer.SendRestorePassword(event.Email, event.Token)
case "email_password_changed":
return emailer.SendPasswordChanged(event.Email)
case "email_verify_user":
link := fmt.Sprintf("%s/verify-user?token=%s", config.App.ServiceUrl, event.Token)
return emailer.SendVerifyUser(event.Email, link)
}
return fmt.Errorf("unknown event type")
metrics := integrations.NewMetrics("notifyer")
ginRouter := gin.New()
ginRouter.GET("/metrics", gin.WrapH(metrics.HttpHandler()))
ginRouter.GET("/health", func(ctx *gin.Context) {
ctx.Status(200)
})
kafkaReader := kafka.NewReader(kafka.ReaderConfig{
Brokers: config.Kafka.Brokers,
Topic: config.Kafka.Topic,
GroupID: config.Kafka.ConsumerGroupId,
})
kafkaReader.SetOffset(kafka.LastOffset)
eventHandler := NewEventHandler(config, logger, metrics, emailer)
go eventHandler.eventLoop(ctx, kafkaReader)
logger.Log().Msg("notifyer service started")
srv := httpserver.New(
httpserver.NewServerOpts{
Logger: logger,
HttpServer: ginRouter,
},
)
srv.Run(ctx, config.App.Port)
}

View File

@ -1,3 +0,0 @@
http_port: 8081
grpc_port: 8082
postgres_url: "postgres://postgres:postgres@localhost:5432/postgres"

View File

@ -3,6 +3,7 @@ package main
import "backend/pkg/config"
type IConfig interface {
GetServiceUrl() string
GetHttpPort() uint16
GetGrpcPort() uint16
GetPostgresUrl() string
@ -13,11 +14,16 @@ func LoadConfig(filePath string) (IConfig, error) {
}
type Config struct {
ServiceUrl string `yaml:"service_url" validate:"required"`
HttpPort uint16 `yaml:"http_port" validate:"required"`
GrpcPort uint16 `yaml:"grpc_port" validate:"required"`
PostgresUrl string `yaml:"postgres_url" validate:"required"`
}
func (c *Config) GetServiceUrl() string {
return c.ServiceUrl
}
func (c *Config) GetHttpPort() uint16 {
return c.HttpPort
}

View File

@ -10,7 +10,7 @@ import (
func NewShortlinksGrpc(log logger.Logger, shortlinkService services.ShortlinkService, host string) *ShortlinksGrpc {
return &ShortlinksGrpc{
handler: NewCreateHandler(log, shortlinkService, host),
handler: NewShortlinkCreateHandler(log, shortlinkService, host),
}
}

View File

@ -19,10 +19,10 @@ type shortlinkCreateOutput struct {
Link string `json:"link"`
}
func NewCreateHandler(
func NewShortlinkCreateHandler(
log logger.Logger,
shortlinkService services.ShortlinkService,
host string,
serviceUrl string,
) httpserver.Handler[shortlinkCreateInput, shortlinkCreateOutput] {
return func(ctx context.Context, input shortlinkCreateInput) (shortlinkCreateOutput, error) {
output := shortlinkCreateOutput{}
@ -39,13 +39,17 @@ func NewCreateHandler(
}
return shortlinkCreateOutput{
Link: fmt.Sprintf("%s/s/%s", host, linkId),
Link: fmt.Sprintf("%s/s/%s", serviceUrl, linkId),
}, nil
}
}
func NewShortlinkCreateHandler(log logger.Logger, shortlinkService services.ShortlinkService, host string) gin.HandlerFunc {
return httpserver.WrapGin(log, NewCreateHandler(log, shortlinkService, host))
func NewShortlinkCreateGinHandler(
log logger.Logger,
shortlinkService services.ShortlinkService,
serviceUrl string,
) gin.HandlerFunc {
return httpserver.WrapGin(log, NewShortlinkCreateHandler(log, shortlinkService, serviceUrl))
}
func NewShortlinkResolveHandler(logger logger.Logger, shortlinkService services.ShortlinkService) gin.HandlerFunc {

View File

@ -10,7 +10,6 @@ import (
"backend/pkg/cache"
"backend/pkg/logger"
"context"
"fmt"
"sync"
"github.com/gin-gonic/gin"
@ -37,7 +36,6 @@ func main() {
rootCmd.MarkPersistentFlagRequired("config")
rootCmd.PersistentFlags().StringVarP(&logPath, "logfile", "l", "", "path to log file")
rootCmd.MarkPersistentFlagRequired("logfile")
if err := rootCmd.Execute(); err != nil {
panic(err)
@ -79,32 +77,32 @@ func main() {
}
func RunServer(ctx context.Context, log logger.Logger, tracer trace.Tracer, conf IConfig, shortlinkService services.ShortlinkService) {
host := fmt.Sprintf("http://localhost:%d", conf.GetHttpPort())
debugMode := true
if !debugMode {
gin.SetMode(gin.ReleaseMode)
}
prometheus := integrations.NewPrometheus()
metrics := integrations.NewMetrics("shortlinks")
serverMetrics := httpserver.NewServerMetrics(metrics)
r := gin.New()
r.Any("/metrics", gin.WrapH(prometheus.GetRequestHandler()))
r.Any("/metrics", gin.WrapH(metrics.HttpHandler()))
r.GET("/health", func(ctx *gin.Context) {
ctx.Status(200)
})
r.Use(httpserver.NewRecoveryMiddleware(log, prometheus, debugMode))
r.Use(httpserver.NewRequestLogMiddleware(log, tracer, prometheus))
r.Use(httpserver.NewRecoveryMiddleware(log, serverMetrics, debugMode))
r.Use(httpserver.NewRequestLogMiddleware(log, tracer, serverMetrics))
r.Use(httpserver.NewTracingMiddleware(tracer))
linkGroup := r.Group("/s")
linkGroup.POST("/new", NewShortlinkCreateHandler(log, shortlinkService, host))
linkGroup.POST("/new", NewShortlinkCreateGinHandler(log, shortlinkService, conf.GetServiceUrl()))
linkGroup.GET("/:linkId", NewShortlinkResolveHandler(log, shortlinkService))
grpcUnderlying := grpc.NewServer()
shortlinks.RegisterShortlinksServer(
grpcUnderlying,
NewShortlinksGrpc(log, shortlinkService, host),
NewShortlinksGrpc(log, shortlinkService, conf.GetServiceUrl()),
)
httpServer := httpserver.New(

View File

@ -1,2 +0,0 @@
run:
go run . -c ./conf.yml -l ./../../.run/shortlinks.log

View File

@ -31,45 +31,22 @@ services:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
grafana:
image: grafana/grafana:11.1.4
shm_size: 256mb
ports:
- 3000:3000
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- grafana-volume:/var/lib/grafana
- ./deploy/grafana-ds.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
prometheus:
image: prom/prometheus:v2.54.0
shm_size: 256mb
user: root
ports:
- 9090:9090
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- prometheus-volume:/etc/prometheus
- ./deploy/prometheus.yml:/etc/prometheus/prometheus.yml
node_exporter:
image: quay.io/prometheus/node-exporter:latest
pid: host
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- 9100:9100
# node_exporter:
# image: quay.io/prometheus/node-exporter:latest
# pid: host
# command:
# - '--path.procfs=/host/proc'
# - '--path.rootfs=/rootfs'
# - '--path.sysfs=/host/sys'
# - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
# volumes:
# - /proc:/host/proc:ro
# - /sys:/host/sys:ro
# - /:/rootfs:ro
# extra_hosts:
# - "host.docker.internal:host-gateway"
# ports:
# - 9100:9100
otel-collector:
image: otel/opentelemetry-collector-contrib:0.108.0
@ -82,25 +59,6 @@ services:
# - 4317:4317 # OTLP gRPC receiver
- 4318:4318 # OTLP http receiver
tempo-init:
image: &tempoImage grafana/tempo:r177-60780f7
user: root
entrypoint:
- "chown"
- "10001:10001"
- "/var/tempo"
volumes:
- tempo-volume:/var/tempo
tempo:
image: *tempoImage
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
- ./deploy/tempo.yaml:/etc/tempo.yaml
- tempo-volume:/var/tempo
depends_on:
- tempo-init
kafka:
image: &kafkaImage apache/kafka:3.8.0
healthcheck:
@ -133,35 +91,34 @@ services:
entrypoint: >
/bin/bash -c "/opt/kafka/bin/kafka-topics.sh --bootstrap-server http://kafka:9092 --create --topic events --partitions 6"
# minio:
# image: quay.io/minio/minio:latest
# command: ["server", "/data", "--console-address", ":9001"]
# healthcheck:
# test: 'mc ready local'
# interval: 1s
# environment:
# MINIO_ROOT_USER: miniouser
# MINIO_ROOT_PASSWORD: miniouser
# MINIO_ACCESS_KEY: miniokey
# MINIO_SECRET_KEY: miniokey
# ports:
# - 9000:9000
# - 9001:9001
# volumes:
# - minio-volume:/data
minio:
image: quay.io/minio/minio:latest
command: ["server", "/data", "--console-address", ":9001"]
healthcheck:
test: 'mc ready local'
interval: 1s
environment:
MINIO_ROOT_USER: miniouser
MINIO_ROOT_PASSWORD: miniouser
MINIO_ACCESS_KEY: miniokey
MINIO_SECRET_KEY: miniokey
ports:
- 9000:9000
- 9001:9001
volumes:
- minio-volume:/data
minio-init:
image: quay.io/minio/mc:latest
depends_on:
- minio
entrypoint: >
/bin/sh -c "
/usr/bin/mc alias set myminio http://minio:9000 miniouser miniouser;
/usr/bin/mc mb minio/bucket;
/usr/bin/mc anonymous set public minio/bucket;
exit 0;
"
# minio-init:
# image: quay.io/minio/mc:latest
# depends_on:
# - minio
# entrypoint: >
# /bin/sh -c "
# /usr/bin/mc alias set myminio http://minio:9000 miniouser miniouser;
# /usr/bin/mc mb minio/bucket;
# /usr/bin/mc anonymous set public minio/bucket;
# exit 0;
# "
smtp4dev:
image: rnwood/smtp4dev:v3
@ -179,9 +136,35 @@ services:
- RelayOptions__Login=maillogin
- RelayOptions__Password=mailpass
backend:
build:
dockerfile: ./backend.Dockerfile
context: .
ports:
- 8080:8080
volumes:
- ./deploy/backend-config.yaml:/backend/config.yaml
- ./deploy/backend-jwt-privkey:/backend/backend-jwt-privkey
notifyer:
build:
dockerfile: ./notifyer.Dockerfile
context: .
ports:
- 8081:8081
volumes:
- ./deploy/notifyer-config.yaml:/backend/config.yaml
shortlinks:
build:
dockerfile: ./shortlinks.Dockerfile
context: .
ports:
- 8082:8082 #http
- 8083:8083 #grpc
volumes:
- ./deploy/notifyer-config.yaml:/backend/config.yaml
volumes:
postgres-volume:
grafana-volume:
tempo-volume:
prometheus-volume:
minio-volume:

View File

@ -0,0 +1,5 @@
port: 8080
postgres_url: "postgres://postgres:postgres@postgres:5432/postgres"
jwt_signing_key: "./backend-jwt-privkey"
kafka_url: "kafka:9091"
kafka_topic: "events"

View File

@ -1,12 +1,13 @@
app:
serviceUrl: "http://localhost:8080"
port: 8081
serviceUrl: "http://backend:8080"
kafka:
brokers:
- localhost:9091
- kafka:9091
topic: events
consumerGroupId: notifyer-group
smtp:
server: localhost
server: smtp4dev
port: 12333
login: "maillogin"
password: "mailpass"

View File

@ -0,0 +1,4 @@
service_url: http://shortlinks:8082
http_port: 8082
grpc_port: 8083
postgres_url: "postgres://postgres:postgres@postgres:5432/postgres"

80
go.mod
View File

@ -8,71 +8,47 @@ require (
github.com/go-playground/validator/v10 v10.22.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.2
github.com/prometheus/client_golang v1.20.2
github.com/rs/zerolog v1.33.0
github.com/segmentio/kafka-go v0.4.47
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/otel v1.29.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0
go.opentelemetry.io/otel/sdk v1.29.0
go.opentelemetry.io/otel/trace v1.29.0
golang.org/x/crypto v0.26.0
golang.org/x/crypto v0.31.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sync v0.8.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect
google.golang.org/grpc v1.65.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.12.1 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/jackc/pgx/v5 v5.6.0
github.com/goccy/go-json v0.10.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@ -80,17 +56,23 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.9.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
)

97
go.sum
View File

@ -2,11 +2,10 @@ github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@ -16,16 +15,12 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@ -43,8 +38,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
@ -55,26 +50,24 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -84,16 +77,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -108,8 +97,6 @@ github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
@ -124,24 +111,12 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -154,8 +129,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@ -181,19 +154,14 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -207,8 +175,8 @@ golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -220,8 +188,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -234,8 +202,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -256,9 +224,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -26,7 +26,7 @@ type actionTokenRepo struct {
func (a *actionTokenRepo) CreateActionToken(ctx context.Context, dto models.ActionTokenDTO) (*models.ActionTokenDTO, error) {
query := `
insert into
action_tokens (user_id, value, target, expiration)
action_tokens (user_id, value, target, expires_at)
values ($1, $2, $3, $4)
returning id;`
row := a.db.QueryRowContext(ctx, query, dto.UserId, dto.Value, dto.Target, dto.Expiration)
@ -51,7 +51,7 @@ func (a *actionTokenRepo) GetActionToken(ctx context.Context, value string, targ
select id, user_id from action_tokens
where
value=$1 and target=$2
and CURRENT_TIMESTAMP < expiration;`
and CURRENT_TIMESTAMP < expires_at;`
row := a.db.QueryRowContext(ctx, query, value, target)
err := row.Scan(&dto.Id, &dto.UserId)

View File

@ -35,7 +35,7 @@ func (e *EventRepo) sendEmail(ctx context.Context, email, actionToken, eventType
return err
}
return e.kafka.SendMessage(ctx, eventType, valueBytes)
return e.kafka.PushMessage(ctx, eventType, valueBytes)
}
func (e *EventRepo) SendEmailPasswordChanged(ctx context.Context, email string) error {

View File

@ -35,8 +35,8 @@ func (u *shortlinkRepo) AddShortlink(ctx context.Context, dto ShortlinkDTO) erro
_, span := u.tracer.Start(ctx, "postgres::AddShortlink")
defer span.End()
query := `insert into shortlinks (url, expires_at) values ($1, $2);`
_, err := u.db.ExecContext(ctx, query, dto.Url, dto.ExpiresAt)
query := `insert into shortlinks (id, url, expires_at) values ($1, $2, $3);`
_, err := u.db.ExecContext(ctx, query, dto.Id, dto.Url, dto.ExpiresAt)
return err
}
@ -72,7 +72,7 @@ func (u *shortlinkRepo) DeleteExpiredShortlinks(ctx context.Context, limit int)
where id in (
select id
from shortlinks
where current_date > expiration
where current_date > expires_at
limit $1
)
returning *

View File

@ -93,7 +93,7 @@ func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO,
query := `
select id, email, secret, full_name, email_verified
from users where id = $1 and activated;`
from users where id = $1 and active;`
row := u.db.QueryRowContext(ctx, query, id)
dto := &models.UserDTO{}
@ -113,7 +113,7 @@ func (u *userRepo) GetUserByEmail(ctx context.Context, login string) (*models.Us
defer span.End()
query := `select id, email, secret, full_name, email_verified
from users where email = $1 and activated;`
from users where email = $1 and active;`
row := u.db.QueryRowContext(ctx, query, login)
dto := &models.UserDTO{}

View File

@ -49,14 +49,15 @@ func NewUserService(deps UserServiceDeps) UserService {
}
type UserServiceDeps struct {
Jwt utils.JwtUtil
Password utils.PasswordUtil
UserRepo repos.UserRepo
UserCache cache.Cache[string, models.UserDTO]
JwtCache cache.Cache[string, string]
EventRepo repos.EventRepo
ActionTokenRepo repos.ActionTokenRepo
Logger logger.Logger
Jwt utils.JwtUtil
Password utils.PasswordUtil
UserRepo repos.UserRepo
UserCache cache.Cache[string, models.UserDTO]
JwtCache cache.Cache[string, string]
LoginAttemptsCache cache.Cache[string, int]
EventRepo repos.EventRepo
ActionTokenRepo repos.ActionTokenRepo
Logger logger.Logger
}
type userService struct {
@ -108,6 +109,22 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) (
}
func (u *userService) AuthenticateUser(ctx context.Context, email, password string) (string, error) {
attempts, ok := u.deps.LoginAttemptsCache.Get(email)
if ok && attempts >= 4 {
return "", fmt.Errorf("too many bad login attempts")
}
token, err := u.authenticateUser(ctx, email, password)
if err != nil {
u.deps.LoginAttemptsCache.Set(email, attempts+1, cache.Expiration{Ttl: 30 * time.Second})
return "", err
}
u.deps.LoginAttemptsCache.Del(email)
return token, nil
}
func (u *userService) authenticateUser(ctx context.Context, email, password string) (string, error) {
user, err := u.deps.UserRepo.GetUserByEmail(ctx, email)
if err != nil {
return "", err

View File

@ -0,0 +1,49 @@
package httpserver
import (
"backend/internal/integrations"
)
func NewServerMetrics(p *integrations.Metrics) *ServerMetrics {
errors5xxCounter := p.NewCounter("server_responses_5xx", "5xx responses counter")
errors4xxCounter := p.NewCounter("server_responses_4xx", "4xx responses count")
requestsCounter := p.NewCounter("server_requests_total", "requests counter")
avgReqTimeHist := p.NewHistogram("server_requests_time", "requests time histogram")
panicsHist := p.NewHistogram("server_panics", "panics histogram metric")
return &ServerMetrics{
rpsCounter: requestsCounter,
avgReqTimeHist: avgReqTimeHist,
panicsHist: panicsHist,
errors4xxCounter: errors4xxCounter,
errors5xxCounter: errors5xxCounter,
}
}
type ServerMetrics struct {
rpsCounter integrations.Counter
avgReqTimeHist integrations.Histogram
panicsHist integrations.Histogram
errors4xxCounter integrations.Counter
errors5xxCounter integrations.Counter
}
func (b *ServerMetrics) AddRequest() {
b.rpsCounter.Inc()
}
func (b *ServerMetrics) AddRequestTime(reqTime float64) {
b.avgReqTimeHist.Observe(reqTime)
}
func (b *ServerMetrics) AddPanic() {
b.panicsHist.Observe(1)
}
func (b *ServerMetrics) Add4xxError() {
b.errors4xxCounter.Inc()
}
func (b *ServerMetrics) Add5xxError() {
b.errors5xxCounter.Inc()
}

View File

@ -3,7 +3,6 @@ package httpserver
// Modified recovery from gin, use own logger
import (
"backend/internal/integrations"
"backend/pkg/logger"
"bytes"
"errors"
@ -30,12 +29,12 @@ var (
slash = []byte("/")
)
func NewRecoveryMiddleware(logger logger.Logger, prometheus *integrations.Prometheus, debugMode bool) gin.HandlerFunc {
func NewRecoveryMiddleware(logger logger.Logger, serverMetrics *ServerMetrics, debugMode bool) gin.HandlerFunc {
handle := defaultHandleRecovery
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
prometheus.AddPanic()
serverMetrics.AddPanic()
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.

View File

@ -1,7 +1,6 @@
package httpserver
import (
"backend/internal/integrations"
log "backend/pkg/logger"
"fmt"
"time"
@ -11,10 +10,13 @@ import (
"go.opentelemetry.io/otel/trace"
)
func NewRequestLogMiddleware(logger log.Logger, tracer trace.Tracer, prometheus *integrations.Prometheus) gin.HandlerFunc {
func NewRequestLogMiddleware(
logger log.Logger,
tracer trace.Tracer,
serverMetrics *ServerMetrics,
) gin.HandlerFunc {
return func(c *gin.Context) {
prometheus.RequestInc()
defer prometheus.RequestDec()
serverMetrics.AddRequest()
requestId := c.GetHeader("X-Request-Id")
if requestId == "" {
@ -34,7 +36,7 @@ func NewRequestLogMiddleware(logger log.Logger, tracer trace.Tracer, prometheus
c.Next()
latency := time.Since(start)
prometheus.AddRequestTime(float64(latency.Microseconds()))
serverMetrics.AddRequestTime(float64(latency.Microseconds()))
method := c.Request.Method
statusCode := c.Writer.Status()
@ -49,12 +51,12 @@ func NewRequestLogMiddleware(logger log.Logger, tracer trace.Tracer, prometheus
}
if statusCode >= 400 && statusCode < 500 {
prometheus.Add4xxError()
serverMetrics.Add4xxError()
ctxLogger.Warning().Msg(msg)
return
}
prometheus.Add5xxError()
serverMetrics.Add5xxError()
ctxLogger.Error().Msg(msg)
}
}

View File

@ -26,7 +26,7 @@ func NewKafka(addr, topic string) *Kafka {
}
}
func (k *Kafka) SendMessage(ctx context.Context, key string, value []byte) error {
func (k *Kafka) PushMessage(ctx context.Context, key string, value []byte) error {
return k.writer.WriteMessages(
context.Background(),
kafka.Message{

View File

@ -8,90 +8,67 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
)
type Prometheus struct {
reg *prometheus.Registry
rpsCounter prometheus.Counter
avgReqTimeHist prometheus.Histogram
panicsHist prometheus.Histogram
errors4xxCounter prometheus.Counter
errors5xxCounter prometheus.Counter
type Counter interface {
Inc()
}
func NewPrometheus() *Prometheus {
reg := prometheus.NewRegistry()
type Gauge interface {
Set(float64)
Inc()
Dec()
}
// Add go runtime metrics and process collectors.
reg.MustRegister(
type Histogram interface {
Observe(float64)
}
type Metrics struct {
registry *prometheus.Registry
registerer prometheus.Registerer
}
func NewMetrics(prefix string) *Metrics {
registry := prometheus.NewRegistry()
registerer := prometheus.WrapRegistererWithPrefix(prefix, registry)
registerer.MustRegister(
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
errors5xxCounter := prometheus.NewCounter(
prometheus.CounterOpts{
Name: "backend_errors_count_5xx",
Help: "5xx errors count",
},
)
errors4xxCounter := prometheus.NewCounter(
prometheus.CounterOpts{
Name: "backend_errors_count_4xx",
Help: "4xx errors count",
},
)
rpsCounter := prometheus.NewCounter(
prometheus.CounterOpts{
Name: "backend_requests_per_second",
Help: "Requests per second metric",
},
)
avgReqTimeHist := prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "backend_requests_average_time",
Help: "Average time of requests",
},
)
panicsHist := prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "backend_panics",
Help: "Panics histogram metric",
},
)
reg.MustRegister(rpsCounter, avgReqTimeHist, panicsHist, errors4xxCounter, errors5xxCounter)
return &Prometheus{
panicsHist: panicsHist,
avgReqTimeHist: avgReqTimeHist,
rpsCounter: rpsCounter,
errors4xxCounter: errors4xxCounter,
errors5xxCounter: errors5xxCounter,
reg: reg,
return &Metrics{
registry: registry,
registerer: registerer,
}
}
func (p *Prometheus) GetRequestHandler() http.Handler {
return promhttp.HandlerFor(p.reg, promhttp.HandlerOpts{Registry: p.reg})
func (m *Metrics) NewCounter(name, description string) Counter {
collector := prometheus.NewCounter(prometheus.CounterOpts{
Name: name,
Help: description,
})
m.registerer.MustRegister(collector)
return collector
}
func (p *Prometheus) RequestInc() {
p.rpsCounter.Inc()
func (m *Metrics) NewGauge(name, description string) Gauge {
collector := prometheus.NewGauge(prometheus.GaugeOpts{
Name: name,
Help: description,
})
m.registerer.MustRegister(collector)
return collector
}
func (p *Prometheus) RequestDec() {
// p.rpsGauge.Dec()
func (m *Metrics) NewHistogram(name, description string) Histogram {
collector := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: name,
Help: description,
})
m.registerer.MustRegister(collector)
return collector
}
func (p *Prometheus) AddRequestTime(reqTime float64) {
p.avgReqTimeHist.Observe(reqTime)
}
func (p *Prometheus) AddPanic() {
p.panicsHist.Observe(1)
}
func (p *Prometheus) Add4xxError() {
p.errors4xxCounter.Inc()
}
func (p *Prometheus) Add5xxError() {
p.errors5xxCounter.Inc()
func (m *Metrics) HttpHandler() http.Handler {
return promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{Registry: m.registerer})
}

View File

@ -14,6 +14,6 @@ grpc:
# --go-grpc_out=. --go-grpc_opt=paths=source_relative \
# helloworld/helloworld.proto
run: install release
mkdir -p ./.run
./.build/release/backend -c ./misc/config.yaml -o ./.run/log.txt -p ./.run/cpu.pprof
# run: install release
# mkdir -p ./.run
# ./.build/release/backend -c ./misc/config.yaml -o ./.run/log.txt -p ./.run/cpu.pprof

47
monitoring-compose.yml Normal file
View File

@ -0,0 +1,47 @@
services:
grafana:
image: grafana/grafana:11.1.4
shm_size: 256mb
ports:
- 3000:3000
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- grafana-volume:/var/lib/grafana
- ./deploy/grafana-ds.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
prometheus:
image: prom/prometheus:v2.54.0
shm_size: 256mb
user: root
ports:
- 9090:9090
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- prometheus-volume:/etc/prometheus
- ./deploy/prometheus.yml:/etc/prometheus/prometheus.yml
tempo-init:
image: &tempoImage grafana/tempo:r177-60780f7
user: root
entrypoint:
- "chown"
- "10001:10001"
- "/var/tempo"
volumes:
- tempo-volume:/var/tempo
tempo:
image: *tempoImage
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
- ./deploy/tempo.yaml:/etc/tempo.yaml
- tempo-volume:/var/tempo
depends_on:
- tempo-init
volumes:
grafana-volume:
tempo-volume:
prometheus-volume:

22
notifyer.Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM golang:1.22-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY cmd/notifyer cmd/notifyer
COPY pkg pkg
COPY internal internal
RUN go build -ldflags "-s -w" -o ./app ./cmd/notifyer
RUN chmod +x ./app
FROM alpine:3.21.2 AS production
WORKDIR /backend
COPY --from=builder /build/app .
COPY deploy/notifyer-config.yaml ./config.yaml
EXPOSE 8081
CMD ["./app", "-c", "config.yaml"]

View File

@ -4,19 +4,18 @@ WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY cmd/backend cmd/backend
COPY cmd/shortlinks cmd/shortlinks
COPY pkg pkg
COPY internal internal
RUN GOEXPERIMENT=boringcrypto go build -ldflags "-s -w" -o ./app ./cmd/backend
RUN GOEXPERIMENT=boringcrypto go build -ldflags "-s -w" -o ./app ./cmd/shortlinks
RUN chmod +x ./app
FROM alpine:3.21.2 AS production
WORKDIR /backend
COPY --from=builder /build/app .
COPY cmd/backend/config.yaml .
COPY cmd/backend/jwt_signing_key .
COPY deploy/shortlinks-config.yaml ./config.yaml
EXPOSE 8080

View File

@ -4,7 +4,7 @@ create table if not exists users (
secret varchar(256) not null,
full_name varchar(256) not null,
email_verified boolean not null default false,
active boolean,
active boolean default true,
created_at timestamp,
updated_at timestamp
);

View File

@ -1,17 +1,5 @@
create table if not exists shortlinks (
id int generated always as identity,
id text primary key,
url text not null,
expires_at timestamp not null,
created_at timestamp,
updated_at timestamp
create or replace trigger trg_shortlink_created
before insert on shortlinks
for each row
when new is distinct from old
execute function trg_proc_row_created();
create or replace trigger trg_shortlink_updated
before update on shortlinks
for each row when new is distinct from old
execute function trg_proc_row_updated();
expires_at timestamp not null
);

View File

@ -1,26 +1,25 @@
create table if not exists action_tokens (
id int primary key generated always as identity,
id int generated always as identity,
user_id int references users(id),
value text not null,
target text not null,
expires_at timestamp not null,
created_at timestamp,
updated_at timestamp
updated_at timestamp,
constraint pk_action_tokens_id primary key(id),
constraint check chk_action_tokens_target target in ('verify', 'restore')
constraint chk_action_tokens_target check(target in ('verify', 'restore'))
);
create index if not exists idx_action_tokens_value on actions_tokens(value);
create index if not exists idx_action_tokens_value on action_tokens(value);
create or replace trigger trg_action_token_created
before insert on action_tokens
for each row
when new is distinct from old
execute function trg_proc_row_created();
create or replace trigger trg_action_token_updated
before update on action_tokens
for each row
when new is distinct from old
when (new is distinct from old)
execute function trg_proc_row_updated();