Added dockerfile, docker compose, created server implementation with some middlewares
This commit is contained in:
2
.env
2
.env
@@ -12,4 +12,4 @@ LOGIFLOW_JWT_KEY=98c5772ae16aaa4fd0013eb338252a93b198fb40e9337506334b3aeb21abbe4
|
|||||||
GOOSE_DRIVER=postgres
|
GOOSE_DRIVER=postgres
|
||||||
GOOSE_DBSTRING=postgres://${LOGIFLOW_DATABASE_USER}:${LOGIFLOW_DATABASE_PASSWORD}@localhost:${LOGIFLOW_DATABASE_PORT}/${LOGIFLOW_DATABASE_NAME}?sslmode=disable
|
GOOSE_DBSTRING=postgres://${LOGIFLOW_DATABASE_USER}:${LOGIFLOW_DATABASE_PASSWORD}@localhost:${LOGIFLOW_DATABASE_PORT}/${LOGIFLOW_DATABASE_NAME}?sslmode=disable
|
||||||
GOOSE_MIGRATION_DIR=./migrations
|
GOOSE_MIGRATION_DIR=./migrations
|
||||||
GOOSE_TABLE=goose_migrations
|
GOOSE_TABLE=goose_migrations
|
||||||
|
|||||||
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM docker.io/library/golang:1.25-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/logiflow ./cmd/main.go
|
||||||
|
|
||||||
|
FROM docker.io/library/alpine:3.20
|
||||||
|
|
||||||
|
RUN addgroup -g 1000 app && adduser -u 1000 -G app -D -H -s /sbin/nologin app
|
||||||
|
|
||||||
|
COPY --from=builder /bin/logiflow /app/logiflow
|
||||||
|
COPY --chown=app:app configs/ /app/configs/
|
||||||
|
COPY --chown=app:app migrations/ /app/migrations/
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
CMD ["/app/logiflow"]
|
||||||
42
cmd/main.go
42
cmd/main.go
@@ -4,8 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/anxi0uz/logiflow/internal/config"
|
"github.com/anxi0uz/logiflow/internal/config"
|
||||||
|
"github.com/anxi0uz/logiflow/internal/database"
|
||||||
|
"github.com/anxi0uz/logiflow/internal/handler"
|
||||||
"github.com/golang-cz/devslog"
|
"github.com/golang-cz/devslog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +38,45 @@ func main() {
|
|||||||
cfg, err := config.NewConfig(ctx, "configs/config.toml")
|
cfg, err := config.NewConfig(ctx, "configs/config.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Cant load configs", slog.String("Error", err.Error()))
|
slog.ErrorContext(ctx, "Cant load configs", slog.String("Error", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.SetLogLoggerLevel(cfg.Logiflow.LogLevel)
|
slog.SetLogLoggerLevel(cfg.Logiflow.LogLevel)
|
||||||
|
connectionPool, err := database.NewConnectionPool(ctx, cfg.DatabaseURL())
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "Ошибка подключения к postgres", slog.String("error", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer connectionPool.Close()
|
||||||
|
|
||||||
|
redis, err := database.NewRedisConnection(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "Ошибка подключения к redis", slog.String("error", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer redis.Close()
|
||||||
|
|
||||||
|
if err := database.RunMigrations(ctx, cfg.DatabaseURL()); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "Ошибка миграций", slog.String("error", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
|
||||||
|
serverErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
serverErr <- handler.NewServer(connectionPool, redis, cfg).Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case sig := <-sigChan:
|
||||||
|
slog.Info("Получен сигнал завершения", "signal", sig.String())
|
||||||
|
case err := <-serverErr:
|
||||||
|
slog.Error("Сервер упал", "error", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
slog.Info("Приложение остановлено")
|
||||||
}
|
}
|
||||||
|
|||||||
11
configs/prometheus.yml
Normal file
11
configs/prometheus.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: "prometheus"
|
||||||
|
static_configs:
|
||||||
|
- targets: ["localhost:9090"]
|
||||||
|
|
||||||
|
- job_name: "handbooks"
|
||||||
|
static_configs:
|
||||||
|
- targets: ["app:3001"]
|
||||||
8
go.mod
8
go.mod
@@ -4,7 +4,10 @@ go 1.25.7
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/golang-cz/devslog v0.0.15
|
github.com/golang-cz/devslog v0.0.15
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/huandu/go-sqlbuilder v1.39.1
|
github.com/huandu/go-sqlbuilder v1.39.1
|
||||||
github.com/jackc/pgx/v5 v5.8.0
|
github.com/jackc/pgx/v5 v5.8.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
@@ -15,6 +18,8 @@ require (
|
|||||||
github.com/oapi-codegen/runtime v1.2.0
|
github.com/oapi-codegen/runtime v1.2.0
|
||||||
github.com/pressly/goose/v3 v3.27.0
|
github.com/pressly/goose/v3 v3.27.0
|
||||||
github.com/redis/go-redis/v9 v9.18.0
|
github.com/redis/go-redis/v9 v9.18.0
|
||||||
|
github.com/rs/xid v1.6.0
|
||||||
|
github.com/samber/slog-chi v1.19.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -22,7 +27,6 @@ require (
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/huandu/go-clone v1.7.3 // indirect
|
github.com/huandu/go-clone v1.7.3 // indirect
|
||||||
github.com/huandu/xstrings v1.4.0 // indirect
|
github.com/huandu/xstrings v1.4.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
@@ -34,6 +38,8 @@ require (
|
|||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
|||||||
14
go.sum
14
go.sum
@@ -15,10 +15,16 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
|||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||||
|
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/golang-cz/devslog v0.0.15 h1:ejoBLTCwJHWGbAmDf2fyTJJQO3AkzcPjw8SC9LaOQMI=
|
github.com/golang-cz/devslog v0.0.15 h1:ejoBLTCwJHWGbAmDf2fyTJJQO3AkzcPjw8SC9LaOQMI=
|
||||||
github.com/golang-cz/devslog v0.0.15/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8=
|
github.com/golang-cz/devslog v0.0.15/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
|
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
|
||||||
@@ -74,6 +80,10 @@ github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfS
|
|||||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/samber/slog-chi v1.19.0 h1:fl4qH5Hhk7feHtyp4CxJUt7U1TqjPrZ1uueDW9D+Cps=
|
||||||
|
github.com/samber/slog-chi v1.19.0/go.mod h1:a1iIuofF2gS1ii8aXIQhC6TEguLOhOvSM958fY5hToU=
|
||||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -84,6 +94,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
"github.com/pressly/goose/v3"
|
"github.com/pressly/goose/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anxi0uz/logiflow/internal/api"
|
||||||
|
"github.com/anxi0uz/logiflow/internal/config"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
slogchi "github.com/samber/slog-chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctxKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
requestIDKey ctxKey = "X-Request-ID"
|
||||||
|
tokenKey ctxKey = "Authorization"
|
||||||
|
)
|
||||||
|
|
||||||
|
type responseOptions struct {
|
||||||
|
respType string
|
||||||
|
requestID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
DB *pgxpool.Pool
|
||||||
|
Config *config.Config
|
||||||
|
ctx context.Context
|
||||||
|
Redis *redis.Client
|
||||||
|
JwtKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(db *pgxpool.Pool, redis *redis.Client, cfg *config.Config) *Server {
|
||||||
|
return &Server{
|
||||||
|
DB: db,
|
||||||
|
Redis: redis,
|
||||||
|
ctx: context.Background(),
|
||||||
|
Config: cfg,
|
||||||
|
JwtKey: []byte(cfg.JwtOpt.Key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Run() error {
|
||||||
|
r := chi.NewMux()
|
||||||
|
r.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: []string{"https://*", "http://*"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||||
|
ExposedHeaders: []string{"Link"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 300,
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.Use(slogchi.NewWithConfig(slog.Default(), slogchi.Config{
|
||||||
|
DefaultLevel: slog.LevelInfo,
|
||||||
|
ClientErrorLevel: slog.LevelWarn, // 400–499 → Warn
|
||||||
|
ServerErrorLevel: slog.LevelError, // 500+ → Error
|
||||||
|
WithRequestID: true, // берёт request-id из контекста
|
||||||
|
Filters: []slogchi.Filter{
|
||||||
|
slogchi.IgnorePath("/health", "/metrics", "/favicon.ico"),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.Use(s.AuthMiddleware)
|
||||||
|
r.Use(s.MiddlewareRequestID)
|
||||||
|
|
||||||
|
h := api.HandlerFromMux(s, r)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Handler: h,
|
||||||
|
Addr: s.Config.ServerURL(),
|
||||||
|
ReadTimeout: s.Config.ReadTimeout(),
|
||||||
|
IdleTimeout: s.Config.IdleTimeout(),
|
||||||
|
WriteTimeout: s.Config.WriteTimeout(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
slog.Error("HTTP сервер упал", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
slog.Info("Приложение запущено успешно ", slog.String("URL", s.Config.ServerURL()))
|
||||||
|
|
||||||
|
<-s.ctx.Done()
|
||||||
|
|
||||||
|
slog.Info("Остановка HTTP сервера...")
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
|
||||||
|
return srv.Shutdown(shutdownCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/auth/register" || r.URL.Path == "/auth/login" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr := r.Header.Get("Authorization")
|
||||||
|
if tokenStr == "" {
|
||||||
|
s.JSON(w, r, http.StatusUnauthorized, "missing token", "error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Проверка авторизации")
|
||||||
|
|
||||||
|
claims, err := s.validateAccessToken(r.Context(), tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
slog.WarnContext(r.Context(), "Токен не прошел валидацию", slog.String("Token", tokenStr), slog.String("error", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := "access_hash:" + tokenStr
|
||||||
|
if _, err := s.Redis.Get(r.Context(), key).Result(); err == redis.Nil {
|
||||||
|
s.JSON(w, r, http.StatusUnauthorized, "token revoked or expired", "error")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), "user", claims)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) MiddlewareRequestID(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rid := r.Header.Get("X-Request-ID")
|
||||||
|
if rid == "" {
|
||||||
|
rid = xid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), requestIDKey, rid)
|
||||||
|
|
||||||
|
w.Header().Set("X-Request-ID", rid)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) JSON(w http.ResponseWriter, r *http.Request, status int, payload any, respType string) {
|
||||||
|
options := responseOptions{
|
||||||
|
respType: respType,
|
||||||
|
requestID: extractRequestID(r),
|
||||||
|
}
|
||||||
|
|
||||||
|
success := status >= 200 && status < 300
|
||||||
|
|
||||||
|
resp := api.ApiResponse{
|
||||||
|
RequestID: &options.requestID,
|
||||||
|
Status: &status,
|
||||||
|
Success: &success,
|
||||||
|
Data: &map[string]interface{}{respType: payload},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "applicaton/json; charset=utf-8")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
slog.ErrorContext(r.Context(), "json encode failed after header written",
|
||||||
|
slog.Int("status", status),
|
||||||
|
slog.String("error", err.Error()),
|
||||||
|
slog.String("request_id", options.requestID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractRequestID(r *http.Request) string {
|
||||||
|
if r == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := r.Context().Value(requestIDKey); v != nil {
|
||||||
|
if id, ok := v.(string); ok {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) validateAccessToken(ctx context.Context, tokenStr string) (*Claims, error) {
|
||||||
|
claims := &Claims{}
|
||||||
|
token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
slog.WarnContext(ctx, "неизвестный алгоритм", "alg", t.Header["alg"])
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
return s.JwtKey, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("token parse error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.Header["alg"] != "HS256" {
|
||||||
|
return nil, errors.New("only HS256 allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
redisKey := "access_hash:" + tokenStr
|
||||||
|
if _, err := s.Redis.Get(ctx, redisKey).Result(); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "redis error during token validation", "error", err.Error())
|
||||||
|
return nil, fmt.Errorf("redis error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.TokenID == "" {
|
||||||
|
return nil, errors.New("missing token id (jti)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.ID == uuid.Nil {
|
||||||
|
return nil, errors.New("missing user id in claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|||||||
26
internal/handler/user.go
Normal file
26
internal/handler/user.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
TokenID string `json:"token_id"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) AuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
}
|
||||||
|
func (s *Server) AuthLogout(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {}
|
||||||
98
podman-compose.yml
Normal file
98
podman-compose.yml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: LogiflowNetwork
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
image: docker.io/library/postgres:latest
|
||||||
|
restart: always
|
||||||
|
container_name: logiflow-postgres
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql
|
||||||
|
- ./migrations:/docker-entrypoint-initdb.d
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${LOGIFLOW_DATABASE_PASSWORD}
|
||||||
|
POSTGRES_USER: ${LOGIFLOW_DATABASE_USER}
|
||||||
|
POSTGRES_DB: ${LOGIFLOW_DATABASE_NAME}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${LOGIFLOW_DATABASE_USER}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
image: docker.io/library/redis:7-alpine
|
||||||
|
container_name: logiflow-redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
command: redis-server --requirepass ${LOGIFLOW_REDIS_PASSWORD}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-a", "${LOGIFLOW_REDIS_PASSWORD}", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
image: docker.io/prom/prometheus:latest
|
||||||
|
container_name: logiflow-prometheus
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
volumes:
|
||||||
|
- ./configs/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
command:
|
||||||
|
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
image: docker.io/grafana/grafana:latest
|
||||||
|
container_name: logiflow-grafana
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
GF_SECURITY_ADMIN_USER: admin
|
||||||
|
GF_SECURITY_ADMIN_PASSWORD: admin
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./configs/datasources:/etc/grafana/provisioning/datasources:ro
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
|
||||||
|
app:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: logiflow-app
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
grafana_data:
|
||||||
Reference in New Issue
Block a user