Added dockerfile, docker compose, created server implementation with some middlewares
This commit is contained in:
@@ -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) {}
|
||||
Reference in New Issue
Block a user