some refactoring started
This commit is contained in:
parent
5ce5cce076
commit
640255357e
@ -18,7 +18,7 @@ func main() {
|
|||||||
plugin.MustNewPluginLoader(ctx, 8081, plugin.NewPluginStore()).
|
plugin.MustNewPluginLoader(ctx, 8081, plugin.NewPluginStore()).
|
||||||
Run(ctx)
|
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)
|
// TODO move cron on a separate job (k8s cronjob / docker cron)
|
||||||
// cleanupSessionsCron := cleanupsessions.New(pg)
|
// cleanupSessionsCron := cleanupsessions.New(pg)
|
||||||
|
@ -13,38 +13,38 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- draincloud-pg
|
- draincloud-pg
|
||||||
|
|
||||||
rw_2:
|
# rw_2:
|
||||||
image: postgres:17
|
# image: postgres:17
|
||||||
container_name: draincloud-db-rw-2
|
# container_name: draincloud-db-rw-2
|
||||||
ports:
|
# ports:
|
||||||
- 5433:5432
|
# - 5433:5432
|
||||||
environment:
|
# environment:
|
||||||
- POSTGRES_USER=draincloud
|
# - POSTGRES_USER=draincloud
|
||||||
- POSTGRES_PASSWORD=mysuperstrongpassword
|
# - POSTGRES_PASSWORD=mysuperstrongpassword
|
||||||
- POSTGRES_DB=draincloud
|
# - POSTGRES_DB=draincloud
|
||||||
volumes:
|
# volumes:
|
||||||
- draincloud-rw-2:/var/lib/postgresql/data
|
# - draincloud-rw-2:/var/lib/postgresql/data
|
||||||
networks:
|
# networks:
|
||||||
- draincloud-pg
|
# - draincloud-pg
|
||||||
|
|
||||||
rw_3:
|
# rw_3:
|
||||||
image: postgres:17
|
# image: postgres:17
|
||||||
container_name: draincloud-db-rw-3
|
# container_name: draincloud-db-rw-3
|
||||||
ports:
|
# ports:
|
||||||
- 5434:5432
|
# - 5434:5432
|
||||||
environment:
|
# environment:
|
||||||
- POSTGRES_USER=draincloud
|
# - POSTGRES_USER=draincloud
|
||||||
- POSTGRES_PASSWORD=mysuperstrongpassword
|
# - POSTGRES_PASSWORD=mysuperstrongpassword
|
||||||
- POSTGRES_DB=draincloud
|
# - POSTGRES_DB=draincloud
|
||||||
volumes:
|
# volumes:
|
||||||
- draincloud-rw-3:/var/lib/postgresql/data
|
# - draincloud-rw-3:/var/lib/postgresql/data
|
||||||
networks:
|
# networks:
|
||||||
- draincloud-pg
|
# - draincloud-pg
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
draincloud-rw-1: {}
|
draincloud-rw-1: {}
|
||||||
draincloud-rw-2: {}
|
# draincloud-rw-2: {}
|
||||||
draincloud-rw-3: {}
|
# draincloud-rw-3: {}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
draincloud-pg: {}
|
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