some refactoring started
This commit is contained in:
parent
5ce5cce076
commit
640255357e
@ -18,7 +18,7 @@ func main() {
|
||||
plugin.MustNewPluginLoader(ctx, 8081, plugin.NewPluginStore()).
|
||||
Run(ctx)
|
||||
|
||||
pg := postgres.New(ctx, "postgres://draincloud:draincloud-rw-password-123@192.168.1.68:5433/draincloud?sslmode=disable")
|
||||
pg := postgres.New(ctx, "postgres://draincloud:mysuperstrongpassword@127.0.0.1:5432/draincloud?sslmode=disable")
|
||||
|
||||
// TODO move cron on a separate job (k8s cronjob / docker cron)
|
||||
// cleanupSessionsCron := cleanupsessions.New(pg)
|
||||
|
@ -13,38 +13,38 @@ services:
|
||||
networks:
|
||||
- draincloud-pg
|
||||
|
||||
rw_2:
|
||||
image: postgres:17
|
||||
container_name: draincloud-db-rw-2
|
||||
ports:
|
||||
- 5433:5432
|
||||
environment:
|
||||
- POSTGRES_USER=draincloud
|
||||
- POSTGRES_PASSWORD=mysuperstrongpassword
|
||||
- POSTGRES_DB=draincloud
|
||||
volumes:
|
||||
- draincloud-rw-2:/var/lib/postgresql/data
|
||||
networks:
|
||||
- draincloud-pg
|
||||
# rw_2:
|
||||
# image: postgres:17
|
||||
# container_name: draincloud-db-rw-2
|
||||
# ports:
|
||||
# - 5433:5432
|
||||
# environment:
|
||||
# - POSTGRES_USER=draincloud
|
||||
# - POSTGRES_PASSWORD=mysuperstrongpassword
|
||||
# - POSTGRES_DB=draincloud
|
||||
# volumes:
|
||||
# - draincloud-rw-2:/var/lib/postgresql/data
|
||||
# networks:
|
||||
# - draincloud-pg
|
||||
|
||||
rw_3:
|
||||
image: postgres:17
|
||||
container_name: draincloud-db-rw-3
|
||||
ports:
|
||||
- 5434:5432
|
||||
environment:
|
||||
- POSTGRES_USER=draincloud
|
||||
- POSTGRES_PASSWORD=mysuperstrongpassword
|
||||
- POSTGRES_DB=draincloud
|
||||
volumes:
|
||||
- draincloud-rw-3:/var/lib/postgresql/data
|
||||
networks:
|
||||
- draincloud-pg
|
||||
# rw_3:
|
||||
# image: postgres:17
|
||||
# container_name: draincloud-db-rw-3
|
||||
# ports:
|
||||
# - 5434:5432
|
||||
# environment:
|
||||
# - POSTGRES_USER=draincloud
|
||||
# - POSTGRES_PASSWORD=mysuperstrongpassword
|
||||
# - POSTGRES_DB=draincloud
|
||||
# volumes:
|
||||
# - draincloud-rw-3:/var/lib/postgresql/data
|
||||
# networks:
|
||||
# - draincloud-pg
|
||||
|
||||
volumes:
|
||||
draincloud-rw-1: {}
|
||||
draincloud-rw-2: {}
|
||||
draincloud-rw-3: {}
|
||||
# draincloud-rw-2: {}
|
||||
# draincloud-rw-3: {}
|
||||
|
||||
networks:
|
||||
draincloud-pg: {}
|
63
internal/common/request.go
Normal file
63
internal/common/request.go
Normal file
@ -0,0 +1,63 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RequestPool struct {
|
||||
sp sync.Pool
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
ID string
|
||||
Session *models.Session
|
||||
User *models.User
|
||||
ResolveValues sync.Map
|
||||
Metadata map[string]any
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// NewRequest builds a new *Request struct from raw http Request. No auth data validated.
|
||||
func NewRequest(pool *RequestPool, req *http.Request) *Request {
|
||||
out := pool.sp.Get().(*Request)
|
||||
|
||||
cookies := req.Cookies()
|
||||
headers := req.Header
|
||||
|
||||
out.Metadata = make(map[string]any, len(cookies))
|
||||
|
||||
for _, cookie := range cookies {
|
||||
out.Metadata[cookie.Name] = cookie.Value
|
||||
}
|
||||
|
||||
for hname, hval := range headers {
|
||||
out.Metadata[hname] = hval
|
||||
}
|
||||
|
||||
reqID := uuid.NewString()
|
||||
out.ID = reqID
|
||||
return out
|
||||
}
|
||||
|
||||
func GetValue[T any](vals map[string]any, key string) (T, error) {
|
||||
var out T
|
||||
if vals == nil {
|
||||
return out, fmt.Errorf("nil vals map")
|
||||
}
|
||||
rawVal, ok := vals[key]
|
||||
if !ok {
|
||||
return out, fmt.Errorf("value not found in resolve values set")
|
||||
}
|
||||
|
||||
out, ok = rawVal.(T)
|
||||
if !ok {
|
||||
return out, fmt.Errorf("type of a value is unexpected")
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
250
internal/common/request_test.go
Normal file
250
internal/common/request_test.go
Normal file
@ -0,0 +1,250 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetValue_string(t *testing.T) {
|
||||
t.Parallel()
|
||||
type args struct {
|
||||
vals map[string]any
|
||||
key string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": "123",
|
||||
"2": "234",
|
||||
},
|
||||
key: "1",
|
||||
},
|
||||
want: "123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "value not presented",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": "123",
|
||||
"2": "234",
|
||||
},
|
||||
key: "3",
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nil map",
|
||||
args: args{
|
||||
vals: nil,
|
||||
key: "1",
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid type",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": "123",
|
||||
"2": 234,
|
||||
},
|
||||
key: "2",
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetValue[string](tt.args.vals, tt.args.key)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetValue() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetValue_struct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type val struct {
|
||||
a int
|
||||
b string
|
||||
c bool
|
||||
}
|
||||
type args struct {
|
||||
vals map[string]any
|
||||
key string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want val
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": val{
|
||||
a: 1,
|
||||
b: "2",
|
||||
c: true,
|
||||
},
|
||||
"2": "234",
|
||||
},
|
||||
key: "1",
|
||||
},
|
||||
want: val{
|
||||
a: 1,
|
||||
b: "2",
|
||||
c: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "value not presented",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": "123",
|
||||
"2": "234",
|
||||
},
|
||||
key: "3",
|
||||
},
|
||||
want: val{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nil map",
|
||||
args: args{
|
||||
vals: nil,
|
||||
key: "1",
|
||||
},
|
||||
want: val{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid type",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": "123",
|
||||
"2": 234,
|
||||
},
|
||||
key: "2",
|
||||
},
|
||||
want: val{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetValue[val](tt.args.vals, tt.args.key)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetValue() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetValue_structptr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type val struct {
|
||||
a int
|
||||
b string
|
||||
c bool
|
||||
}
|
||||
type args struct {
|
||||
vals map[string]any
|
||||
key string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *val
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": &val{
|
||||
a: 1,
|
||||
b: "2",
|
||||
c: true,
|
||||
},
|
||||
"2": "234",
|
||||
},
|
||||
key: "1",
|
||||
},
|
||||
want: &val{
|
||||
a: 1,
|
||||
b: "2",
|
||||
c: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "value not presented",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": "123",
|
||||
"2": "234",
|
||||
},
|
||||
key: "3",
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nil map",
|
||||
args: args{
|
||||
vals: nil,
|
||||
key: "1",
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid type",
|
||||
args: args{
|
||||
vals: map[string]any{
|
||||
"1": "123",
|
||||
"2": 234,
|
||||
},
|
||||
key: "2",
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetValue[*val](tt.args.vals, tt.args.key)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetValue() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
9
internal/errs/errors.go
Normal file
9
internal/errs/errors.go
Normal file
@ -0,0 +1,9 @@
|
||||
package errs
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorUnauthorized = errors.New("unauthorized")
|
||||
ErrorAccessDenied = errors.New("access denied")
|
||||
ErrorSessionExpired = errors.New("session expired")
|
||||
)
|
17
internal/handler/handler.go
Normal file
17
internal/handler/handler.go
Normal file
@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
|
||||
)
|
||||
|
||||
type Writer interface {
|
||||
Write(ctx context.Context, resp any)
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
Name string
|
||||
RequiredResolveParams []string
|
||||
ProcessFn func(ctx context.Context, req *common.Request, w Writer) error
|
||||
}
|
@ -1 +0,0 @@
|
||||
package jetstream
|
51
internal/processor/processor.go
Normal file
51
internal/processor/processor.go
Normal file
@ -0,0 +1,51 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/domain"
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/errs"
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/handler"
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
rp *common.RequestPool
|
||||
authStorage storage.AuthStorage
|
||||
}
|
||||
|
||||
func (p *Processor) Process(handler *handler.Handler) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
//req := common.NewRequest(p.rp, ctx.Request)
|
||||
// if handler.WithAuth {
|
||||
// if err := p.authorize(ctx, req); err != nil {
|
||||
// p.writeError(ctx, err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) writeError(ctx *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, errs.ErrorAccessDenied):
|
||||
ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
|
||||
Code: http.StatusForbidden,
|
||||
Message: err.Error(),
|
||||
})
|
||||
case errors.Is(err, errs.ErrorSessionExpired):
|
||||
ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
|
||||
Code: http.StatusForbidden,
|
||||
Message: err.Error(),
|
||||
})
|
||||
default:
|
||||
ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: "Internal Error",
|
||||
})
|
||||
}
|
||||
}
|
95
internal/resolvers/auth/auth_resolver.go
Normal file
95
internal/resolvers/auth/auth_resolver.go
Normal file
@ -0,0 +1,95 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/errs"
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/logger"
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models"
|
||||
)
|
||||
|
||||
const (
|
||||
csrfTokenCookie = "__Csrf_token"
|
||||
sessionTokenCookie = "__Session_token"
|
||||
)
|
||||
|
||||
type AuthResolver struct {
|
||||
authStorage storage.AuthStorage
|
||||
}
|
||||
|
||||
func (r *AuthResolver) Resolve(ctx context.Context, req *common.Request) error {
|
||||
return r.authorize(ctx, req)
|
||||
}
|
||||
|
||||
func (p *AuthResolver) authorize(ctx context.Context, req *common.Request) error {
|
||||
session, err := p.getSession(ctx, req)
|
||||
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
||||
return errs.ErrorUnauthorized
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
return errs.ErrorUnauthorized
|
||||
}
|
||||
|
||||
if err := validateSession(ctx, req, session); err != nil {
|
||||
// TODO add audit log entry
|
||||
return errs.ErrorUnauthorized
|
||||
}
|
||||
|
||||
user, err := p.authStorage.GetUserByID(ctx, session.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch user by id: %w", err)
|
||||
}
|
||||
logger.Debug(ctx, "[authorize] user authorized", slog.String("session_id", session.ID.String()))
|
||||
|
||||
req.User = user
|
||||
req.Session = session
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AuthResolver) getSession(ctx context.Context, req *common.Request) (*models.Session, error) {
|
||||
token, err := common.GetValue[string](req.Metadata, sessionTokenCookie)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err)
|
||||
}
|
||||
|
||||
if len(token) == 0 {
|
||||
return nil, fmt.Errorf("session token or csrf token is empty")
|
||||
}
|
||||
|
||||
session, err := d.authStorage.GetSession(ctx, token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch session from repo: %w", err)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func validateSession(ctx context.Context, req *common.Request, session *models.Session) error {
|
||||
if session == nil {
|
||||
return errs.ErrorAccessDenied
|
||||
}
|
||||
|
||||
csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
|
||||
}
|
||||
|
||||
if session.CsrfToken != csrfToken {
|
||||
return errs.ErrorAccessDenied
|
||||
}
|
||||
|
||||
if session.ExpiredAt.Before(time.Now()) {
|
||||
return errs.ErrorSessionExpired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
11
internal/resolvers/resolver.go
Normal file
11
internal/resolvers/resolver.go
Normal file
@ -0,0 +1,11 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
|
||||
)
|
||||
|
||||
type Resolver interface {
|
||||
Resolve(ctx context.Context, req *common.Request) error
|
||||
}
|
Loading…
Reference in New Issue
Block a user