feat(user): migrate from gRPC to REST
This commit is contained in:
parent
2b94be99e3
commit
7b8d15a2c9
20 changed files with 977 additions and 1150 deletions
16
Makefile
16
Makefile
|
@ -1,19 +1,9 @@
|
|||
tag = latest
|
||||
|
||||
gen:
|
||||
@protoc --proto_path=proto --go_opt=paths=source_relative \
|
||||
--go_out=proto --go-grpc_out=proto --grpc-gateway_out=proto \
|
||||
proto/user/v1/user.proto
|
||||
gen-openapi:
|
||||
@protoc --proto_path=proto --openapi_out=proto/user/v1 \
|
||||
proto/user/v1/user.proto
|
||||
dev:
|
||||
@make gen
|
||||
@oapi-codegen --config=config.yaml ./proto/user/v1/openapi.yaml
|
||||
dev: gen
|
||||
@go run cmd/ms-auth/main.go
|
||||
proxy:
|
||||
@make gen
|
||||
@go run cmd/ms-auth-proxy/main.go
|
||||
build:
|
||||
@make gen
|
||||
build: gen
|
||||
@docker build . -t ms-auth:${tag}
|
||||
@#docker push ms-auth:${tag}
|
|
@ -1,144 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-auth/config"
|
||||
delivery "git.sch9.ru/new_gate/ms-auth/internal/users/delivery/grpc"
|
||||
userv1gw "git.sch9.ru/new_gate/ms-auth/proto/user/v1"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
"github.com/rs/cors"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CustomOutgoingHeaderMatcher(key string) (string, bool) {
|
||||
if key == delivery.SessionHeaderName {
|
||||
return "Set-Cookie", true
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", runtime.MetadataHeaderPrefix, key), true
|
||||
}
|
||||
|
||||
func CustomIncomingHeaderMatcher(key string) (string, bool) {
|
||||
if key == "Cookie" {
|
||||
return "Cookie", true
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", runtime.MetadataPrefix, key), true
|
||||
}
|
||||
|
||||
func UnaryClientInterceptor(
|
||||
ctx context.Context,
|
||||
method string,
|
||||
req, reply interface{},
|
||||
cc *grpc.ClientConn,
|
||||
invoker grpc.UnaryInvoker,
|
||||
opts ...grpc.CallOption,
|
||||
) error {
|
||||
md, ok := metadata.FromOutgoingContext(ctx)
|
||||
|
||||
hasCookie := ok && len(md.Get("Cookie")) > 0
|
||||
|
||||
if hasCookie {
|
||||
cookies, err := http.ParseCookie(md.Get("Cookie")[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(cookies) != 1 {
|
||||
return errors.New("invalid cookie")
|
||||
}
|
||||
|
||||
md.Set(delivery.SessionHeaderName, cookies[0].Value)
|
||||
}
|
||||
|
||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||
|
||||
err := invoker(ctx, method, req, reply, cc, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
header, ok := o.(grpc.HeaderCallOption)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
values := header.HeaderAddr.Get(delivery.SessionHeaderName)
|
||||
|
||||
if len(values) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
sessionId := values[0]
|
||||
|
||||
cookie := http.Cookie{
|
||||
Name: "SESSIONID",
|
||||
Value: sessionId,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
}
|
||||
|
||||
if len(sessionId) == 0 {
|
||||
cookie.Expires = time.Unix(0, 0)
|
||||
} else {
|
||||
cookie.MaxAge = 3600
|
||||
}
|
||||
|
||||
header.HeaderAddr.Set(delivery.SessionHeaderName, cookie.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var cfg config.Config
|
||||
err := cleanenv.ReadConfig(".env", &cfg)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error reading config: %s", err.Error()))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
mux := runtime.NewServeMux(runtime.WithOutgoingHeaderMatcher(CustomOutgoingHeaderMatcher), runtime.WithIncomingHeaderMatcher(CustomIncomingHeaderMatcher))
|
||||
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithChainUnaryInterceptor(UnaryClientInterceptor)}
|
||||
err = userv1gw.RegisterUserServiceHandlerFromEndpoint(ctx, mux, cfg.Address, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"http://*", "https://*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Content-Type", "Set-Cookie", "Credentials"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
|
||||
go func() {
|
||||
err = http.ListenAndServe(cfg.ProxyAddress, c.Handler(mux))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Println("server proxy started")
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
<-stop
|
||||
return
|
||||
}
|
|
@ -5,17 +5,16 @@ import (
|
|||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-auth/config"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/models"
|
||||
usersDelivery "git.sch9.ru/new_gate/ms-auth/internal/users/delivery/grpc"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/users/delivery/rest"
|
||||
usersRepository "git.sch9.ru/new_gate/ms-auth/internal/users/repository"
|
||||
usersUseCase "git.sch9.ru/new_gate/ms-auth/internal/users/usecase"
|
||||
"git.sch9.ru/new_gate/ms-auth/pkg"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
|
||||
userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
fiberlogger "github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
@ -23,42 +22,42 @@ import (
|
|||
|
||||
// InterceptorLogger adapts zap logger to interceptor logger.
|
||||
// This code is simple enough to be copied and not imported.
|
||||
func InterceptorLogger(l *zap.Logger) logging.Logger {
|
||||
return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) {
|
||||
f := make([]zap.Field, 0, len(fields)/2)
|
||||
|
||||
for i := 0; i < len(fields); i += 2 {
|
||||
key := fields[i]
|
||||
value := fields[i+1]
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
f = append(f, zap.String(key.(string), v))
|
||||
case int:
|
||||
f = append(f, zap.Int(key.(string), v))
|
||||
case bool:
|
||||
f = append(f, zap.Bool(key.(string), v))
|
||||
default:
|
||||
f = append(f, zap.Any(key.(string), v))
|
||||
}
|
||||
}
|
||||
|
||||
logger := l.WithOptions(zap.AddCallerSkip(1)).With(f...)
|
||||
|
||||
switch lvl {
|
||||
case logging.LevelDebug:
|
||||
logger.Debug(msg)
|
||||
case logging.LevelInfo:
|
||||
logger.Info(msg)
|
||||
case logging.LevelWarn:
|
||||
logger.Warn(msg)
|
||||
case logging.LevelError:
|
||||
logger.Error(msg)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown level %v", lvl))
|
||||
}
|
||||
})
|
||||
}
|
||||
//func InterceptorLogger(l *zap.Logger) fiber.Handler {
|
||||
// return func(ctx context.Context, lvl logging.Level, msg string, fields ...any) {
|
||||
// f := make([]zap.Field, 0, len(fields)/2)
|
||||
//
|
||||
// for i := 0; i < len(fields); i += 2 {
|
||||
// key := fields[i]
|
||||
// value := fields[i+1]
|
||||
//
|
||||
// switch v := value.(type) {
|
||||
// case string:
|
||||
// f = append(f, zap.String(key.(string), v))
|
||||
// case int:
|
||||
// f = append(f, zap.Int(key.(string), v))
|
||||
// case bool:
|
||||
// f = append(f, zap.Bool(key.(string), v))
|
||||
// default:
|
||||
// f = append(f, zap.Any(key.(string), v))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// logger := l.WithOptions(zap.AddCallerSkip(1)).With(f...)
|
||||
//
|
||||
// switch lvl {
|
||||
// case logging.LevelDebug:
|
||||
// logger.Debug(msg)
|
||||
// case logging.LevelInfo:
|
||||
// logger.Info(msg)
|
||||
// case logging.LevelWarn:
|
||||
// logger.Warn(msg)
|
||||
// case logging.LevelError:
|
||||
// logger.Error(msg)
|
||||
// default:
|
||||
// panic(fmt.Sprintf("unknown level %v", lvl))
|
||||
// }
|
||||
// })
|
||||
//}
|
||||
|
||||
func main() {
|
||||
var cfg config.Config
|
||||
|
@ -101,22 +100,25 @@ func main() {
|
|||
sessionRepo := usersRepository.NewValkeyRepository(vk)
|
||||
userUC := usersUseCase.NewUseCase(userRepo, sessionRepo, cfg)
|
||||
|
||||
gserver := grpc.NewServer(grpc.ChainUnaryInterceptor(
|
||||
logging.UnaryServerInterceptor(InterceptorLogger(logger)),
|
||||
))
|
||||
defer gserver.GracefulStop()
|
||||
server := fiber.New()
|
||||
|
||||
usersDelivery.NewUserHandlers(gserver, userUC)
|
||||
reflection.Register(gserver)
|
||||
|
||||
ln, err := net.Listen("tcp", cfg.Address)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
userv1.RegisterHandlersWithOptions(server, rest.NewUserHandlers(userUC, cfg.JWTSecret), userv1.FiberServerOptions{
|
||||
Middlewares: []userv1.MiddlewareFunc{
|
||||
fiberlogger.New(),
|
||||
rest.AuthMiddleware(cfg.JWTSecret, userUC),
|
||||
//cors.New(cors.Config{
|
||||
// AllowOrigins: "http://localhost:3000",
|
||||
// AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
||||
// AllowHeaders: "Content-Type,Set-Cookie,Credentials",
|
||||
// AllowCredentials: true,
|
||||
//}),
|
||||
},
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err = gserver.Serve(ln); err != nil {
|
||||
panic(err)
|
||||
err := server.Listen(cfg.Address)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("error starting server: %s", err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
5
config.yaml
Normal file
5
config.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
package: userv1
|
||||
generate:
|
||||
fiber-server: true
|
||||
models: true
|
||||
output: ./proto/user/v1/user.go
|
58
go.mod
58
go.mod
|
@ -4,42 +4,76 @@ go 1.23.2
|
|||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/open-policy-agent/opa v1.1.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/valkey-io/valkey-go v1.0.47
|
||||
github.com/valkey-io/valkey-go/mock v1.0.47
|
||||
go.uber.org/mock v0.4.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.35.2
|
||||
golang.org/x/crypto v0.32.0
|
||||
google.golang.org/grpc v1.70.0
|
||||
google.golang.org/protobuf v1.36.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // 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/klauspost/compress v1.17.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // 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/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/yashtewari/glob-intersection v0.2.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
|
|
175
go.sum
175
go.sum
|
@ -1,28 +1,79 @@
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
|
||||
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
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/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
|
||||
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=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps=
|
||||
github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA=
|
||||
github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I=
|
||||
github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
|
||||
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8=
|
||||
github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
|
||||
|
@ -39,32 +90,101 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
|||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/open-policy-agent/opa v1.1.0 h1:HMz2evdEMTyNqtdLjmu3Vyx06BmhNYAx67Yz3Ll9q2s=
|
||||
github.com/open-policy-agent/opa v1.1.0/go.mod h1:T1pASQ1/vwfTa+e2fYcfpLCvWgYtqtiUv+IuA/dLPQs=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM=
|
||||
github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
|
||||
github.com/valkey-io/valkey-go v1.0.47 h1:fW5+m2BaLAbxB1EWEEWmj+i2n+YcYFBDG/jKs6qu5j8=
|
||||
github.com/valkey-io/valkey-go v1.0.47/go.mod h1:BXlVAPIL9rFQinSFM+N32JfWzfCaUAqBpZkc4vPY6fM=
|
||||
github.com/valkey-io/valkey-go/mock v1.0.47 h1:fQZUJrJEx4IG7vH1CjSSqPmx+5Gd6cwwdr7gcDDAIe0=
|
||||
github.com/valkey-io/valkey-go/mock v1.0.47/go.mod h1:k+lHD29cYer1FO+/HyxWLDUU1JakG6uQ/VgR1OGqe+I=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
|
||||
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
|
@ -73,26 +193,33 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
|||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
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/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
@ -101,3 +228,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Id string `json:"id" db:"id"`
|
||||
UserId int32 `json:"user_id" db:"user_id"`
|
||||
Role Role `json:"role" db:"role"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
//UserAgent string `json:"user_agent"`
|
||||
//Ip string `json:"ip"`
|
||||
Id string `json:"id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Ip string `json:"ip"`
|
||||
}
|
||||
|
||||
func (s Session) Valid() error {
|
||||
|
@ -26,40 +26,46 @@ func (s Session) Valid() error {
|
|||
if s.CreatedAt.IsZero() {
|
||||
return errors.New("empty created at")
|
||||
}
|
||||
if !s.Role.IsAdmin() && !s.Role.IsModerator() && !s.Role.IsParticipant() {
|
||||
return errors.New("invalid role")
|
||||
if s.ExpiresAt.IsZero() {
|
||||
return errors.New("empty expires at")
|
||||
}
|
||||
//if s.UserAgent == "" {
|
||||
// return errors.New("empty user agent")
|
||||
//}
|
||||
//if s.Ip == "" {
|
||||
// return errors.New("empty ip")
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSession(userId int32, role Role) (string, string, error) {
|
||||
s := &Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: userId,
|
||||
Role: role,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := s.Valid(); err != nil {
|
||||
return "", "", err
|
||||
type JWT struct {
|
||||
SessionId string `json:"session_id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
ExpiresAt int64 `json:"exp"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
NotBefore int64 `json:"nbf"`
|
||||
Permissions []grant `json:"permissions"`
|
||||
}
|
||||
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
func (j JWT) Valid() error {
|
||||
if uuid.Validate(j.SessionId) != nil {
|
||||
return errors.New("invalid session id")
|
||||
}
|
||||
|
||||
return string(b), s.Id, nil
|
||||
if j.UserId == 0 {
|
||||
return errors.New("empty user id")
|
||||
}
|
||||
|
||||
func ParseSession(s string) (*Session, error) {
|
||||
sess := &Session{}
|
||||
if err := json.Unmarshal([]byte(s), sess); err != nil {
|
||||
return nil, err
|
||||
if j.ExpiresAt == 0 {
|
||||
return errors.New("empty expires at")
|
||||
}
|
||||
|
||||
if err := sess.Valid(); err != nil {
|
||||
return nil, err
|
||||
if j.IssuedAt == 0 {
|
||||
return errors.New("empty issued at")
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
if j.NotBefore == 0 {
|
||||
return errors.New("empty not before")
|
||||
}
|
||||
if len(j.Permissions) == 0 {
|
||||
return errors.New("empty permissions")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/open-policy-agent/opa/v1/rego"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"time"
|
||||
)
|
||||
|
@ -9,30 +11,90 @@ import (
|
|||
type Role int32
|
||||
|
||||
const (
|
||||
RoleParticipant Role = 0
|
||||
RoleModerator Role = 1
|
||||
RoleGuest Role = -1
|
||||
RoleStudent Role = 0
|
||||
RoleTeacher Role = 1
|
||||
RoleAdmin Role = 2
|
||||
)
|
||||
|
||||
func (role Role) IsAdmin() bool {
|
||||
return role == RoleAdmin
|
||||
func (r Role) String() string {
|
||||
switch r {
|
||||
case RoleGuest:
|
||||
return "guest"
|
||||
case RoleStudent:
|
||||
return "student"
|
||||
case RoleTeacher:
|
||||
return "teacher"
|
||||
case RoleAdmin:
|
||||
return "admin"
|
||||
}
|
||||
|
||||
func (role Role) IsModerator() bool {
|
||||
return role == RoleModerator
|
||||
panic("invalid role")
|
||||
}
|
||||
|
||||
func (role Role) IsParticipant() bool {
|
||||
return role == RoleParticipant
|
||||
type Action string
|
||||
|
||||
const (
|
||||
Create Action = "create"
|
||||
Read Action = "read"
|
||||
Update Action = "update"
|
||||
Delete Action = "delete"
|
||||
)
|
||||
|
||||
type Resource string
|
||||
|
||||
const (
|
||||
ResourceAnotherUser Resource = "another-user"
|
||||
ResourceMeUser Resource = "me-user"
|
||||
ResourceListUser Resource = "list-user"
|
||||
|
||||
ResourceOwnSession Resource = "own-session"
|
||||
)
|
||||
|
||||
type grant struct {
|
||||
Action Action `json:"action"`
|
||||
Resource Resource `json:"resource"`
|
||||
}
|
||||
|
||||
func (role Role) AtLeast(other Role) bool {
|
||||
return role >= other
|
||||
var Grants = map[string][]grant{
|
||||
RoleGuest.String(): {},
|
||||
RoleStudent.String(): {
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
RoleTeacher.String(): {
|
||||
{Create, ResourceAnotherUser},
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Read, ResourceListUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
RoleAdmin.String(): {
|
||||
{Create, ResourceAnotherUser},
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Read, ResourceListUser},
|
||||
{Update, ResourceAnotherUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceAnotherUser},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
}
|
||||
|
||||
func (role Role) AtMost(other Role) bool {
|
||||
return role <= other
|
||||
const module = `package app.rbac
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
some grant in input.role_grants[input.role]
|
||||
|
||||
input.action == grant.action
|
||||
input.resource == grant.resource
|
||||
}
|
||||
`
|
||||
|
||||
type User struct {
|
||||
Id int32 `db:"id"`
|
||||
|
@ -43,10 +105,61 @@ type User struct {
|
|||
Role Role `db:"role"`
|
||||
}
|
||||
|
||||
func (user *User) ComparePassword(password string) error {
|
||||
func (user *User) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]interface{}{
|
||||
"id": user.Id,
|
||||
"username": user.Username,
|
||||
"created_at": user.CreatedAt,
|
||||
"modified_at": user.ModifiedAt,
|
||||
"role": user.Role,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (user *User) IsSamePwd(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return errors.New("bad username or password")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var query rego.PreparedEvalQuery
|
||||
|
||||
func (r Role) HasPermission(action Action, resource Resource) bool {
|
||||
ctx := context.TODO()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"action": action,
|
||||
"resource": resource,
|
||||
"role": r.String(),
|
||||
"role_grants": Grants,
|
||||
}
|
||||
|
||||
results, err := query.Eval(ctx, rego.EvalInput(input))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return results.Allowed()
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
ctx := context.TODO()
|
||||
|
||||
query, err = rego.New(
|
||||
rego.Query("data.app.rbac.allow"),
|
||||
rego.Module("ms-auth.rego", module),
|
||||
).PrepareForEval(ctx)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type UserHandlers interface {
|
||||
CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error)
|
||||
GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error)
|
||||
UpdateUser(ctx context.Context, req *userv1.UpdateUserRequest) (*emptypb.Empty, error)
|
||||
DeleteUser(ctx context.Context, req *userv1.DeleteUserRequest) (*emptypb.Empty, error)
|
||||
Login(ctx context.Context, req *userv1.LoginRequest) (*emptypb.Empty, error)
|
||||
Verify(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error)
|
||||
Refresh(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error)
|
||||
Logout(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error)
|
||||
CompleteLogout(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error)
|
||||
ListSessions(c *fiber.Ctx) error
|
||||
CompleteLogout(c *fiber.Ctx) error
|
||||
Login(c *fiber.Ctx) error
|
||||
Logout(c *fiber.Ctx) error
|
||||
Refresh(c *fiber.Ctx) error
|
||||
Verify(c *fiber.Ctx) error
|
||||
ListUsers(c *fiber.Ctx, params userv1.ListUsersParams) error
|
||||
CreateUser(c *fiber.Ctx) error
|
||||
GetMe(c *fiber.Ctx) error
|
||||
DeleteUser(c *fiber.Ctx, id int32) error
|
||||
GetUser(c *fiber.Ctx, id int32) error
|
||||
UpdateUser(c *fiber.Ctx, id int32) error
|
||||
}
|
||||
|
|
|
@ -1,283 +0,0 @@
|
|||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-auth/pkg"
|
||||
userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UserHandlers struct {
|
||||
userv1.UnimplementedUserServiceServer
|
||||
userUC users.UseCase
|
||||
}
|
||||
|
||||
func NewUserHandlers(gserver *grpc.Server, userUC users.UseCase) {
|
||||
handlers := &UserHandlers{
|
||||
userUC: userUC,
|
||||
}
|
||||
|
||||
userv1.RegisterUserServiceServer(gserver, handlers)
|
||||
}
|
||||
|
||||
const (
|
||||
SessionHeaderName = "x-session-id"
|
||||
AuthUserHeaderName = "x-auth-user-id"
|
||||
)
|
||||
|
||||
func (h *UserHandlers) Login(ctx context.Context, req *userv1.LoginRequest) (*emptypb.Empty, error) {
|
||||
const op = "UserHandlers.Login"
|
||||
|
||||
var (
|
||||
err error
|
||||
user *models.User
|
||||
)
|
||||
|
||||
username := req.GetUsername()
|
||||
password := req.GetPassword()
|
||||
|
||||
user, err = h.userUC.ReadUserByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
|
||||
err = user.ComparePassword(password)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(pkg.ErrNotFound, err, op, "bad username or password"))
|
||||
}
|
||||
|
||||
sessionId, err := h.userUC.CreateSession(ctx, user.Id, user.Role)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
|
||||
header := metadata.New(map[string]string{
|
||||
SessionHeaderName: sessionId,
|
||||
})
|
||||
err = grpc.SendHeader(ctx, header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func AuthSessionIdFromContext(ctx context.Context) (string, error) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
|
||||
if !ok {
|
||||
return "", errors.New("failed to get metadata")
|
||||
}
|
||||
tokens := md.Get(SessionHeaderName)
|
||||
sessionId := strings.Join(tokens, "")
|
||||
if len(sessionId) == 0 {
|
||||
return "", errors.New("no session id in context")
|
||||
}
|
||||
return sessionId, nil
|
||||
}
|
||||
|
||||
func (h *UserHandlers) Refresh(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
const op = "UserHandlers.Refresh"
|
||||
|
||||
sessionId, err := AuthSessionIdFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context"))
|
||||
}
|
||||
err = h.userUC.UpdateSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandlers) Logout(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
const op = "UserHandlers.Logout"
|
||||
|
||||
sessionId, err := AuthSessionIdFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context"))
|
||||
}
|
||||
err = h.userUC.DeleteSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
|
||||
header := metadata.New(map[string]string{
|
||||
SessionHeaderName: "",
|
||||
})
|
||||
err = grpc.SendHeader(ctx, header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandlers) CompleteLogout(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
const op = "UserHandlers.CompleteLogout"
|
||||
|
||||
sessionId, err := AuthSessionIdFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context"))
|
||||
}
|
||||
|
||||
session, err := h.userUC.ReadSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
|
||||
err = h.userUC.DeleteAllSessions(ctx, session.UserId)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandlers) Verify(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
const op = "UserHandlers.Verify"
|
||||
|
||||
sessionId, err := AuthSessionIdFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context"))
|
||||
}
|
||||
token, err := h.userUC.Verify(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
|
||||
header := metadata.New(map[string]string{
|
||||
AuthUserHeaderName: token,
|
||||
})
|
||||
err = grpc.SendHeader(ctx, header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandlers) CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error) {
|
||||
const op = "UserHandlers.CreateUser"
|
||||
|
||||
sessionId, err := AuthSessionIdFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context"))
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, "userId", sessionId)
|
||||
|
||||
id, err := h.userUC.CreateUser(
|
||||
ctx,
|
||||
req.GetUsername(),
|
||||
req.GetPassword(),
|
||||
models.RoleParticipant,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
|
||||
return &userv1.CreateUserResponse{
|
||||
Id: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandlers) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
|
||||
const op = "UserHandlers.GetUser"
|
||||
|
||||
var userId = req.GetId()
|
||||
|
||||
if req.GetMe() {
|
||||
sessionId, err := AuthSessionIdFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context"))
|
||||
}
|
||||
|
||||
session, err := h.userUC.ReadSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
|
||||
userId = session.UserId
|
||||
}
|
||||
|
||||
user, err := h.userUC.ReadUserById(
|
||||
ctx,
|
||||
userId,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
|
||||
return &userv1.GetUserResponse{
|
||||
User: &userv1.User{
|
||||
Id: user.Id,
|
||||
Username: user.Username,
|
||||
CreatedAt: timestamppb.New(user.CreatedAt),
|
||||
ModifiedAt: timestamppb.New(user.ModifiedAt),
|
||||
Role: userv1.Role(user.Role),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandlers) UpdateUser(ctx context.Context, req *userv1.UpdateUserRequest) (*emptypb.Empty, error) {
|
||||
const op = "UserHandlers.UpdateUser"
|
||||
|
||||
sessionId, err := AuthSessionIdFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context"))
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, "userId", sessionId)
|
||||
|
||||
err = h.userUC.UpdateUser(
|
||||
ctx,
|
||||
req.GetId(),
|
||||
AsStringP(req.Username),
|
||||
AsMRoleP(req.Role),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandlers) DeleteUser(ctx context.Context, req *userv1.DeleteUserRequest) (*emptypb.Empty, error) {
|
||||
const op = "UserHandlers.DeleteUser"
|
||||
|
||||
sessionId, err := AuthSessionIdFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context"))
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, "userId", sessionId)
|
||||
|
||||
err = h.userUC.DeleteUser(
|
||||
ctx,
|
||||
req.GetId(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, pkg.ToGRPC(err)
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func AsMRoleP(v userv1.Role) *models.Role {
|
||||
vv := models.Role(v.Number())
|
||||
return &vv
|
||||
}
|
||||
|
||||
func AsRoleP(v models.Role) *models.Role {
|
||||
return &v
|
||||
}
|
||||
|
||||
func AsStringP(str string) *string {
|
||||
return &str
|
||||
}
|
|
@ -1,428 +0,0 @@
|
|||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/users"
|
||||
mock_users "git.sch9.ru/new_gate/ms-auth/internal/users/delivery/mock"
|
||||
userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func startServer(t *testing.T, uc users.UseCase, addr string) {
|
||||
t.Helper()
|
||||
|
||||
gserver := grpc.NewServer()
|
||||
NewUserHandlers(gserver, uc)
|
||||
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = gserver.Serve(ln); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
gserver.Stop()
|
||||
})
|
||||
}
|
||||
|
||||
func buildClient(t *testing.T, addr string) userv1.UserServiceClient {
|
||||
t.Helper()
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.NoError(t, err)
|
||||
|
||||
return userv1.NewUserServiceClient(conn)
|
||||
}
|
||||
|
||||
func TestUserHandlers_Login(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62999"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid login", func(t *testing.T) {
|
||||
password := "password"
|
||||
hpwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
require.NoError(t, err)
|
||||
|
||||
user := &models.User{
|
||||
Id: 1,
|
||||
Username: "username",
|
||||
HashedPassword: string(hpwd),
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
sid := uuid.NewString()
|
||||
|
||||
uc.EXPECT().ReadUserByUsername(gomock.Any(), user.Username).Return(user, nil)
|
||||
uc.EXPECT().CreateSession(gomock.Any(), user.Id, user.Role).Return(sid, nil)
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
var header metadata.MD
|
||||
_, err = client.Login(ctx, &userv1.LoginRequest{
|
||||
Username: user.Username,
|
||||
Password: password,
|
||||
}, grpc.Header(&header))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, sid, header.Get(SessionHeaderName)[0])
|
||||
})
|
||||
|
||||
t.Run("invalid login (wrong password)", func(t *testing.T) {
|
||||
password := "password"
|
||||
hpwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
require.NoError(t, err)
|
||||
|
||||
user := &models.User{
|
||||
Id: 1,
|
||||
Username: "username",
|
||||
HashedPassword: string(hpwd),
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
uc.EXPECT().ReadUserByUsername(gomock.Any(), user.Username).Return(user, nil)
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, err = client.Login(ctx, &userv1.LoginRequest{
|
||||
Username: user.Username,
|
||||
Password: "wrongpassword",
|
||||
})
|
||||
|
||||
s, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, codes.NotFound, s.Code())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandlers_Refresh(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62998"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid refresh", func(t *testing.T) {
|
||||
sid := uuid.NewString()
|
||||
uc.EXPECT().UpdateSession(gomock.Any(), sid).Return(nil)
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, sid)
|
||||
|
||||
_, err := client.Refresh(ctx, &emptypb.Empty{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid refresh (no session id in context)", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, err := client.Refresh(ctx, &emptypb.Empty{})
|
||||
|
||||
s, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, codes.Unauthenticated, s.Code())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandlers_Logout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62997"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid logout", func(t *testing.T) {
|
||||
sid := uuid.NewString()
|
||||
uc.EXPECT().DeleteSession(gomock.Any(), sid).Return(nil)
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, sid)
|
||||
|
||||
_, err := client.Logout(ctx, &emptypb.Empty{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid logout (no session id in context)", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, err := client.Logout(ctx, &emptypb.Empty{})
|
||||
|
||||
s, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, codes.Unauthenticated, s.Code())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandlers_CompleteLogout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62996"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid complete logout", func(t *testing.T) {
|
||||
sid := uuid.NewString()
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, sid)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
uc.EXPECT().ReadSession(gomock.Any(), sid).Return(&models.Session{UserId: 1}, nil)
|
||||
uc.EXPECT().DeleteAllSessions(gomock.Any(), int32(1)).Return(nil)
|
||||
|
||||
_, err := client.CompleteLogout(ctx, &emptypb.Empty{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid complete logout (no session id in context)", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, err := client.CompleteLogout(ctx, &emptypb.Empty{})
|
||||
|
||||
s, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, codes.Unauthenticated, s.Code())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandlers_Verify(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62995"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid verify", func(t *testing.T) {
|
||||
sid := uuid.NewString()
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, sid)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
uc.EXPECT().Verify(gomock.Any(), sid).Return("jwt", nil)
|
||||
|
||||
var header metadata.MD
|
||||
_, err := client.Verify(ctx, &emptypb.Empty{}, grpc.Header(&header))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, header.Get(AuthUserHeaderName)[0], "jwt")
|
||||
})
|
||||
|
||||
t.Run("invalid verify (no session id in context)", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, err := client.Verify(ctx, &emptypb.Empty{})
|
||||
|
||||
s, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, codes.Unauthenticated, s.Code())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandlers_CreateUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62994"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid create user", func(t *testing.T) {
|
||||
username := "username"
|
||||
password := "password"
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, uuid.NewString())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
uc.EXPECT().CreateUser(gomock.Any(), username, password, models.RoleParticipant).Return(int32(2), nil)
|
||||
|
||||
_, err := client.CreateUser(ctx, &userv1.CreateUserRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid create user (no session id in context)", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, err := client.CreateUser(ctx, &userv1.CreateUserRequest{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
})
|
||||
|
||||
s, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, codes.Unauthenticated, s.Code())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandlers_GetUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62993"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid get user", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
uc.EXPECT().ReadUserById(gomock.Any(), int32(1)).Return(&models.User{
|
||||
Id: 1,
|
||||
Username: "username",
|
||||
CreatedAt: time.Now(),
|
||||
ModifiedAt: time.Now(),
|
||||
Role: models.RoleParticipant,
|
||||
}, nil)
|
||||
|
||||
_, err := client.GetUser(ctx, &userv1.GetUserRequest{
|
||||
Id: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandlers_UpdateUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62992"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid update user", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, uuid.NewString())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
uc.EXPECT().UpdateUser(gomock.Any(),
|
||||
int32(1),
|
||||
AsStringP("username"),
|
||||
AsRoleP(models.RoleModerator),
|
||||
).Return(nil)
|
||||
|
||||
_, err := client.UpdateUser(ctx, &userv1.UpdateUserRequest{
|
||||
Id: 1,
|
||||
Username: "username",
|
||||
Role: userv1.Role_ROLE_MODERATOR,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid update user (no session id in context)", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, err := client.UpdateUser(ctx, &userv1.UpdateUserRequest{
|
||||
Id: 1,
|
||||
Username: "username",
|
||||
Role: userv1.Role_ROLE_MODERATOR,
|
||||
})
|
||||
|
||||
s, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, codes.Unauthenticated, s.Code())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandlers_DeleteUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const addr = "127.0.0.1:62991"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
uc := mock_users.NewMockUseCase(ctrl)
|
||||
startServer(t, uc, addr)
|
||||
|
||||
client := buildClient(t, addr)
|
||||
|
||||
t.Run("valid delete user", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, uuid.NewString())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
uc.EXPECT().DeleteUser(gomock.Any(), int32(1)).Return(nil)
|
||||
|
||||
_, err := client.DeleteUser(ctx, &userv1.DeleteUserRequest{
|
||||
Id: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
265
internal/users/delivery/rest/handlers.go
Normal file
265
internal/users/delivery/rest/handlers.go
Normal file
|
@ -0,0 +1,265 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-auth/pkg"
|
||||
userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserHandlers struct {
|
||||
userUC users.UseCase
|
||||
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
func NewUserHandlers(userUC users.UseCase, jwtSecret string) *UserHandlers {
|
||||
return &UserHandlers{
|
||||
userUC: userUC,
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UserHandlers) Login(c *fiber.Ctx) error {
|
||||
const op = "UserHandlers.Login"
|
||||
|
||||
authHeader := c.Get("Authorization", "")
|
||||
if authHeader == "" {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
authParts := strings.Split(authHeader, " ")
|
||||
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "basic" {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
decodedAuth, err := base64.StdEncoding.DecodeString(authParts[1])
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
authParts = strings.Split(string(decodedAuth), ":")
|
||||
if len(authParts) != 2 {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
user, err := h.userUC.ReadUserByUsername(ctx, authParts[0])
|
||||
if err != nil {
|
||||
if errors.Is(err, pkg.ErrNotFound) {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
if !user.IsSamePwd(authParts[1]) {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
userAgent := c.Get("User-Agent", "")
|
||||
ip := c.IP()
|
||||
|
||||
session, err := h.userUC.CreateSession(ctx, user.Id, user.Role, userAgent, ip)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, models.JWT{
|
||||
SessionId: session.Id,
|
||||
UserId: user.Id,
|
||||
Role: user.Role,
|
||||
ExpiresAt: session.ExpiresAt.Unix(),
|
||||
IssuedAt: time.Now().Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
Permissions: models.Grants[user.Role.String()],
|
||||
})
|
||||
|
||||
token, err := claims.SignedString([]byte(h.jwtSecret))
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
|
||||
c.Set("Authorization", "Bearer "+token)
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *UserHandlers) Refresh(c *fiber.Ctx) error {
|
||||
const op = "UserHandlers.Refresh"
|
||||
|
||||
token, ok := c.Locals(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
err := h.userUC.UpdateSession(c.Context(), token.SessionId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *UserHandlers) Logout(c *fiber.Ctx) error {
|
||||
const op = "UserHandlers.Logout"
|
||||
|
||||
token, ok := c.Locals(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
err := h.userUC.DeleteSession(c.Context(), token.SessionId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *UserHandlers) CompleteLogout(c *fiber.Ctx) error {
|
||||
const op = "UserHandlers.CompleteLogout"
|
||||
|
||||
token, ok := c.Locals(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
err := h.userUC.DeleteAllSessions(ctx, token.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *UserHandlers) Verify(c *fiber.Ctx) error {
|
||||
const op = "UserHandlers.Verify"
|
||||
|
||||
return c.SendStatus(fiber.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (h *UserHandlers) CreateUser(c *fiber.Ctx) error {
|
||||
const op = "UserHandlers.CreateUser"
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
id, err := h.userUC.CreateUser(
|
||||
ctx,
|
||||
c.FormValue("username"),
|
||||
c.FormValue("password"),
|
||||
models.RoleStudent,
|
||||
)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(map[string]interface{}{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandlers) GetMe(c *fiber.Ctx) error {
|
||||
const op = "UserHandlers.GetMe"
|
||||
|
||||
token, ok := c.Locals(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
user, err := h.userUC.ReadUserById(c.Context(), token.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(map[string]interface{}{
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandlers) GetUser(c *fiber.Ctx, id int32) error {
|
||||
const op = "UserHandlers.GetUser"
|
||||
|
||||
user, err := h.userUC.ReadUserById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(map[string]interface{}{
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandlers) UpdateUser(c *fiber.Ctx, id int32) error {
|
||||
const op = "UserHandlers.UpdateUser"
|
||||
|
||||
var req = &userv1.UpdateUserRequest{}
|
||||
|
||||
err := c.BodyParser(req)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
err = h.userUC.UpdateUser(c.Context(), id, req.Username, int32PtrToRolePtr(req.Role))
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *UserHandlers) DeleteUser(c *fiber.Ctx, id int32) error {
|
||||
const op = "UserHandlers.DeleteUser"
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
err := h.userUC.DeleteUser(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *UserHandlers) ListUsers(c *fiber.Ctx, params userv1.ListUsersParams) error {
|
||||
const op = "UserHandlers.ListUsers"
|
||||
|
||||
usersList, count, err := h.userUC.ListUsers(c.Context(), params.Page, params.PageSize)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(map[string]interface{}{
|
||||
"users": usersList,
|
||||
"page": params.Page,
|
||||
"max_page": func() int32 {
|
||||
if count%params.PageSize == 0 {
|
||||
return count / params.PageSize
|
||||
}
|
||||
return count/params.PageSize + 1
|
||||
}(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandlers) ListSessions(c *fiber.Ctx) error {
|
||||
const op = "UserHandlers.ListSessions"
|
||||
|
||||
return c.SendStatus(fiber.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func int32PtrToRolePtr(i *int32) *models.Role {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
ii := models.Role(*i)
|
||||
return &ii
|
||||
}
|
74
internal/users/delivery/rest/middleware.go
Normal file
74
internal/users/delivery/rest/middleware.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-auth/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenKey = "token"
|
||||
)
|
||||
|
||||
func AuthMiddleware(jwtSecret string, userUC users.UseCase) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
const op = "AuthMiddleware"
|
||||
|
||||
authHeader := c.Get("Authorization", "")
|
||||
if authHeader == "" {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
authParts := strings.Split(authHeader, " ")
|
||||
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
parsedToken, err := jwt.ParseWithClaims(authParts[1], &models.JWT{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
token, ok := parsedToken.Claims.(*models.JWT)
|
||||
if !ok {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
err = token.Valid()
|
||||
if err != nil {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
// check if session exists
|
||||
_, err = userUC.ReadSession(ctx, token.SessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, pkg.ErrNotFound) {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
c.Locals(TokenKey, token)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ type Caller interface {
|
|||
ReadUserById(ctx context.Context, id int32) (*models.User, error)
|
||||
UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error
|
||||
DeleteUser(ctx context.Context, id int32) error
|
||||
ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error)
|
||||
}
|
||||
|
||||
type TxCaller interface {
|
||||
|
@ -25,7 +26,7 @@ type PgRepository interface {
|
|||
}
|
||||
|
||||
type ValkeyRepository interface {
|
||||
CreateSession(ctx context.Context, userId int32, role models.Role) (string, error)
|
||||
CreateSession(ctx context.Context, userId int32, role models.Role, userAgent, ip string) (*models.Session, error)
|
||||
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
|
||||
UpdateSession(ctx context.Context, sessionId string) error
|
||||
DeleteSession(ctx context.Context, sessionId string) error
|
||||
|
|
|
@ -47,6 +47,7 @@ type TxOrDB interface {
|
|||
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
|
||||
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
|
||||
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
}
|
||||
|
||||
type Caller struct {
|
||||
|
@ -202,6 +203,36 @@ func (c *Caller) DeleteUser(ctx context.Context, id int32) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ListUsers = "SELECT * FROM users LIMIT ? OFFSET ?"
|
||||
CountUsers = "SELECT COUNT(*) FROM users"
|
||||
)
|
||||
|
||||
func (c *Caller) ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error) {
|
||||
const op = "Caller.ListUsers"
|
||||
|
||||
if pageSize > 20 {
|
||||
return nil, 0, pkg.Wrap(pkg.ErrBadInput, nil, op, "limit > 20")
|
||||
}
|
||||
|
||||
var usersList []*models.User
|
||||
query := c.db.Rebind(ListUsers)
|
||||
|
||||
err := c.db.SelectContext(ctx, &usersList, query, pageSize, (page-1)*pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
query = c.db.Rebind(CountUsers)
|
||||
var count int32
|
||||
err = c.db.GetContext(ctx, &count, query)
|
||||
if err != nil {
|
||||
return nil, 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return usersList, count, nil
|
||||
}
|
||||
|
||||
func handlePgErr(err error, op string) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
|
@ -255,9 +286,9 @@ func ValidRole(role models.Role) error {
|
|||
switch role {
|
||||
case models.RoleAdmin:
|
||||
return nil
|
||||
case models.RoleModerator:
|
||||
case models.RoleTeacher:
|
||||
return nil
|
||||
case models.RoleParticipant:
|
||||
case models.RoleStudent:
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid role")
|
||||
|
|
|
@ -2,6 +2,9 @@ package repository
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-auth/pkg"
|
||||
|
@ -23,31 +26,55 @@ func NewValkeyRepository(db valkey.Client) *ValkeyRepository {
|
|||
|
||||
const sessionLifetime = time.Minute * 40
|
||||
|
||||
func (r *ValkeyRepository) CreateSession(ctx context.Context, userId int32, role models.Role) (string, error) {
|
||||
func sha256string(s string) string {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(s))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
func (r *ValkeyRepository) CreateSession(ctx context.Context, userId int32, role models.Role, userAgent, ip string) (*models.Session, error) {
|
||||
const op = "ValkeyRepository.CreateSession"
|
||||
|
||||
sessionData, sessionId, err := models.NewSession(userId, role)
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: userId,
|
||||
Role: role,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(sessionLifetime),
|
||||
UserAgent: userAgent,
|
||||
Ip: ip,
|
||||
}
|
||||
|
||||
err := session.Valid()
|
||||
if err != nil {
|
||||
return "", pkg.Wrap(pkg.ErrBadInput, err, op, "building session")
|
||||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "validating session")
|
||||
}
|
||||
|
||||
userIdHash := sha256string(strconv.FormatInt(int64(userId), 10))
|
||||
sessionIdHash := sha256string(session.Id)
|
||||
|
||||
sessionData, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "marshaling session")
|
||||
}
|
||||
|
||||
resp := r.db.Do(ctx, r.db.
|
||||
B().Set().
|
||||
Key(fmt.Sprintf("userid:%d:sessionid:%s", userId, sessionId)).
|
||||
Value(sessionData).
|
||||
Ex(sessionLifetime).
|
||||
Key(fmt.Sprintf("userid:%s:sessionid:%s", userIdHash, sessionIdHash)).
|
||||
Value(string(sessionData)).
|
||||
Exat(session.ExpiresAt).
|
||||
Build(),
|
||||
)
|
||||
|
||||
err = resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return "", pkg.Wrap(pkg.ErrBadInput, err, op, "nil response")
|
||||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "nil response")
|
||||
}
|
||||
return "", pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
return nil, pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return sessionId, nil
|
||||
return session, nil
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -67,11 +94,13 @@ func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (*
|
|||
return nil, pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation")
|
||||
}
|
||||
|
||||
sessionIdHash := sha256string(sessionId)
|
||||
|
||||
resp := valkey.NewLuaScript(readSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionId)},
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
|
||||
)
|
||||
|
||||
if err = resp.Error(); err != nil {
|
||||
|
@ -86,12 +115,20 @@ func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (*
|
|||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session storage corrupted")
|
||||
}
|
||||
|
||||
session, err := models.ParseSession(str)
|
||||
var session models.Session
|
||||
|
||||
err = json.Unmarshal([]byte(str), &session)
|
||||
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session corrupted")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
err = session.Valid()
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "validating session")
|
||||
}
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -111,11 +148,13 @@ func (r *ValkeyRepository) UpdateSession(ctx context.Context, sessionId string)
|
|||
return pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation")
|
||||
}
|
||||
|
||||
sessionIdHash := sha256string(sessionId)
|
||||
|
||||
resp := valkey.NewLuaScript(updateSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionId), sessionLifetimeString},
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash), sessionLifetimeString},
|
||||
)
|
||||
|
||||
err = resp.Error()
|
||||
|
@ -140,11 +179,13 @@ func (r *ValkeyRepository) DeleteSession(ctx context.Context, sessionId string)
|
|||
return pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation")
|
||||
}
|
||||
|
||||
sessionIdHash := sha256string(sessionId)
|
||||
|
||||
resp := valkey.NewLuaScript(deleteSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionId)},
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
|
||||
)
|
||||
|
||||
err = resp.Error()
|
||||
|
@ -175,11 +216,13 @@ return dels`
|
|||
func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error {
|
||||
const op = "ValkeyRepository.DeleteAllSessions"
|
||||
|
||||
userIdHash := sha256string(strconv.FormatInt(int64(userId), 10))
|
||||
|
||||
resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:%d:sessionid:*", userId)},
|
||||
[]string{fmt.Sprintf("userid:%s:sessionid:*", userIdHash)},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
|
|
|
@ -11,10 +11,10 @@ type UseCase interface {
|
|||
ReadUserByUsername(ctx context.Context, username string) (*models.User, error)
|
||||
UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error
|
||||
DeleteUser(ctx context.Context, id int32) error
|
||||
CreateSession(ctx context.Context, userId int32, role models.Role) (string, error)
|
||||
CreateSession(ctx context.Context, userId int32, role models.Role, userAgent, ip string) (*models.Session, error)
|
||||
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
|
||||
UpdateSession(ctx context.Context, sessionId string) error
|
||||
DeleteSession(ctx context.Context, sessionId string) error
|
||||
DeleteAllSessions(ctx context.Context, userId int32) error
|
||||
Verify(ctx context.Context, sessionId string) (string, error)
|
||||
ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error)
|
||||
}
|
||||
|
|
|
@ -7,9 +7,6 @@ import (
|
|||
"git.sch9.ru/new_gate/ms-auth/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-auth/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-auth/pkg"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UseCase struct {
|
||||
|
@ -30,20 +27,19 @@ func NewUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
const (
|
||||
TokenKey = "token"
|
||||
)
|
||||
|
||||
func (u *UseCase) CreateUser(ctx context.Context, username string, password string, role models.Role) (int32, error) {
|
||||
const op = "UseCase.CreateUser"
|
||||
|
||||
meId, ok := ctx.Value("userId").(int32)
|
||||
token, ok := ctx.Value(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return 0, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no user id in context")
|
||||
return 0, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
|
||||
}
|
||||
|
||||
me, err := u.userRepo.C().ReadUserById(ctx, meId)
|
||||
if err != nil {
|
||||
return 0, pkg.Wrap(nil, err, op, "can't read user by id")
|
||||
}
|
||||
|
||||
if !me.Role.AtLeast(models.RoleModerator) || me.Role.AtMost(role) && !me.Role.IsAdmin() {
|
||||
if !token.Role.HasPermission(models.Create, models.ResourceAnotherUser) {
|
||||
return 0, pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
|
@ -58,6 +54,19 @@ func (u *UseCase) CreateUser(ctx context.Context, username string, password stri
|
|||
func (u *UseCase) ReadUserById(ctx context.Context, id int32) (*models.User, error) {
|
||||
const op = "UseCase.ReadUserById"
|
||||
|
||||
token, ok := ctx.Value(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
|
||||
}
|
||||
|
||||
if token.UserId == id && !token.Role.HasPermission(models.Read, models.ResourceMeUser) {
|
||||
return nil, pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
if token.UserId != id && !token.Role.HasPermission(models.Read, models.ResourceAnotherUser) {
|
||||
return nil, pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
user, err := u.userRepo.C().ReadUserById(ctx, id)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "can't read user by id")
|
||||
|
@ -65,6 +74,7 @@ func (u *UseCase) ReadUserById(ctx context.Context, id int32) (*models.User, err
|
|||
return user, nil
|
||||
}
|
||||
|
||||
// ReadUserByUsername is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *UseCase) ReadUserByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||
const op = "UseCase.ReadUserByUsername"
|
||||
|
||||
|
@ -78,38 +88,16 @@ func (u *UseCase) ReadUserByUsername(ctx context.Context, username string) (*mod
|
|||
func (u *UseCase) UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error {
|
||||
const op = "UseCase.UpdateUser"
|
||||
|
||||
meId, ok := ctx.Value("userId").(int32)
|
||||
token, ok := ctx.Value(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no user id in context")
|
||||
return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
|
||||
}
|
||||
|
||||
me, err := u.userRepo.C().ReadUserById(ctx, meId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "can't read user by id")
|
||||
if token.UserId == id && !token.Role.HasPermission(models.Update, models.ResourceMeUser) {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
user, err := u.userRepo.C().ReadUserById(ctx, id)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "can't read user by id")
|
||||
}
|
||||
|
||||
hasPermission := func() bool {
|
||||
if me.Id == user.Id && role != nil {
|
||||
return false
|
||||
}
|
||||
if me.Role.IsAdmin() {
|
||||
return true
|
||||
}
|
||||
if role != nil && me.Role.AtMost(*role) {
|
||||
return false
|
||||
}
|
||||
if !me.Role.AtMost(user.Role) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}()
|
||||
|
||||
if !hasPermission {
|
||||
if token.UserId != id && !token.Role.HasPermission(models.Update, models.ResourceAnotherUser) {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
|
@ -137,17 +125,16 @@ func (u *UseCase) UpdateUser(ctx context.Context, id int32, username *string, ro
|
|||
func (u *UseCase) DeleteUser(ctx context.Context, id int32) error {
|
||||
const op = "UseCase.DeleteUser"
|
||||
|
||||
userId, ok := ctx.Value("userId").(int32)
|
||||
token, ok := ctx.Value(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no user id in context")
|
||||
return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
|
||||
}
|
||||
|
||||
me, err := u.ReadUserById(ctx, userId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "can't read user by id")
|
||||
if token.UserId == id && !token.Role.HasPermission(models.Delete, models.ResourceMeUser) {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
if me.Id == id || !me.Role.IsAdmin() {
|
||||
if token.UserId != id && !token.Role.HasPermission(models.Delete, models.ResourceAnotherUser) {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
|
@ -173,17 +160,19 @@ func (u *UseCase) DeleteUser(ctx context.Context, id int32) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (u *UseCase) CreateSession(ctx context.Context, userId int32, role models.Role) (string, error) {
|
||||
// CreateSession is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *UseCase) CreateSession(ctx context.Context, userId int32, role models.Role, userAgent, ip string) (*models.Session, error) {
|
||||
const op = "UseCase.CreateSession"
|
||||
|
||||
sessionId, err := u.sessionRepo.CreateSession(ctx, userId, role)
|
||||
session, err := u.sessionRepo.CreateSession(ctx, userId, role, userAgent, ip)
|
||||
if err != nil {
|
||||
return "", pkg.Wrap(nil, err, op, "cannot create session")
|
||||
return nil, pkg.Wrap(nil, err, op, "cannot create session")
|
||||
}
|
||||
|
||||
return sessionId, nil
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// ReadSession is for internal use only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *UseCase) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
|
||||
const op = "UseCase.ReadSession"
|
||||
|
||||
|
@ -197,6 +186,19 @@ func (u *UseCase) ReadSession(ctx context.Context, sessionId string) (*models.Se
|
|||
func (u *UseCase) UpdateSession(ctx context.Context, sessionId string) error {
|
||||
const op = "UseCase.UpdateSession"
|
||||
|
||||
token, ok := ctx.Value(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
|
||||
}
|
||||
|
||||
if token.SessionId != sessionId {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
if token.SessionId == sessionId && !token.Role.HasPermission(models.Update, models.ResourceOwnSession) {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
err := u.sessionRepo.UpdateSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot update session")
|
||||
|
@ -207,6 +209,19 @@ func (u *UseCase) UpdateSession(ctx context.Context, sessionId string) error {
|
|||
func (u *UseCase) DeleteSession(ctx context.Context, sessionId string) error {
|
||||
const op = "UseCase.DeleteSession"
|
||||
|
||||
token, ok := ctx.Value(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
|
||||
}
|
||||
|
||||
if token.SessionId != sessionId {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
if token.SessionId == sessionId && !token.Role.HasPermission(models.Delete, models.ResourceOwnSession) {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
err := u.sessionRepo.DeleteSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot delete session")
|
||||
|
@ -217,6 +232,19 @@ func (u *UseCase) DeleteSession(ctx context.Context, sessionId string) error {
|
|||
func (u *UseCase) DeleteAllSessions(ctx context.Context, userId int32) error {
|
||||
const op = "UseCase.DeleteAllSessions"
|
||||
|
||||
token, ok := ctx.Value(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
|
||||
}
|
||||
|
||||
if token.UserId != userId {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
if token.UserId == userId && !token.Role.HasPermission(models.Delete, models.ResourceOwnSession) {
|
||||
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
err := u.sessionRepo.DeleteAllSessions(ctx, userId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot delete all sessions")
|
||||
|
@ -225,61 +253,21 @@ func (u *UseCase) DeleteAllSessions(ctx context.Context, userId int32) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
SessionId string `json:"sid"`
|
||||
UserId int32 `json:"sub"`
|
||||
Role models.Role `json:"rle"`
|
||||
ExpiresAt time.Time `json:"exp"`
|
||||
IssuedAt time.Time `json:"iat"`
|
||||
NotBefore time.Time `json:"nbf"`
|
||||
func (u *UseCase) ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error) {
|
||||
const op = "UseCase.ListUsers"
|
||||
|
||||
token, ok := ctx.Value(TokenKey).(*models.JWT)
|
||||
if !ok {
|
||||
return nil, 0, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
|
||||
}
|
||||
|
||||
func (t Token) Valid() error {
|
||||
if err := uuid.Validate(t.SessionId); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.UserId <= 0 {
|
||||
return errors.New("invalid user id")
|
||||
}
|
||||
if t.Role <= 0 {
|
||||
return errors.New("invalid role")
|
||||
}
|
||||
if t.ExpiresAt.IsZero() {
|
||||
return errors.New("invalid exp")
|
||||
}
|
||||
if t.IssuedAt.IsZero() {
|
||||
return errors.New("invalid iat")
|
||||
}
|
||||
if t.NotBefore.IsZero() {
|
||||
return errors.New("invalid nbf")
|
||||
}
|
||||
return nil
|
||||
if !token.Role.HasPermission(models.Read, models.ResourceAnotherUser) {
|
||||
return nil, 0, pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
|
||||
}
|
||||
|
||||
func (u *UseCase) Verify(ctx context.Context, sessionId string) (string, error) {
|
||||
const op = "UseCase.Verify"
|
||||
|
||||
session, err := u.sessionRepo.ReadSession(ctx, sessionId)
|
||||
usersList, count, err := u.userRepo.C().ListUsers(ctx, page, pageSize)
|
||||
if err != nil {
|
||||
return "", pkg.Wrap(nil, err, op, "cannot read session")
|
||||
return nil, 0, pkg.Wrap(nil, err, op, "can't list users")
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(
|
||||
jwt.SigningMethodHS256,
|
||||
Token{
|
||||
SessionId: sessionId,
|
||||
UserId: session.UserId,
|
||||
Role: session.Role,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24),
|
||||
IssuedAt: time.Now(),
|
||||
NotBefore: time.Now(),
|
||||
},
|
||||
)
|
||||
|
||||
signedToken, err := token.SignedString([]byte(u.cfg.JWTSecret))
|
||||
if err != nil {
|
||||
return "", pkg.Wrap(pkg.ErrInternal, err, op, "cannot sign token")
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
return usersList, count, nil
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@ package pkg
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -20,19 +19,19 @@ func Wrap(basic error, err error, op string, msg string) error {
|
|||
return errors.Join(basic, err, fmt.Errorf("during %s: %s", op, msg))
|
||||
}
|
||||
|
||||
func ToGRPC(err error) error {
|
||||
func ToREST(err error) int {
|
||||
switch {
|
||||
case errors.Is(err, ErrUnauthenticated):
|
||||
return status.Errorf(codes.Unauthenticated, err.Error())
|
||||
return http.StatusUnauthorized
|
||||
case errors.Is(err, ErrBadInput):
|
||||
return status.Errorf(codes.InvalidArgument, err.Error())
|
||||
return http.StatusBadRequest
|
||||
case errors.Is(err, ErrNotFound):
|
||||
return status.Errorf(codes.NotFound, err.Error())
|
||||
return http.StatusNotFound
|
||||
case errors.Is(err, ErrInternal):
|
||||
return status.Errorf(codes.Internal, err.Error())
|
||||
return http.StatusInternalServerError
|
||||
case errors.Is(err, NoPermission):
|
||||
return status.Errorf(codes.PermissionDenied, err.Error())
|
||||
return http.StatusForbidden
|
||||
}
|
||||
|
||||
return status.Errorf(codes.Unknown, err.Error())
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
|
2
proto
2
proto
|
@ -1 +1 @@
|
|||
Subproject commit 11644adc889ee8f0e0e90c11ccf386a081ee000f
|
||||
Subproject commit b7ea2e6cc71f7393f9afe722204c5c33b6a6f2b6
|
Loading…
Add table
Reference in a new issue